考題:串列有沒有環
用雜湊集合記住走過的節點要 O(n) 記憶體;快慢指標(Floyd)只用兩個指標,就能找到環和環的入口。
解法
10
4
題目:給一個單向鏈結串列的開頭,判斷它有沒有繞回自己形成環;有的話,回傳環開始的那個節點。
1/12
S 慢指標、F 快指標(第二階段為 P、Q)相遇處
慢指標 S 和快指標 F 都從頭開始。每一輪 S 走 1 步、F 走 2 步。
兩種解法跑同一個串列
| 答案 | 步數 | 額外記憶體 | Big O | |
|---|---|---|---|---|
| 雜湊集合 | 入口 = 節點 4 | 11 次走訪 | 10 個節點 | 時間 O(n)、記憶體 O(n) |
| Floyd | 入口 = 節點 4 | 26 次指標移動 | 2 個指標 | 時間 O(n)、記憶體 O(1) |
亮起來的是這一步執行的程式碼
function cycleEntryHashSet(next: number[]): number { const seen = new Set<number>(); for (let node = next.length ? 0 : -1; node !== -1; node = next[node]) { if (seen.has(node)) return node; // the first repeat seen.add(node); } return -1;} function cycleEntryFloyd(next: number[]): number { if (next.length === 0) return -1; let slow = 0, fast = 0; do { if (next[fast] === -1 || next[next[fast]] === -1) return -1; slow = next[slow]; fast = next[next[fast]]; } while (slow !== fast); let p = 0; // head and meeting point are equally far from the entry while (p !== slow) { p = next[p]; slow = next[slow]; } return p;}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- 先講雜湊集合:最直覺、一定對,面試官會接著問「能不能不用額外空間」。
- 再講 Floyd,並說明為什麼第二階段一定在入口相遇:相遇時慢指標走了 k 步、快指標 2k 步,多走的 k 是環長的倍數,所以從開頭和從相遇點出發,到入口的距離相同。
和其他主題的關係
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 雜湊集合 空間 O(n):每個走過的節點都存起來 | O(n) | O(n) |
| Floyd 第一階段(判斷有沒有環) 進環之後快指標每輪追近一格,最多一圈就追上 | O(n) | O(n) |
| Floyd 第二階段(找入口) 空間 O(1) | O(n) | O(n) |
空間:O(n) vs O(1)
Big O 實測:n 變大時步數怎麼長
數的是:步數或存下的節點(環從中間開始)
| Big O | n = 1,000 | n = 4,000 | n = 16,000 | n = 64,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 雜湊集合:存的節點 | O(n) | 1,000 | 4,000 | 16,000 | 64,000 | ×64 (×64) |
| Floyd:指標移動 | O(n) | 2,500 | 10,000 | 40,000 | 160,000 | ×64 (×64) |
| Floyd:額外記憶體(指標數) | O(1) | 2 | 2 | 2 | 2 | ×1.0 (×1.0) |
和其他做法比
| 雜湊集合:走訪 | 雜湊集合:存的節點 | Floyd:指標移動 | Floyd:額外記憶體 | |
|---|---|---|---|---|
| n = 100 | 101 | 100 | 250 | 2 個指標 |
| n = 1,000 | 1,001 | 1,000 | 2,500 | 2 個指標 |
| n = 100,000 | 100,001 | 100,000 | 250,000 | 2 個指標 |
環從中間開始(最後一個節點指回第 n/2 個)。兩種方法時間都是 O(n),Floyd 甚至走得比較多(快指標一次跨兩格、第二階段再走一段);差別全在記憶體:雜湊集合要記住每一個節點。
真實世界裡的它
- LeetCode 287「找重複的數」:把陣列看成 i → nums[i] 的串列,重複的數就是環的入口。
- Happy number:反覆把各位數平方相加,用快慢指標判斷會不會掉進迴圈。
- Pollard 的 ρ 演算法(分解質因數)用的就是同一個「ρ 形狀」與快慢指標。
取捨與陷阱
- 快指標走兩步前沒檢查 fast.next:串列沒有環時會在結尾附近存取 null。
- 第二階段讓兩個指標一個走一步、一個走兩步:那只會再相遇一次,不會停在入口。
- 用「節點的值」判斷是否看過:值可能重複,要用節點本身(參考或位址)。