-
Notifications
You must be signed in to change notification settings - Fork 0
Create 142. Linked List Cycle II.md #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1739d7f
749012e
d69358c
258d9c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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分を切れた。なんだか勿体無い気がする。 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 殆どのソフトウェアエンジニアは PC とキーボードでコードを書くと思います。 PC とキーボードで書くことをおすすめします。 ガラケー時代に、ガラケーを使ってオンラインジャッジで問題を解いている人は見かけたことがあります。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
https://google.github.io/styleguide/pyguide.html#2144-decision
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
ですね。明示がないと誤判定による事故が起きる可能性があるんですね。ありがとうございます! |
||
| 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
!=の認識は良さそうです。
is not は「オブジェクトが同一でないかどうか」の判定ですね。is notの働きを聞かれているのに、他のものと合わせる使い方で答えるのはよくないです。もう少し何を比較しているかで、言葉をまとめられると正解でした。また、is notを値の比較と解釈していたら、valだけが重複するものが出た時に訪問済み
諸々ご指摘ありがとうございます。 |
||
| 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 ほどで見積もれそう。 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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);
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pos = pos + 1とスペースを空けたいですね。orpos += 1