Skip to content

82. Remove Duplicates from Sorted List II - #4

Open
kazizi55 wants to merge 3 commits into
mainfrom
82-remove-duplicates-from-sorted-list-ii
Open

82. Remove Duplicates from Sorted List II#4
kazizi55 wants to merge 3 commits into
mainfrom
82-remove-duplicates-from-sorted-list-ii

Conversation

@kazizi55

@kazizi55 kazizi55 commented Sep 7, 2025

Copy link
Copy Markdown
Owner

@kazizi55
kazizi55 marked this pull request as ready for review September 7, 2025 00:10

### Step 2

まずは「重複したら引き継ぐ」パターンで解いてみた。dummy.next を return する前に previous.next = None と書くのが疑問だったが、最後が重複で終わった時 ([1,2,3,3]の場合など)でも previous.next = current としているため、previous の先には重複がまだ残っていることになるため、必要だと理解した。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dummy.next を return する前に previous.next = None と書くのが疑問

これもまあ、約束次第で「次に処理する列車」を後ろにつけておくという約束( previous.next = current )ならば外さないといけません。
「切り離して引き継ぐ」という約束にしておけば外す必要はないです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

おっしゃる通りですね。
確かに以下のようにすれば「切り離して引き継ぐ」になり、previous.next = Noneを最後につける必要は無くなりますね!

class SolutionWithSeparationAndHandOver:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(next=head)
        previous = dummy
        current = head
        val_to_remove = None

        while current is not None:
            if current.val == val_to_remove:
                current = current.next
                continue
            
            if current.next is not None and current.val == current.next.val:
                val_to_remove = current.val
                previous.next = None // 切り離してから引き継ぐ
                current = current.next
                continue
            previous.next = current
            previous = previous.next
            current = current.next
        return dummy.next

@nanae772 nanae772 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

お疲れ様です。全体的にロジックが整理されていて非常に読みやすかったです。私はこの問題のロジックが上手く整理できず苦戦したので、いろいろと勉強になりました。


### Step 1

最初は既存の List を変更する形で行なっていたが、重複している node を完全に排除するには新しい List を作る方がすぐできるのではと思い、実装してみたが、時間が足らず 5 分経過。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新しい List を作る方がすぐできるのでは

この「すぐに」はプログラムの実行速度的な意味でしょうか?
そうであれば新しいListを作るには各ListNodeのオブジェクト生成がかかるのであまり速くはならないのかなと思いました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

分かりづらくてすみません、これは、より実装時間をかけないで完成させられるのではないかという意味合いで書いた次第です。

そうであれば新しいListを作るには各ListNodeのオブジェクト生成がかかるのであまり速くはならないのかなと思いました。

実行速度という観点では確かにそうですね!

if current.next is not None and current.val == current.next.val:
value_to_skip = current.val
continue
previous.next = current

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「上記2つのifのどちらでもない場合ということはここの条件は…」と少し理解に時間がかかったので、コメントやassertなどでここに来る条件が一目で分かるようになっているとより読みやすいかなと個人的には思いました

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。
個人的には、value_to_skipと同じ値ではなくて (value_to_skip != current.val)、かつ 現在の値とその次の値が異なる(current.val != current.next.val) ということは、値がダブっていないということだ、というのは自明に思えたので、あえてコメントとしては残しませんでした。

参考までに、他のパターンと比べてこの step 2の SolutionWithHandOver が分かりづらかった理由を聞いてもよろしいでしょうか?あと、これは「重複を見つけたら引き継ぐ」パターンで書いたのですが、「重複を見つけたら帰るな」だとコメント不要だと思った感じですか?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。
まず結論を先に述べると「コメントやassertはやはり無くてもよいかも」になりました(意見変わってすみません 🙇‍♂️ )

以下、当時の私が上のコメントをした理由になります。

16行目に入る条件は何だろうと考えるときに

  1. 10行目の条件の否定を考えて覚えておく
  2. 13行目の条件の否定を考えて覚えておく
  3. 1,2のandを取る

と思考に数ステップ必要なので、以下のようにコメントかassertがあったらそれを読むだけでよいのでその思考を省略できて分かりやすいかなと考えました。

# value_to_skip != current.val and (current.next is None and current.val != current.next.val)
assert value_to_skip != current.val and (current.next is None and current.val != current.next.val)

またもう1つの解法は前のifが1つだけなので前にあるif条件の否定を考えるだけでよく、そちらは不要だなと思っていました。

…とこのコメントをした当時は考えていたのですが、今になりちょっとそれも微妙かなと思い直しました。

  1. コメントにしろassertにしろ、余分な行を付け加えることはそれを読んでなぜそれが書かれているかを解釈するというコストがかかってトータルで読み手の負担の軽減にはならなさそうなこと。
  2. 将来ロジックに変更があった場合、コメントやassertの整合性を取るコストも増え、それを忘れてしまうとより分かりにくい・ミスリードなコードになってしまうこと。

なのでif2個くらいなら書かないほうがマシかなと今は思っています。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ご丁寧に返答いただき、ありがとうございます! 🙏

コメントにしろassertにしろ、余分な行を付け加えることはそれを読んでなぜそれが書かれているかを解釈するというコストがかかってトータルで読み手の負担の軽減にはならなさそうなこと。
将来ロジックに変更があった場合、コメントやassertの整合性を取るコストも増え、それを忘れてしまうとより分かりにくい・ミスリードなコードになってしまうこと。

全体的に同意です。コメントがあることもコストになるので、あくまでもコードから読み取れないことかつ知ることで読みやすくなること (例えば、Linked List Cycle でいうなら、Hare and Tortoise のリンクなど) をコメントすべきということですよね 👍

previous.next = current
previous = previous.next
current = current.next
previous.next = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここはwhileの中でやるのもいいのかなと思いました。コードは大きく変わらないですが

  • whileの中でpreviousの連結リストが常に一つしかないものだけを指しているようにできる
  • whileの外でやるとやや例外的な処理に見える

ということでwhileの中でやるのが個人的な好みです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oda さんとのコメントでも出てきた、「切り離してから引き継ぐ」ですね。

whileの中でpreviousの連結リストが常に一つしかないものだけを指しているようにできる
whileの外でやるとやや例外的な処理に見える

確かに while の中で処理をする方が previous の動きをよりわかりやすい形で提示できそうですね 👍

@@ -0,0 +1,64 @@
class Solution1:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

私はこのやり方が思いつきませんでした、読んでみてコードもシンプルで分かりやすくてよいなと思いました!

Comment on lines +53 to +54
previous = dummy
current = head

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(全体的にですが)previous, currentは「前のもの」「今見てるもの」くらいのと書き手の視点からの名前になっているので、もう少し伝わりやすい名前をつけてあげるとより分かりやすくなるかなと思いました。
特にcurrentについては一般的には違和感があるようです(私はあまりまだその感覚が身についてないですが)。
Kota-Isayama/leetcode#1 (comment)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current の文脈としては、 previous との対比であえて使ってみていました。
が、変数名に情報がほとんどないというのは同意です。なので一旦 node と tail を使う形で書いてみました!

class SolutionWithClearVariableNames:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        def moveToDistinctNode(node: Optional[ListNode], val_to_skip: int) -> ListNode:
            while node is not None and node.val == val_to_skip:
                node = node.next
            return node
        dummy = ListNode()
        dummy.next = head
        tail = dummy
        node = head

        while node is not None:
            if node.next is not None and node.val == node.next.val:
                node = moveToDistinctNode(node.next, node.val)
                tail.next = None
                continue
            tail.next = node
            tail = tail.next
            node = node.next
        return dummy.next


class Solution3:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
def skipDuplicates(node, value_to_skip):

@nanae772 nanae772 Sep 7, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1まとまりの処理を関数に切り出していただいてるのは非常に読みやすくてよいなと思いました!
名前がskipDuplicatesだけだと何が返ってくるかやや不明瞭な点があるため、自分ならmoveToNextDistinctNodeとかにするかもと思いました。
あと引数や返り値の型アノテーションがあるとより親切かなと思いました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

名前がskipDuplicatesだけだと何が返ってくるかやや不明瞭な点があるため、自分ならmoveToNextDistinctNodeとかにするかもと思いました。

確かにListNode[]を返すのかListNodeを返すのかあるいは何も返さないのかが不明瞭ですね。moveToDistinctNode に変えました!

あと引数や返り値の型アノテーションがあるとより親切かなと思いました。

確かに自分で関数を定義するときは特に型アノテーションがあった方が可読性が上がりますね!

class SolutionWithMoveToDistinctNode:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        def moveToNextDistinctNode(node: Optional[ListNode], val_to_skip: int) -> ListNode:
            while node is not None and node.val == val_to_skip:
                node = node.next
            return node
        dummy = ListNode()
        dummy.next = head
        previous = dummy
        current = head

        while current is not None:
            if current.next is not None and current.val == current.next.val:
                current = moveToNextDistinctNode(current.next, current.val)
                previous.next = None
                continue
            previous.next = current
            previous = previous.next
            current = current.next
        return dummy.next

@t-ooka t-ooka left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

思考過程がしっかりと言語化されていて素晴らしいなと思いました。一点だけコメントをしています!

dummy.next = head
previous = dummy
current = head
value_to_skip = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value_to_skip を変数として持たないでも前後のListNodeでvalが同じかどうかを見て、どこまで飛ばすか判断できるかなと思いました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、その発想はなかったです!

class SolutionWithoutValueToSkip:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(next=head)
        previous = dummy
        current = head

        while current is not None:            
            if current.next is not None and current.val == current.next.val:
                while current.next is not None and current.val == current.next.val:
                    current = current.next
                current = current.next
                continue    
            previous.next = current
            previous = previous.next
            current = current.next
        previous.next = None
        return dummy.next
	

こうみると、説明変数を定義しなくていい代わりに、while が2重になってしまうので、ケースバイケースで使い分けるのが良さそうですね。

return node

dummy = ListNode()
dummy.next = head

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dummy = ListNode(0) とした方が自分はダミーとしての意図が伝わりやすくていいかなと思いました。
あと、ListNode(0, head)にした方が、少し簡潔になると思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dummy = ListNode(0) とした方が自分はダミーとしての意図が伝わりやすくていいかなと思いました。

dummy と命名しているので、ダミーという意図は十分伝わるかなと思ったのと、0よりも undefined にしておく方が初期値に意味がないことを明示できていいかなと思った次第です。
初期値0 は dummy とするみたいな慣例がある感じなのでしょうか?

あと、ListNode(0, head)にした方が、少し簡潔になると思います。

確かに行数は減りますね!
個人的には previous = previous.next と current = current.next に書き方を合わせたかったのであえてそう書きました。ポインターを次にずらす、という同じことはなるべく同じ書き方で表現した方が一貫していてわかりやすいかなと思ったためです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants