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
109 changes: 109 additions & 0 deletions 141. Linked List Cycle/LinkedListCycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# step1 何も見ずに解く
問題文の説明を理解するのに時間を要す。
何のために`pos`という概念があるのか。
とりあえず循環を検知するコードを書いて動かすことで、実装のためというよりテストケースの説明のための概念であることを理解する。
この`pos`はpositionの略でしょうか?
Copy link

Choose a reason for hiding this comment

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

そうだと思います(私もこの問題に関係無いposが出てきて少し混乱した記憶があります)

Copy link
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます!
ビギナーなのでleetcodeの問題や変数の名付けなどに戸惑ってしまいました。


## 解答
- 普通に書いたら書けたが、問題文を理解するのに時間がかかった。
- 普段IDEに頼りきっているので、leetcode上のただのエディタで書くとメソッド名などまごついた。
- contains()だっけ、has()だっけみたいな。
- O(1)の解答方法は全く思いつかず。
- 計算量
- 時間計算量: O(n)
- 空間計算量: O(n)

```java
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null) return false;
// 巡回済みのnodeを保持するSet
HashSet<ListNode> nodeSet = new HashSet<>();
ListNode checkNode = head;
while (checkNode.next != null) {
if (nodeSet.contains(checkNode)) return true;
nodeSet.add(checkNode);
checkNode = checkNode.next;
}
return false;
}
}
```

# step2 他の方の解答を見る
フロイドの循環検出法を知る。 循環小数の検出などにも使用するよう。
速度違いで2つのポインタを動かすことで、ループがあればいつか必ず追いつくということか。

## 解答
- leetcodeの画面で動かすと、フロイド法は0ms、Set使用時は4msとなる。
- 時間計算量は同じO(n)なのにここまで差がつくのが直感に反した。
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

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。
これくらいの入力であれば、定数倍の影響が大きいということですよね。
見誤らないよう意識するようにします。

Choose a reason for hiding this comment

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

LeetCode の実行時間はブレがあるので、あまり信用しないほうがよさそうです。
手元で実行して計測してみると違いが分かりやすかったです。
kazizi55/coding-challenges#1 (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.

当初気づかなかったのですが、同じプログラムでもleetcodeの実行時間ブレますね。
ありがとうございます!

- Setを使った場合、Hashの計算やメモリの割り当てなどで余計に時間がかかるからだろうか?
- 計算量
- 時間計算量: O(n)
- 空間計算量: O(1)

```java
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) return true;
}
return false;
}
}
```

## 解答2
- Setを使った解法についてもstep1の変数名を少し改良して再実装

```java
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) return false;
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.

確かに後続の実装と意味的に分かれてるので改行入れた方が親切ですね。

HashSet<ListNode> visited = new HashSet<>();
ListNode node = head;
while (node.next != null) {
if (visited.contains(node)) return true;
visited.add(node);
node = node.next;
}
return false;
}
}
```

## step3 3回ミスなく書く
何回か書いたのでスラスラかける。
所要時間 2分程度
## 解答
```java
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) return false;
Copy link

Choose a reason for hiding this comment

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

こっちのチェックは消しても動きますが、visited 構築コストが必ずかかるようになるので、つけておいてもいいでしょう。
ぶらさがり if 文は時々事故の原因になります。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.gblcuhn8bpdi

Copy link
Owner Author

Choose a reason for hiding this comment

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

個人的にはこのようなgurad節的なものを書きたくなります。

確かに毎回{}で囲う方が事故は確実に少なくなりますね。
選べる場合はぶら下がりifを避けるようにいったん心掛けてみます。

HashSet<ListNode> visited = new HashSet<>();
Copy link

Choose a reason for hiding this comment

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

長くはなりますが、 visitedNodes とかのほうが丁寧かなと思いました!
(JavaってlowerCameCaseでしたっけ、調べた感じそうでしたが違ったらすみません。)

Copy link
Owner Author

Choose a reason for hiding this comment

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

やっぱりそちらの方が親切ですね。
型からわかることを書くか書かないかよく悩んでしまいます。

Javaのメソッドや変数名はlowerCamelCaseであってます!

Choose a reason for hiding this comment

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

これも好みかもしれませんが、僕も kt-from-j さんと同様に型からわかることは書かないですね。
ちなみに協会内では、型注釈がない Python でも visited や reached などのように書いている方が多い印象です。node が入ってくることは後の処理から見て自明なのではと個人的には判断しています。

ListNode node = head;
while (node != null && node.next != null) {
Copy link

Choose a reason for hiding this comment

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

これ、node.next のチェックっていります? いや、してはだめということはないですが。

Copy link
Owner Author

Choose a reason for hiding this comment

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

フロイドの循環検出法で書いた時のことが頭の片隅にあり、なんとなくで書いてしまっていたものと思います。

いや、してはだめということはないですが。

このチェックがある場合、ラスト1回のループが不要になるため完全に無意味ではないから駄目ではないということでしょうか。
とはいえループの度にしてもしなくてもいいチェックを行うコストの方が高そうなので、この場合は消した方がベターでしょうね。

if (visited.contains(node)) return true;
visited.add(node);
node = node.next;
}
return false;
}
}
```