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
225 changes: 225 additions & 0 deletions arai60/141_linked_list_cycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
https://leetcode.com/problems/linked-list-cycle/

# Step1

かかった時間:40min

```ruby
# Definition for singly-linked list.
# class ListNode
# attr_accessor :val, :next
# def initialize(val)
# @val = val
# @next = nil
# end
# end

# @param {ListNode} head
# @return {Boolean}
def hasCycle(head)
return false if head.nil? || head.next.nil?
slow, fast = head, head

while !fast.nil? && !fast.next.nil?
slow = slow.next
fast = fast.next.next
return true if slow.val == fast.val
end
false
end
```
思考ログ:
- 解説見たことあったので解法はなんとなく頭に入ってるつもり
- slowとfastのポインタを用意してfastが追い越したらcycle

何がわからなかったか
- `@param {ListNode} head` に入ってくるデータの構造がよくわかってなかった
- こんな感じのコードを書いて`fast`がnilになったりして困っていた
```ruby
def hasCycle(head)
slow = head
fast = head.next

loop do
if slow.val == fast.val
return true
elsif fast.next.nil?
return false
end
slow = slow.next
fast = fast.next.next
end
end
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

このコードで一番謎なのは、nil チェックするのと動かすのが対になっていないところでどう考えているのかが知りたいです。動かす前に確認したいし、確認したら動かしたい気持ちになりませんか。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

https://discord.com/channels/1084280443945353267/1192728121644945439/1194203372115464272

動かせるか確認してから、動かさない
ニンジンを切って、炊飯器のスイッチをいれて、ジャガイモを切れ、みたいな指示書かないじゃないですか。

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.

言われてみれば確かにそうですね。
これを書いた時の気持ちとして、「まず循環してるかの結果が出てたらすぐに判定してループを抜けたい」が先に来てif節を書いてました。

2歩動かせるか確認して動かせなかったらループはない。動かせるなら動かす。同じところにきたらループがあったと宣言

解いてる時にこの順番の発想が浮かびませんでした!ちょっと書き直してみます 👀

- >`@param {ListNode} head` に入ってくるデータの構造がよくわかってなかった
- これはデータ構造が分かってないんじゃなくて、ノードの長さが1だけのときのケースについてケアできてないだけだった
- 答えを見てから書いたコード
```ruby
def hasCycle(head)
return false if head.nil? || head.val.nil?
slow, fast = head, head

while !fast.nil? && !fast.next.nil? # <- fastがnilで落ちる
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

これ、loop do + if return に分解してみて欲しいです。

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.

やってみました!仰ってることが肌で分かった気がします。
フロイドの循環検出法をloop+ifで書く

slow = slow.next
fast = fast.next.next

return true if slow.val == fast.val
end
false
end
```
- パスしなかったのでもう一回消して答えを見てから書いた(一番上のコード)。パスしたのでStep1終わり

何を考えて解いていたか
- アルゴリズムの仕組みとしては分かってたもののコードでどう書いたらその通りに動くのかが掴めなくて時間を使った
- そもそも事前に解説を見たことがあるという半分ズルなので初見だとまず解き方を思いついてなかったと思う

正解してから気づいたこと
- 頭からnilのチェックをするケースを書き始めると混乱するのでやりたいメジャーケース(今回だとwhileの中)から書き始めて、あとから細かいケースについて詰めていくと自分の考えてることと書いてるコードが一致してて書きやすいかもと思った
- off topic気味だけどleetcodeのエディタにダイレクトで書いてるのはいいのかな(IDE使わないようにしてればOKだと思うが)

# Step2

参考にした過去ログなど:
- https://github.com/sendahuang14/leetcode/pull/1/files
- https://github.com/kagetora0924/leetcode-grind/pull/1/files

思考ログ:
- Step1はwhileの条件が読みにくいかも?改善ポイントってこういうのでいいのかな?
- わからなかったので他の人のPRを探す
- https://github.com/sendahuang14/leetcode/pull/1/files
- HashMapでたどったノードをメモする解法。チラッと考えたけど前に見たことある解法で解くイメージが先行して試してなかった。
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を使った解法の方がオーソドックスというか想定解だと思われますね。
tk-hirom/Arai60#1 (comment)

- 辞書に書き込むのと配列にpushするのだと辞書に書き込んだ方がO(1)?でアクセスできるので良さそう?
- そういえば計算量について全然意識できてなかったことに気づく
- 今のアルゴリズムだとどうなるんだ...?
- 時間計算量は最大でO(N)?
- 空間計算量はO(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.

合っていると思います。計算量を見ると、空間計算量がフロイドの解法の方がO(1)で良いですね。setを使うと最悪計算量がO(N)になるかと思います。(すべてのノードの値をsetに入れることになるため。)

- 「より自然な」コードだとたどったノードをメモっていくのが直感的な感じがするので辞書にたどったノードを書いてみる
- と思ったけど辞書のキーがnode.valだったとして、キーに対応する値に何をいれるべきなのか迷った
- 値にboolean入れる想像をしたけど、結局アクセスする時に返ってくる値がややこしくなるのでやめた
- のでとりあえず配列に詰めることにした
- 配列に重複があることをどう判定する?
- `Array#uniq` は重複を排除した配列を返すので、`visited_nodes.length == visited_nodes.uniq.length`で判定はできるけど冗長な感じがする
- RubyにSetがあったことを思い出す
- https://docs.ruby-lang.org/ja/latest/class/Set.html#I_--3C--3C
- >Set#add? は、集合に要素が追加された場合には self を、変化がなかった場合には nil を返します。
- これの返り値で判定できそう
- とりあえずパスするコードは書けた
```ruby
def hasCycle(head)
return false if head.nil? || head.next.nil?

visited_nodes = Set.new
visited_nodes.add(head.val)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

私もStep3のコードのように、headを特別扱いしないバージョンの方が好みです。

current = head.next

while !current.nil? do
has_visited = visited_nodes.add?(current.val).nil?
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

これ、val が unique であるという保証あるんでしたっけ? Set は Object 入れられたと思いますので、そちらのほうが素直では。

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.

確認したのですが、勝手にvalがuniqueだと思い込んでました😇
(leetcode上のサンプルのテストケースは通ってたけどsubmitしたら他のケースで通ってませんでした)

Setにはプリミティブなintとかstringぐらいしか入らないと思い込んでたんですがHashも入るんですね。
ありがとうございます!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

https://docs.ruby-lang.org/ja/latest/library/set.html

Set は内部記憶として Hash を使うため、集合要素の等価性は Object#eql?Object#hash を用いて判断されます。したがって、集合の各要素には、これらのメソッドが適切に定義されている必要があります。

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.

return true if has_visited

current = current.next
end
false
end
```
- セルフツッコミしてみる
- もうちょっと最初の5行ぐらいをシュッと書けるかも
- 先にhead.valをaddするんじゃなくて、そのまま全部whileの中に入れられないかな
- `has_visited = visited_nodes.add?(current.val).nil?`が読みづらい気がする
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

includeメソッド使う方が素直な気がします。
https://docs.ruby-lang.org/ja/latest/class/Set.html#I_--3D--3D--3D

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.

確かに!ドキュメント途中までしか見てませんでした 😇

- `Set#add?`が要素を追加できなかったらnilを返すことを知ってなきゃシュッと読めない

# Step3

かかった時間:15min

```ruby
def hasCycle(head)
visited_nodes = Set.new
current = head

while !current.nil? do
has_visited = visited_nodes.add?(current.val).nil?
return true if has_visited

current = current.next
end
false
end
```
思考ログ:
- 書きながらどうやったら直感的になるか考えた
- 何も考えずに最初から「headをcurrentとして、valを詰める→ポインタが示す先に移動する→ノードがあるか確認する」を繰り返すだけで行けるかも
- 書けた。だいぶ直感的になった気がする。
- このぐらいシュッとしてるなら`visited_nodes.add?(current.val).nil?`の部分については許容できるレベルだと思った
- 変数名については振り返ってなかったけど、見直してもあんまり改善ポイントが浮かばなかった
- 1発で3回連続パスした!終わり。

# Step4

レビューを受けて修正する。
- >これ、val が unique であるという保証あるんでしたっけ? Set は Object 入れられたと思いますので、そちらのほうが素直では。
- https://github.com/canisterism/leetcode/pull/2/files#r1686085757
- valはすべてのノードでuniqueでないので、~Hash~ Node自体を詰めないと同じvalが出てくると正しい結果にならない。
- 修正するとこうなる
```ruby
def hasCycle(head)
visited_nodes = Set.new
current = head

while !current.nil? do
has_visited = visited_nodes.add?(current).nil?
return true if has_visited

current = current.next
end
false
end
```
- SetにHashを入れた時の等価の判定ってどうなるんだっけ。多分key:valueで見ると思うけど...
- https://docs.ruby-lang.org/ja/latest/library/set.html
- >Set は内部記憶として Hash を使うため、集合要素の等価性は Object#eql? と Object#hash を用いて判断されます。
```ruby
{'a': 1}.hash
=> 575188353908168896
{'a': 2}.hash
=> 1910308263874936072
{'a': 1}.eql?({'a': 2})
=> false
{'a': 1}.eql?({'a': 1})
=> true
```

```ruby
set = Set.new
=> #<Set: {}>
set.add({'a': 1})
=> #<Set: {{:a=>1}}>
set.add({'a': 1})
=> #<Set: {{:a=>1}}>
set.add({'a': 2})
=> #<Set: {{:a=>1}, {:a=>2}}>
```
- 合ってそう。
- key:valueでハッシュ値を作ってるぽいけどそれで合ってるのかソースを見に行こうとしたがどうやらCで実装してるぽくて読めなかった
- https://github.com/ruby/ruby/blob/master/hash.c これかな?

>loop do + if return に分解してみて欲しいです。
https://github.com/canisterism/leetcode/pull/2/files#r1685762007
slow, fastのアルゴリズムをloop + if で書き直してみた。

```ruby
def hasCycle(head)
slow = head
fast = head

loop do
return false if fast.nil? || fast.next.nil?

slow = slow.next
fast = fast.next.next
return true if slow == fast
end
end
```
- 「次のノードまで見る→進んでもOKだったのでポインタを動かす→一致してるか判定」の流れで書いたら一発でこれが出てきて自分でビックリした。
- slow, fastのアルゴリズム(フロイドの循環検出法と言うらしい)苦手だと思ってたけど思考の流れが整理されてないから分からないだけだったのかと思うなど