diff --git a/142. Linked List Cycle II/142. Linked List Cycle II.md b/142. Linked List Cycle II/142. Linked List Cycle II.md new file mode 100644 index 0000000..e52678c --- /dev/null +++ b/142. Linked List Cycle II/142. Linked List Cycle II.md @@ -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分を切れた。なんだか勿体無い気がする。 + + +# 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: + 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: + 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 ほどで見積もれそう。 + +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 + 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 + ```