Conversation
| if node.next is None or node.val != node.next.val: | ||
| tail.next = node | ||
| tail = tail.next | ||
| node = node.next |
There was a problem hiding this comment.
切断しておいて繋ぐ、が私の意図とは異なるもので伝わっている気がします。
ここの部分は、node が取り除かれないものであると確定して、node が node.next に移った瞬間だと思いますが、この時点で tail.next が node を指しているはずです。
つまり、「dummy_head からたどれるものは、すべて最終的な完成品に含まれると確定したものだけである」とはなっていませんね。
「切断しておいて繋ぐ」の私の意図は、ここで tail.next = None することによって、「dummy_head からたどれるものは、すべて最終的な完成品に含まれると確定したものだけである」ようにしようということです。そうすると return の前の tail.next = None が不要になります。
There was a problem hiding this comment.
コメントありがとうございます。
- 切断しておいて繋ぐ:nodeが取り除かれないと確定したタイミングでtailを更新(tail.nextはそのままnodeの先に繋がる)
- 繋いでおいて切断:今注目しているnode(これ自体は、取り除かれないと確定)について、
node.nextを更新
みたいな認識でした。
たしかに、このコードだと切断のニュアンスはありませんね。
| node = dummy_head | ||
| while node is not None: |
There was a problem hiding this comment.
nodeをNoneチェックする必要があるか疑問に感じました。
というのも、nodeの初期値がNoneの可能性があるheadなら、nodeにはNoneチェックが必要なものが入っていって、nodeがNoneの可能性がない実体で始まるなら、それと同じようにnodeにはNoneの可能性がない実体があるものが入っていくのかなと個人的には考えます。
繋いでから切断、切断してから繋ぐみたいなのも、自分自身ちゃんとは分かっていないので、もしかしたら変な指摘かもしれません🙇♂️
There was a problem hiding this comment.
繋いでから切断、切断してから繋ぐみたいなのも、自分自身ちゃんとは分かっていないので、もしかしたら変な指摘かもしれません🙇♂️
こちらですが、言い換えただけにはなりますが、自分にとって腑に落ちたイメージを話します。
まず、繋いでから切断する、切断しておいて繋ぐという解法のどちらも以下の共通点があります。
- 与えられたsingly-linked listをheadから順に見ていく
- 値に重複があれば、適宜ノードの繋ぐ先を繋ぎかえる
- 返り値として、「重複のない、昇順に並んだsingly-linked listのhead」を返す
次に相違点ですが、「与えられたsingly-linked listをheadから順に見ていく」ループの中で、保持している値に違いがあります。
- 切断しておいて繋ぐ:常に、「回答となるものの一部もしくは全部」、つまり、「重複のない、昇順に並んだsingly-linked list」を保持します。
- 繋いでから切断:「回答となるものの一部もしくは全部 + まだチェックしていないlinked-list」を保持します。
ですので、例えば1 -> 1 -> 2 -> 3 -> 4 -> 4 -> Noneなるリストが入力として与えられた場合、各数値ごとにそれぞれ次のような遷移をします。
- 切断しておいて繋ぐ
(何もチェックしていない) dummy_head -> None 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> None (1までチェックした) dummy_head -> None 2 -> 3 -> 4 -> 4 -> None (2までチェックした) dummy_head -> 2 -> None 3 -> 4 -> 4 -> None (3までチェックした) dummy_head-> 2 -> 3 -> None 4 -> 4 -> None (4までチェックした) dummy_head-> 2 -> 3 -> None None (末尾まで到達) dummy_head -> 2 -> 3 -> None None - 繋いでおいて切断する
(何もチェックしていない) dummy_head -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> None (1までチェックした) dummy_head -> 2 -> 3 -> 4 -> 4 -> None (2までチェックした) dummy_head -> 2 -> 3 -> 4 -> 4 -> None (3までチェックした) dummy_head -> 2 -> 3 -> 4 -> 4 -> None (4までチェックした) dummy_head -> 2 -> 3 -> None (末尾まで到達) dummy_head -> 2 -> 3 -> None
どちらの解法も考えやすい部分と、考えにくい部分があると思います。
切断しておいて繋ぐ場合、ループ内で意味的に変わらないものが返り値の条件を満たしている点では考えやすいですが、一つのリストの繋ぎ換えをしているのに、元のリストと返り値となるリストを別で扱うようなイメージになる点が少し考えにくいです(これは、ノードの繋ぎ変えの順序を間違えるとうまく動作しない面倒さに現れていると思います)。
一方で、繋いでおいて切断するものは、一つのリストを繋ぎかえているイメージはつきやすい一方、返り値の条件を満たすものが最後の最後まで出てこない面倒さがあると思います。また、単にチェック済みのノードの末尾をtailと置くと、tail.next以降にもリストがつながっていることがあり、下手な命名をすると単語の意味が実際と結びつきません。
どちらの解法もstep 4で追加したので、よければ見てみてください(step 3までの解法は、すべて繋いでおいて切断するになっています。認識違いでした)。
nodeをNoneチェックする必要があるか疑問に感じました。
というのも、nodeの初期値がNoneの可能性があるheadなら、nodeにはNoneチェックが必要なものが入っていって、nodeがNoneの可能性がない実体で始まるなら、それと同じようにnodeにはNoneの可能性がない実体があるものが入っていくのかなと個人的には考えます。
こちらはおそらく型に誤解があるのではないかと感じました。nodeの初期値がdummy_head(=ListNode(next=head))となっているため、nodeの型をListNodeと捉えているのではないでしょうか?
僕の想定では、この型はOptional[ListNode]( = ListNode | None)となっています。というのも、nodeを末尾まで探索していくと、サイクルがなければいずれはnode = Noneとなるからです。こちらについては、typeアノテーションしておくべきだったかもしれません。
こちらが参考になれば幸いです。コメントありがとうございます。
There was a problem hiding this comment.
繋いでから切断、切断してから繋ぐ
これ、私が適当に様子を表現しただけで、特に専門用語ではありませんので、それほどこれ自体を真面目に考える必要はないです。
ただ、これ、ループごとに仕事の引き継ぎをしていると思う(物理的にこれを繋ぎ変える仕事をしていると考える、下のリンク参照)と、「こういう状態にして引き継ぐからね」という約束をどうするかはとても大切なはずです。
ichika0615/arai60#3 (comment)
列車
https://discord.com/channels/1084280443945353267/1195700948786491403/1197102971977211966
鎖
https://discord.com/channels/1084280443945353267/1231966485610758196/1239417493211320382
There was a problem hiding this comment.
丁寧な説明ありがとうございます!繋いでから切断、切断してから繋ぐのイメージがわかりました
こちらはおそらく型に誤解があるのではないかと感じました。nodeの初期値がdummy_head(=ListNode(next=head))となっているため、nodeの型をListNodeと捉えているのではないでしょうか?
自分の書き方が悪くて意図を伝えられてませんでした。意図としてはnodeの初期値がListNodeなら、ループが進んでも同様に、nodeをOptionalの付かないListNodeの型にしておける書き方がある、ということを伝えたかったです。自分が書いた1重whileのコードと動作が同じなのに、こちらだと2重whileのコードになっていたので、nodeのNoneチェックするwhileを減らせると思いコメントしました。
| ```python3 | ||
| class Solution: | ||
| def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
| def hasSameValueAsNextNode(node): |
There was a problem hiding this comment.
LeetCode で指定されている関数名は lowerCamel なのですが、 Python では一般的には関数名は lower_snake で書くことが多いと思います。lower_camel で書くことをお勧めいたします。
https://peps.python.org/pep-0008/#function-and-variable-names
Function names should be lowercase, with words separated by underscores as necessary to improve readability.
https://google.github.io/styleguide/pyguide.html#316-naming
function_name
| while node.next is not None and node.val == node.next.val: | ||
| node = node.next | ||
| if isSameAsNext(node.next): | ||
| return skipDuplicates(node.next) |
There was a problem hiding this comment.
同じ値を持つ一連のノードを取り除いたあと、その直後の同じ値を持つ一連のノードを取り除くために再起関数を使うという点が、不必要に複雑に感じました。
| dummy_head = ListNode(next=head) | ||
| node = dummy_head | ||
| while node is not None: | ||
| has_duplicate = False |
There was a problem hiding this comment.
has_duplicate フラグで処理を切り替える部分が不必要に複雑に感じました。 step3 の最初のソースコードのほうがシンプルだと感じました。
問題
https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/description/
次に解く問題
https://leetcode.com/problems/add-two-numbers/description/