考題:Two Sum
在陣列裡找兩個加起來等於目標的數:暴力兩層迴圈、排序後雙指標、雜湊表一次掃過,成本從 n² 一路降到 n。
解法
目標
10
題目
給一個陣列 nums 和目標 target,回傳兩個不同位置的索引,使它們的值加起來等於 target。例如 nums = [2, 7, 11, 15]、target = 9 → [0, 1]。1/17
nums · target = 66
- 720
- 41
- 462
- 83
- 764
- 485
- 496
- 177
- 278
- 559
雜湊表:數值 → 索引
正在檢查已記進雜湊表答案
找兩個位置,數值加起來等於 66。
| 解法 | 答案 | 檢查+排序比較 | 額外記憶體 | Big O |
|---|---|---|---|---|
| 暴力兩層迴圈 | [6, 7] (49 + 17) | 40 | 0 | O(n²) |
| 排序+雙指標 | [6, 7] (49 + 17) | 29 | 10 格 | O(n log n) |
| 雜湊表一次掃過 | [6, 7] (49 + 17) | 8 | 7 格 | O(n) |
亮起來的是這一步執行的程式碼
function twoSumBrute(nums: number[], target: number): [number, number] | null { for (let i = 0; i < nums.length; i++) { for (let j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] === target) return [i, j]; } } return null;} function twoSumSorted(nums: number[], target: number): [number, number] | null { // Sort indices, not values: the answer is the original positions. const order = nums.map((_, i) => i).sort((a, b) => nums[a] - nums[b]); let lo = 0, hi = order.length - 1; while (lo < hi) { const sum = nums[order[lo]] + nums[order[hi]]; if (sum === target) { return [Math.min(order[lo], order[hi]), Math.max(order[lo], order[hi])]; } if (sum < target) lo++; else hi--; } return null;} function twoSumHash(nums: number[], target: number): [number, number] | null { const seen = new Map<number, number>(); // value -> first index for (let i = 0; i < nums.length; i++) { const j = seen.get(target - nums[i]); if (j !== undefined) return [j, i]; if (!seen.has(nums[i])) seen.set(nums[i], i); } return null;}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- 面試官要看的是你能不能從暴力解一路優化:先講兩層迴圈(O(n²)、正確但慢),再指出瓶頸是「找另一半」這一步,最後用雜湊表把它變成一次查詢。
- 記憶體很緊、或輸入本來就排好序時,排序+雙指標是很好的替代:O(1) 額外空間(已排序時)而且不用雜湊。
和其他主題的關係
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 暴力兩層迴圈 額外空間 O(1) | O(n²) | O(n²) |
| 排序+雙指標 排序佔大頭;索引陣列 O(n) 空間 | O(n log n) | O(n log n) |
| 雜湊表一次掃過 最差是雜湊全部碰撞;O(n) 空間 | O(n) | O(n²) |
空間:O(1) / O(n) / O(n),依序是暴力、排序、雜湊
Big O 實測:n 變大時步數怎麼長
數的是:檢查次數(排序法含排序比較),無解的輸入
| Big O | n = 250 | n = 500 | n = 1,000 | n = 2,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 暴力兩層迴圈 | O(n²) | 31,125 | 124,750 | 499,500 | 1,999,000 | ×64 (×64) |
| 排序+雙指標 | O(n log n) | 1,468 | 3,537 | 8,122 | 18,168 | ×12 (×11) |
| 雜湊表一次掃過 | O(n) | 250 | 500 | 1,000 | 2,000 | ×8.0 (×8.0) |
和其他做法比
| n = 250 | n = 500 | n = 1,000 | n = 2,000 | |
|---|---|---|---|---|
| 暴力兩層迴圈 | 31,125 | 124,750 | 499,500 | 1,999,000 |
| 排序+雙指標 | 1,468 | 3,537 | 8,122 | 18,168 |
| 雜湊表一次掃過 | 250 | 500 | 1,000 | 2,000 |
沒有解的輸入(最差情況):每種做法都得做完全部的工作。n 從 250 變成 2,000,暴力法的檢查次數變成 64 倍,雜湊表只變成 8 倍。排序法的數字包含排序本身的比較次數。
真實世界裡的它
- 變形:輸入已排序(LeetCode 167)→ 直接雙指標;3Sum → 排序後固定一個數,剩下做雙指標,O(n²)。
- 追問:要回傳所有解?有重複數字?資料流一筆筆進來(設計一個 add / find 的類別)?
取捨與陷阱
- 排序之後索引就變了:要排的是索引,或把(值, 原索引)一起排。
- 同一個元素不能用兩次:雜湊表要先查再存,否則 target = 2×x 時會配到自己。
- 雜湊表平均 O(1),最差 O(n):面試時說「平均」比較精確。