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
120 changes: 120 additions & 0 deletions 0142-linked-list-cycle-ii/0142-linked-list-cycle-ii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
## Step 1

前回の問題 linked-list-cycle で、返り値が複雑になった問題。
ひとまずset()で訪問済みを管理するやり方なら返り値を変えるだけでいいので、実装。

- 時間計算量: O(n)
- 空間計算量: O(n)

```python3
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

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

... is not Noneは暗黙的falseを嫌った書き方。

ここまで3分。

さて、予想できていたが、以下、
> Follow up: Can you solve it using O(1) (i.e. constant) memory?

Floydの方法の拡張だと思うが、脳内シミュレーションをしても、出会う場所はリストの長さとサイクルの長さによってまちまち。
わからないので、GPT-5にヒントをもらう。

> * まず **slow**(1歩ずつ)と **fast**(2歩ずつ)を動かすと、ループがある場合いつか同じ場所で会う。
> * その後、**一方を head に戻して、両方を 1 歩ずつ進める**と、次に会う場所がループの入口になる。

おそらく、リストの長さとサイクルの長さを文字で置いて証明できる気がする。
Copy link

Choose a reason for hiding this comment

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

数式での証明も可能ですが、Floydのアルゴリズムは自然言語でも直観的に説明できます。
https://discord.com/channels/1084280443945353267/1246383603122966570/1252209488815984710

しかし、実際の面接におけるFloydのアルゴリズムの出題意図はおおむね次のようなものらしいので、以上の説明を自分で思いつけなくても特に問題はないと思います。
pineappleYogurt/leetCode#3 (comment)

Copy link
Owner Author

Choose a reason for hiding this comment

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

補足ありがとうございます!

しかし、実際の面接におけるFloydのアルゴリズムの出題意図はおおむね次のようなものらしいので、以上の説明を自分で思いつけなくても特に問題はないと思います。
pineappleYogurt/leetCode#3 (comment)

なるほどですね。常識を問う手段ではあるが、それ自体常識ではないってことですね。

数式での証明も可能ですが、Floydのアルゴリズムは自然言語でも直観的に説明できます。
https://discord.com/channels/1084280443945353267/1246383603122966570/1252209488815984710

「かめがスタート地点に戻った時、うさぎはどこにいるでしょうか。実は、うさぎは衝突点にいます。なぜかというと、うさぎは倍速で走っているからです。スタート地点から衝突点を通って衝突点に到達するうさぎルートの長さは、スタートから衝突点に到達するかめルートの2倍だからです。」

の部分が前文から飛躍しているように感じられました。

少し調べ、「亀が歩いた距離がサイクル長の整数倍なので、」という行間を埋めることで理解に達しました。

パズルとして面白かったです。

Copy link

Choose a reason for hiding this comment

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

あれ、「亀が歩いた距離がサイクル長の整数倍なので、」は論理に必要ですか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。

いやたしかに、その衝突点で衝突した(=両者の歩いた距離にサイクル長の整数倍の差が生じた)という事実から簡単に導けますね。

かめがスタート地点に戻った時、うさぎはどこにいるでしょうか。実は、うさぎは衝突点にいます。なぜかというと、うさぎは倍速で走っているからです。スタート地点から衝突点を通って衝突点に到達するうさぎルートの長さは、スタートから衝突点に到達するかめルートの2倍だからです。

ここはそういう意味ですね。


ひとまずはこのアルゴリズムを実装する。

- 時間計算量: O(n)
- 空間計算量: O(1)

```python3
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
meet = None
late = head
while fast is not None and fast.next is not None:
fast = fast.next.next
slow = slow.next
if fast is slow:
meet = fast
break
if meet is None:
return None
while late is not meet:
late = late.next
meet = meet.next
return late
```

変数名、meetはいいけどlateはこれでよかったのかな、という気持ちが残る。

restartとかのがいいのか。でも本人はrestartしてないので違和感があり、遅刻的なニュアンスでlateにした。

でもlateは遅刻というよりlatestのニュアンスで取られそう。他の方がどう命名されたか気になる点。

ここまで16分。

## Step 2

- https://github.com/TrsmYsk/leetcode/pull/2
- Floydの方法の前半、whileの条件に、fastのNoneチェックを集約させた方が良さそう。
- Floydの方法の後半、meet, lateの変数名に、from_start, from_meetingとしていて、fromが位置を元に命名していることを明確にしていていいなと思った。
- ネストが深くなることを躊躇されていて、素敵だなと思った。
- https://github.com/yas-2023/leetcode_arai60/pull/2
- Floydの方法の後半、変数にslow, fastを使い回すと、何しているかが一見してわかりにくいという印象を持った。
- https://github.com/Kaichi-Irie/leetcode-python/pull/20
- Floydの方法の前半と後半で、変数の初期化を直前に行なっているのが、とてもわかりやすいなと思った。
- 自分の実装は、最初に4つ全てを初期化しているが、読み手のワーキングメモリを食わせてしまう。
- 自分の実装で、if fast is slow:が成り立つ瞬間meet=fastと慌てて代入しているが、別にfast(の位置)はすぐに失われないので、ここはフラグを保持すれば十分。

## Step 3

```python3
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
fast = head
slow = head
has_cycle = False
while fast is not None and fast.next is not None:
fast = fast.next.next
slow = slow.next
if fast is slow:
has_cycle = True
break
Comment on lines +95 to +103
Copy link

Choose a reason for hiding this comment

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

私が過去にいただいたコメントですが、こちらを一つの関数として切り出すと見通しがよくなるかなと思います。
理由に関しては以下のリンクをご参照ください。
nanae772/leetcode-arai60#3 (comment)

またフラグによる分岐はgotoよりも構造化されていないという意見もあったので、それも関数化することによって避けることができるかなと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。

確かに、 前半は関数として切り出すことで、前半の変数をもう使わない/操作しないと明示できるのはいいですね。
フラグとgotoも見てみます。

if not has_cycle:
Copy link

Choose a reason for hiding this comment

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

コードの整え方のところを見ておいてください。要はよく使う変形があるんですよ。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.9kpbwslvv3yv

return

from_start = head
from_meeting = fast
while from_start is not from_meeting:
from_start = from_start.next
from_meeting = from_meeting.next
return from_start
```

no cycleでNoneを返すところ、冗長だと思ったので、
```python3
if not has_cycle:
return
Comment on lines +115 to +118
Copy link

Choose a reason for hiding this comment

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

ここは明示的にNoneを返す方が分かりやすいかなと思いました。
returnとだけ書く場合は返り値を一切気にしない関数の場合に使うのが良いように思います。
nanae772/leetcode-arai60#3 (comment)

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。

ですね、同意します。

```
としてみました。