Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions 142. Linked List Cycle II/142. Linked List Cycle II.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Step1: 5分答えを見ずに解く

## 考えたこと
### 問題と例を複数見たところ、「サイクルがつながるノードのindexが欲しいのか」と理解。
しかし、解答欄に初期で与えられているコードの型はListNodeを返せとある。混乱する。

```py
class Solution:

def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
```

indexを返せそうなロジックとして「ノードを辿るごとに、posをインクリメント...」
```py
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
visited = set()
pos = -1 # そもそもここ、headがある時点で0にする処理も必要
while head:
if head in visited:
return pos
pos = pos+1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pos = pos + 1 とスペースを空けたいですね。or pos += 1

visited.add(head)
head = head.next
return None
```
と考えたが、posはパラメータとして渡されない! 141. Linked List Cycleでもこのミスをした。もちろん通らない。indexを返すロジックのところだけを納得しようとしてしまった。Cで実装をしてた時の悪い癖(納得したい癖)みたいなものが抜けていない。

### 「与えられた型に従って、サイクルの結合部分のListNodeを返したら、良い感じに内部でListNodeのindexを返すように処理されるのか?」と思い以下。 合計5分54秒でAC

```py
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
visited = set()
while head:
if head in visited:
return head
visited.add(head)
head = head.next
return None

```

スマホで初めてLeetcodeに書いたので手こずった。acceptedだが、141. Linked List Cycleの時に気を付けていたnodeへのhead代入ができていない。焦ると綺麗にするのをまだ忘れる。負け惜しみだが、これは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.

殆どのソフトウェアエンジニアは PC とキーボードでコードを書くと思います。 PC とキーボードで書くことをおすすめします。

ガラケー時代に、ガラケーを使ってオンラインジャッジで問題を解いている人は見かけたことがあります。

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.

ありがとうございます。

スマホ使用はレビューの時に限定しようと思います。



# Step2: 模範解答を読み、コードを可能な限り読みやすくする

他の人の解答をいくつかみたが、141. Linked List Cycleの時に気をつけていたことと共通するものが多そう。自分の発想の範囲でコードを綺麗にする。また、fastとslowを用いる解法も、考え方の1つとして実装する。解法を思いつくためというよりは、今後何かを理解するときの型として面白そう。

```py
# setを使う解法。自分の解答を綺麗にした。nodeを用意して、headを代入
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
node = head
visited = set()
while node:
if node in visited:
return node
visited.add(node)
node = node.next
return None

```

```py
# 別解: fastとslowを用いる解法
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is 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.

is not None を用いる条件文と、None が falsy であることを利用した暗黙評価の条件文が混在しているようです。どちらかに統一した方がよいと思います。Google Style Guide だと is (not) None 推奨ですね。

https://google.github.io/styleguide/pyguide.html#2144-decision

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.

while node:

while node is not None:

while fast.next and fast.next.next:

while fast is not None and fast.next is not None:

ですね。明示がないと誤判定による事故が起きる可能性があるんですね。ありがとうございます!

return None
slow = head
fast = head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None

slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
```
https://discord.com/channels/1084280443945353267/1195700948786491403/1196010117120925777

一番綺麗だと思う。この解答では、fast自体がNoneになるとnextに走査を送れない。面白い。修正する。


```py
# fastとslowを用いる解法を綺麗にしたもの
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return None

slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None

slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
```

## 関数名への疑問
```py
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
```

```py
def detectCycleJoint(self, head: Optional[ListNode]) -> Optional[ListNode]:
```

でもいいのでは?となった。detectCycleだと「サイクルの何を検出?」となるのでJointか、あまり好ましくないがEntryとかが浮かんだ。自信がないので特に変更はせず、そのままstep3を行った。

# Step3: 10分以内に一回もエラーを出さずに3回書く

```py
# Step2と全く同じ。2分で3連続AC。setを使う解法。自分の解答を綺麗にしたもの。
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
node = head
visited = set()
while node:
if node in visited:
return node
visited.add(node)
node = node.next
return None
```

```py
# Step2と全く同じ。4分程度で3連続AC。fastとslowを用いる解法を綺麗にしたもの
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return None

slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None

slow = head
while slow != fast:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

!=is not の違いを説明できますか。

Copy link
Copy Markdown
Owner Author

@brood0783 brood0783 Aug 18, 2025

Choose a reason for hiding this comment

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

  • != は 値が違うものか判定
  • is not はNoneやboolにも対応
    という認識なので調べました。

!=の認識は良さそうです。オブジェクト型が違っても良いということに注意。

6.10.1. Value comparisons
The operators <, >, ==, >=, <=, and != compare the values of two objects. The objects do not need to have the same type.
https://docs.python.org/3/reference/expressions.html

is not は「オブジェクトが同一でないかどうか」の判定ですね。is notの働きを聞かれているのに、他のものと合わせる使い方で答えるのはよくないです。もう少し何を比較しているかで、言葉をまとめられると正解でした。また、is notを値の比較と解釈していたら、valだけが重複するものが出た時に訪問済みか判定できないですねと判定してしまいますね

6.10.3. Identity comparisons
The operators is and is not test for an object’s identity: x is y is true if and only if x and y are the same object. An Object’s identity is determined using the id() function. x is not y yields the inverse truth value. [4]
https://docs.python.org/3/reference/expressions.html

諸々ご指摘ありがとうございます。

slow = slow.next
fast = fast.next
return slow
```

# その他、考察したこと

## 空間計算量の見積もり
- setを使う解法
- 平均空間計算量: O(N)
ListNodeとsetがそれぞれどの程度要素数に比例してメモリを使うかを手元でみる。

Node1つにつき、どのくらいのサイズのメモリ占有を行っているか見る。

sys.getsizeof(ListNode(1)): 48 bytes
sys.getsizeof(ListNode(1).__dict__): 104 bytes
sys.getsizeof(ListNode(1).val): 28 bytes
sys.getsizeof(ListNode(1).next): 16 bytes

dictってこんな大きいのか。141.LinkedListCycle では、Nodeのインスタンス自体とvalのメモリ占有量を足して76 bytesとしていた(https://github.com/brood0783/arai60/pull/2/files#r2187906575) が、インスタンスとdictの占有量を足して1ノード152 bytes程とするのが良いと思う。(Nodeの数)・152 bytesで見積もれそう。

setはハッシュテーブルの埋まり具合に応じて、リサイズが起きる。the internal load factor(内部負荷率、ハッシュテーブルが埋まっている率)が3/5を超えるとリサイズが起きるようになっているらしい(https://stackoverflow.com/questions/75291343/what-is-the-internal-load-factor-of-a-sets-in-python)


=== setリサイズ境界 ===
境界 1: 要素数 5 で 216 → 728 bytes (+512 bytes)
境界 2: 要素数 19 で 728 → 2264 bytes (+1536 bytes)
境界 3: 要素数 77 で 2264 → 8408 bytes (+6144 bytes)


要素数2^6=64について、 64・3/5=38 近辺でリサイズが起きていないのが不思議だが、(ListNodeの要素数)・107+216 bytes ほどで見積もれそう。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CPythonだと以下の部分ですかね。

if ((size_t)so->fill*5 < mask*3)
    return 0;
return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);

ref. https://github.com/python/cpython/blob/e39255e76d4b6755a44f6d4e63180136c778d2a5/Objects/setobject.c#L194

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.

ありがとうございます。ref見て気づきました。usedの4倍なので要素数64の時はリサイズないですね。助かりました。


ListNodeとsetで見積もった各値にバッファを設けて合計すれば、メモリ占有量は抑えられそう。

- フロイドの循環検出法の場合
- 平均空間計算量: O(1)
- Nodeの数から見積もった値(Nodeの数・152 bytes)にバッファを設ければ、2つのポインター分のメモリも用意できそう。

## 時間計算量の見積もり
- setを使う解法
- 平均時間計算量: O(N)
- バッファを設けて1ノードにつき60nsで抑えられそう。setを使っているので、ノードが多いと探索が速い。

```
サイクル位置別の実行時間分析:
100ノード:
位置 0: 中央値 6167 ns
位置 25: 中央値 5667 ns
位置 50: 中央値 5583 ns
位置 75: 中央値 5750 ns
位置 99: 中央値 5709 ns
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

setを使ってもノードを1個1個処理していかなければいけないのでサイクルの位置が後ろにいくほど時間は上がるかなと思ったのですが、そうでもないんですね。勉強になります。
コードは良いと思いました。

1000ノード:
位置 0: 中央値 53208 ns
位置250: 中央値 50542 ns
位置500: 中央値 50417 ns
位置750: 中央値 50958 ns
位置999: 中央値 54875 ns
```

- フロイドの循環検出法
- 平均時間計算量: O(N)
- バッファを設けて1ノードにつき400nsで抑えられそう。fastがたくさん動かなければいけないので、サイクル位置が後ろになる程時間かかる。

```
サイクル位置別の実行時間分析:
100ノード:
位置 0: 中央値 23042 ns
位置 25: 中央値 21750 ns
位置 50: 中央値 24709 ns
位置 75: 中央値 34166 ns
位置 99: 中央値 35875 ns
1000ノード:
位置 0: 中央値192667 ns
位置250: 中央値199375 ns
位置500: 中央値178750 ns
位置750: 中央値262500 ns
位置999: 中央値356083 ns
```