Add Linked List Cycle II solution and explanation#10
Conversation
Document the implementation and thought process for detecting a cycle in a linked list using both set and two-pointer methods.
|
|
||
| class Solution: | ||
| def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
| # ListNode -> indexとするdictをつくれればいけるのだが、こういう複雑な構造もkeyになれる、つまりHash化できるのかがわからない(多分できない) |
There was a problem hiding this comment.
Pythonではhash値を計算するメソッドが定義されていればハッシュテーブルのキーとして利用することができるようです。
ユーザーが定義したクラスはhash値の計算が可能なので利用可能です。
https://docs.python.org/3/glossary.html#term-hashable
class MyClass:
def __init__(self, value):
self.value = value
a = MyClass(10)
b = MyClass(10)
d = {a: "first", b: "second"}
print(d){<__main__.MyClass object at 0x102a15a90>: 'first', <__main__.MyClass object at 0x1029cb250>: 'second'}
There was a problem hiding this comment.
知らなかった&調べなかったですね
ありがとうございます
| head = head.next | ||
| slow_pointer = slow_pointer.next |
There was a problem hiding this comment.
headが動くのが違和感があるのと、ここのコンテキストだとheadもslow_pointerも1ずつ動いているので別の名前が良いように感じました。
slowとfastで出会った点と、開始点という意味で、from_startとfrom_meeting等はいかがでしょうか。
There was a problem hiding this comment.
個人的にrenameのために変数増やすのが違和感あるのですが、一つの変数に複数の仕事させるほうが常識ないのだと理解しました。
renameを積極的に取り入れようと思います。
コメントありがとうございます。
There was a problem hiding this comment.
rename が多くなる場合は関数に切り出すと見通しが良くなるかなと思います。
具体的には、while head is not slow_pointer: の時点ではslow, fastという概念はもうないので、最初からslow, fastを関数に切り出しておくと、各処理で何をやっているかを変数で表現しやすくなるのではと思います。
...
def find_intersection(node):
slow = node
fast = node
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
return slow
return None
intersection = find_intersection(head)
if intersection is None:
return None
finder = head
while finder is not intersection:
...| ```python | ||
| class Solution: | ||
| def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
| seen_nodes = set() |
There was a problem hiding this comment.
あくまで個人的な感想ですが、seenよりもvisitedの方がノードを処理している様子が想像出来るので好みです。
There was a problem hiding this comment.
今までseenで書いたところ全てで指摘されているので、こういう1ループしてメモする変数はvisitedのほうが適しているんだと理解しました。
コメントありがとうございます。
There was a problem hiding this comment.
こういう1ループしてメモする変数はvisited
おそらくですが連結リストを有向グラフのように見たときに、この処理をグラフ探索のように考えると単に見る(see)よりノードを訪れていく(visit)という単語がしっくり来るのかなというイメージです。
私も最初はcheckedとかにしていて別にseenとかでもいいかなと思ったりしましたが他の方が分かりやすいと思う変数名であることに越したことはないのでvisitedにしています。
There was a problem hiding this comment.
私は seen でよいと思います。
Visitor pattern などというので visited も自然なものでしょう。
変数名については LLM に聞いてみるのも一つです。
nanae772
left a comment
There was a problem hiding this comment.
お疲れ様です。最近同じ問題を解いたので少しコメントさせていただきました。
| ``` | ||
|
|
||
| だいぶすっきりした。 | ||
| Linked List Cycleでもあった、2 pointerを用いた方法も書こうとしたが、追いついたところが the head of a linked list になるわけではなく厳しい。slow_pointerの動いた数を保存しておいて、1つずつ前にずらせばどこかで答えに当たる。2分探索すればそれほどかからないが、正攻法ではないように思える。 |
There was a problem hiding this comment.
この2分探索というのはheadから衝突地点のノードを一直線で見たときに、どこかのノードで「サイクルの中である」というbool値がFalseからTrueに切り替わるのでその境界を二分探索で見つけるということでしょうか?
その場合、「サイクルの中である」ことを判定する処理はどのように行う想定でしたでしょうか?
There was a problem hiding this comment.
この2分探索というのはheadから衝突地点のノードを一直線で見たときに、どこかのノードで「サイクルの中である」というbool値がFalseからTrueに切り替わるのでその境界を二分探索で見つけるということでしょうか?
その意味です
その場合、「サイクルの中である」ことを判定する処理はどのように行う想定でしたでしょうか?
衝突時点から1周するまでに見ているノードと一致する瞬間があるかを見る想定でした
There was a problem hiding this comment.
ご回答ありがとうございます。
まず二分探索について、配列とは異なり連結リストAのi番目の要素A[i]にアクセスするためには毎回先頭からiステップ進めなければいけないので二分探索の各チェックごとにO(N)かかり全体としての計算量は
O(NlogN) * (サイクル内判定にかかる計算量)
となるかと思われます。
一方で単に先頭から1つずつ愚直に確かめていく線形探索は
O(N) * (サイクル内判定にかかる計算量)
となるのでこの場合は二分探索を採用するメリットがあまり無いのではないかと考えました。
衝突時点から1周するまでに見ているノードと一致する瞬間があるかを見る想定でした
こちら理解できました!
私は「サイクルの長さを求めてから見ているノードからスタートしてその長さ分nextを見ていって元に戻ってこれたらTrue」みたいなことをしていたのですが、サイクルの長さを求めるということをしなくてもよい分そちらの方がスマートですね。良いやり方だと思いました。
|
|
||
| - 最初pos(index)を返すものと勘違いしてごちゃごちゃしてしまった。> the `head` of a linked list, return _the node where the cycle begins_) と明記してあったが、見逃している。返り値の型も見逃していて2敗。そもそもの英語力が足りていない話もある。 | ||
| - seen_nodeは単にnodeで良さそう | ||
| - `while node is not None:`でループを回したい(無限ループはどこで抜けるか見るのに認知負荷が高いため)。is_cycledが使えなくなるが、ループ抜けた時に`node is None`かどうかで判断すれば良さそう。 |
There was a problem hiding this comment.
(無限ループはどこで抜けるか見るのに認知負荷が高いため)
安易に無限ループを使ってしまいがちなのですが、言われてみると確かにどこでbreak, returnしているか注意深く見ないといけないので認知負荷がかかりますね…。気をつけようと思いました。
| slow_pointer = head.next | ||
| fast_pointer = head.next.next |
There was a problem hiding this comment.
私はこの部分を
slow_pointer = head
fast_pointer = headとどちらもヘッドからスタートしたのですが一個進めたところからでもいいですね。
こう書かれている方がslow,fastの意味が一目瞭然なので良いのかもしれないと思いました。
There was a problem hiding this comment.
どちらもheadにしてしまうと169行目のwhileの判定に引っかかるというプログラム面での問題もあり、こうしています。
There was a problem hiding this comment.
確かにそうですね。私はwhileの判定をfast_pointer is not None and fast_pointer.next is not Noneのほうにしていたので気づきませんでした。補足ありがとうございます!
| ```python | ||
| class Solution: | ||
| def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
| seen_nodes = set() |
| slow_pointer = head.next | ||
| fast_pointer = head.next.next | ||
|
|
||
| while slow_pointer is not fast_pointer: |
There was a problem hiding this comment.
ここでは fast_pointer が NULL にならないこと(循環がないこと)を判定しておいて、
ループの中で、slow_pointer == fast_pointer を判定するやり方もあるかと思いました。
while fast_pointer and fast_pointer.next:
slow_pointer == slow_pointer.next
fast_pointer == fast_pointer.next.next
if slow_pointer == fast_pointer:
slow_pointer = head
while slow_pointer != fast_pointer:
slow_pointer = slow_pointer.next
fast_pointer = fast_pointer.next
return fast_pointer
return None
みたいな感じで。
There was a problem hiding this comment.
自分もこのやり方をとりますね。
head とそれ以降のケースで NULL ではないことを判定するロジックを分ける必要はないかなと考えています。
| ``` | ||
| 8minくらい | ||
|
|
||
| - 最初pos(index)を返すものと勘違いしてごちゃごちゃしてしまった。> the `head` of a linked list, return _the node where the cycle begins_) と明記してあったが、見逃している。返り値の型も見逃していて2敗。そもそもの英語力が足りていない話もある。 |
There was a problem hiding this comment.
全く同じ間違えをしました笑
型ヒント優先で、問題文の太字に気をつける意識を持つと修正されるかと思います(自分も修正途中ですが)
| ```python | ||
| class Solution: | ||
| def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
| if head is None or head.next is None: |
There was a problem hiding this comment.
これ、ノード1個の時のnextがNoneの場合を弾いてるんですね。勉強になります。
There was a problem hiding this comment.
関数上ではhead = Noneもありえて、None.nextは動かないのでそれ予防ですね
| head = head.next | ||
| slow_pointer = slow_pointer.next | ||
|
|
||
| return head |
There was a problem hiding this comment.
akmhmgc さんがすでにご指摘ですが、自分もheadを動かすのに抵抗があるので
node = head
などの代入を使ってもいいと思いました。
There was a problem hiding this comment.
Pull Request Overview
This PR adds a comprehensive solution and explanation for the LeetCode problem "Linked List Cycle II" which requires finding the node where a cycle begins in a linked list.
- Provides two different approaches: hash set method and Floyd's cycle detection algorithm (two pointers)
- Includes detailed explanations of the mathematical reasoning behind the two-pointer approach
- Documents the iterative problem-solving process across multiple steps
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
|
|
||
| class Solution: | ||
| def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
| # ListNode -> indexとするdictをつくれればいけるのだが、こういう複雑な構造もkeyになれる、つまりHash化できるのかがわからない(多分できない) |
There was a problem hiding this comment.
The comment contains inaccurate information. Python's ListNode objects are hashable by default (using object identity) and can be used as dictionary keys or set elements. The comment should be corrected to reflect that ListNode objects can indeed be hashed and used in sets/dictionaries.
| # ListNode -> indexとするdictをつくれればいけるのだが、こういう複雑な構造もkeyになれる、つまりHash化できるのかがわからない(多分できない) | |
| # ListNodeオブジェクトはPythonではデフォルトでハッシュ可能(オブジェクトのIDで)なので、setやdictのキーとして使える。 |
| Linked List Cycleでもあった、2 pointerを用いた方法も書こうとしたが、追いついたところが the head of a linked list になるわけではなく厳しい。slow_pointerの動いた数を保存しておいて、1つずつ前にずらせばどこかで答えに当たる。2分探索すればそれほどかからないが、正攻法ではないように思える。 | ||
| 考えてもわからなかったため、パス | ||
|
|
There was a problem hiding this comment.
The explanation about binary search approach is unclear and potentially misleading. The comment suggests using binary search to find the cycle start, but this doesn't align with the standard Floyd's cycle detection algorithm that follows. Consider clarifying or removing this confusing explanation.
| Linked List Cycleでもあった、2 pointerを用いた方法も書こうとしたが、追いついたところが the head of a linked list になるわけではなく厳しい。slow_pointerの動いた数を保存しておいて、1つずつ前にずらせばどこかで答えに当たる。2分探索すればそれほどかからないが、正攻法ではないように思える。 | |
| 考えてもわからなかったため、パス | |
| Linked List Cycleでもあった、2 pointerを用いた方法(Floydの循環検出アルゴリズム)が標準的な解法である。追いついた場所からheadとslow_pointerを同時に進めていくことで、サイクルの開始点を特定できる。 |
| - fast_pointerが辿った道のり = x + y + z + y | ||
| - ここで、fast_pointerが辿った道のり = slow_pointerが辿った道のり* 2であることから、 | ||
| - x = z | ||
| - よって、headとslowを進めて、邂逅した場所が the head of a linked list. |
There was a problem hiding this comment.
The term '邂逅した場所が the head of a linked list' is confusing. It should clarify that the meeting point of head and slow pointer is 'the start of the cycle' rather than 'the head of a linked list'.
| - よって、headとslowを進めて、邂逅した場所が the head of a linked list. | |
| - よって、headとslowを進めて、邂逅した場所がサイクルの開始点(the node where the cycle begins)となる。 |
| if not is_cycled: | ||
| return None |
There was a problem hiding this comment.
この部分、is_cycled = False のところで return None すれば、フラグを一つ消せますね。
この順序の入れ替えの部分を見ておいていただけますか。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.4qnnfvpo8ij5
This problem:
https://leetcode.com/problems/linked-list-cycle-ii/description/
Next problem:
https://leetcode.com/problems/valid-parentheses/description/