跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · 考題:串列有沒有環

考題:串列有沒有環

用雜湊集合記住走過的節點要 O(n) 記憶體;快慢指標(Floyd)只用兩個指標,就能找到環和環的入口。

解法
10
4

題目:給一個單向鏈結串列的開頭,判斷它有沒有繞回自己形成環;有的話,回傳環開始的那個節點。

1/12
0SF123456789
S 慢指標、F 快指標(第二階段為 P、Q)相遇處

慢指標 S 和快指標 F 都從頭開始。每一輪 S 走 1 步、F 走 2 步。

兩種解法跑同一個串列
答案步數額外記憶體Big O
雜湊集合入口 = 節點 411 次走訪10 個節點時間 O(n)、記憶體 O(n)
Floyd入口 = 節點 426 次指標移動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 On = 1,000n = 4,000n = 16,000n = 64,000成長倍數:實測(理論)
雜湊集合:存的節點O(n)1,0004,00016,00064,000×64 (×64)
Floyd:指標移動O(n)2,50010,00040,000160,000×64 (×64)
Floyd:額外記憶體(指標數)O(1)2222×1.0 (×1.0)

和其他做法比

雜湊集合:走訪雜湊集合:存的節點Floyd:指標移動Floyd:額外記憶體
n = 1001011002502 個指標
n = 1,0001,0011,0002,5002 個指標
n = 100,000100,001100,000250,0002 個指標

環從中間開始(最後一個節點指回第 n/2 個)。兩種方法時間都是 O(n),Floyd 甚至走得比較多(快指標一次跨兩格、第二階段再走一段);差別全在記憶體:雜湊集合要記住每一個節點。

真實世界裡的它

  • LeetCode 287「找重複的數」:把陣列看成 i → nums[i] 的串列,重複的數就是環的入口。
  • Happy number:反覆把各位數平方相加,用快慢指標判斷會不會掉進迴圈。
  • Pollard 的 ρ 演算法(分解質因數)用的就是同一個「ρ 形狀」與快慢指標。

取捨與陷阱

  • 快指標走兩步前沒檢查 fast.next:串列沒有環時會在結尾附近存取 null。
  • 第二階段讓兩個指標一個走一步、一個走兩步:那只會再相遇一次,不會停在入口。
  • 用「節點的值」判斷是否看過:值可能重複,要用節點本身(參考或位址)。

LeetCode 練習