主題: 學習筆記

TypeScript 最長連續序列:為什麼 Set 掃描不是 O(n²)

從排序解法進步到 Set 解法,推導只從序列起點展開的關鍵,並嚴謹說明最長連續序列的預期 O(n) 複雜度。

我一開始會想到「去重、排序、再掃描」,而且那個解法確實能得到正確答案;問題是題目要求預期 O(n),排序已經讓它成為 O(n log n)。在 AI 輔助下改成 Set 後,我真正需要補上的不是另一段程式碼,而是:為什麼外層迴圈裡還有 while,總工作量卻不會變成 O(n²)

題目

給定一組未排序整數,找出最長連續整數序列的長度。重複數字只算一次,不得使用排序,並以預期 O(n) 時間完成。

[100, 4, 200, 1, 3, 2, 2]
→ 4,因為最長序列是 [1, 2, 3, 4]

排序版為什麼不符合限制

去重後排序,再比較相鄰數字,是正確而直觀的解法。掃描本身是 O(n),但比較排序通常以 O(n log n) 分析,所以整體無法達到題目要求。

這個版本仍有學習價值:它先說清楚「連續」的判斷。但面試遇到明確的 O(n) 限制時,必須找到不靠全域排序的查找方式。

關鍵:只從序列起點往後走

先把所有數字放進 Set 去重。對每個 num,若 num - 1 也存在,num 就不是序列起點,直接略過。只有找不到前一個數字時,才從 num 開始向後尋找 num + 1num + 2

例如 {1, 2, 3, 4} 中,只有 1 會啟動 while234 都因為存在前一個數字而跳過。若從每個數字都往後掃,則會分別走長度 4、3、2、1;在一條長序列上,總和會成為 O(n²)

TypeScript 解法

function longestConsecutive(nums: readonly number[]): number {
  const values = new Set(nums);
  let longest = 0;

  for (const num of values) {
    if (values.has(num - 1)) continue;

    let length = 1;
    while (values.has(num + length)) {
      length += 1;
    }

    longest = Math.max(longest, length);
  }

  return longest;
}

為什麼所有 while 加起來仍是線性

不能只因為 length 持續增加,就宣稱每個數字只走一次。真正的理由是「只有序列起點能進入 while」。去重後的每個數字只屬於一條連續序列,而每條序列只會被它的最小值展開一次。

外層 for 查看每個唯一數字一次;所有內層 while 合計也只走過這些序列中的數字一次。因此,在 Set.has() 採常見的平均常數時間模型時,預期時間是 O(n),額外空間是 O(n)

語言規格並沒有要求 JavaScript Set 一定使用 hash table 或保證 O(1)。ECMAScript 要求平均存取時間必須次線性,實作也可以使用 tree。把這題說成預期 O(n),是在面試常用的 hash-set 模型下分析,而不是對所有 engine 的最壞情況保證。

最小驗證

import assert from "node:assert/strict";

assert.strictEqual(longestConsecutive([100, 4, 200, 1, 3, 2, 2]), 4);
assert.strictEqual(longestConsecutive([0, -1, 1, 2, -2]), 5);
assert.strictEqual(longestConsecutive([]), 0);
assert.strictEqual(longestConsecutive([7, 7, 7]), 1);

這些案例涵蓋重複值、負數、空陣列與只有一個唯一值。它們不需要測試框架,卻能在核心行為退化時直接失敗。

常見陷阱

  • 使用排序,答案雖然正確,卻違反預期 O(n) 的限制。
  • 從每個數字都向後展開,長序列會退化成 O(n²)
  • 忘記先去重,重複值會干擾計數或造成重複工作。
  • 看到巢狀迴圈就直接判定 O(n²),沒有計算元素實際被內層迴圈處理幾次。
  • Set.has() 的平均常數時間模型說成 ECMAScript 的硬性保證。

我學到什麼

  • 題目限制常在提示需要哪種資料結構;禁止排序時,快速 membership lookup 是明顯方向。
  • 巢狀迴圈不必然是平方時間,重點是所有內層迭代加總後的工作量。
  • num - 1 的判斷不是小優化,而是避免重複展開同一序列的正確性與複雜度核心。
  • AI 提供的解法仍需要我能用 amortized analysis 自己解釋,才算真正理解。

外部參考連結