Joseph Chen
學習背景與痛點
這篇復盤來自我在學習 LeetCode 過程中與 AI 的對話紀錄——七段對話,橫跨陣列、鏈表、哈希表三大主題, 涵蓋了 #2、#26、#49、#169、#229、#242、#347 共七道題目。 回頭看這些對話,我的問題集中在三個層面:
演算法原理理解
- •Boyer-Moore 投票邏輯「為什麼這樣就能選出多數元素?」
- •Top-K 問題為什麼 Heap 比排序好?
程式碼邏輯 Bug
- •Counter 迭代不等於按頻率排序
- •range(nums) 忘記 len()
- •defaultdict 用錯 key 名稱
解法選擇迷茫
- •哈希表 vs 排序,什麼時候用哪個?
- •dummy_head 是必要的嗎?
- •原地修改和建新陣列有什麼差別?
本次復盤涵蓋的題目
核心知識點拆解
2-1 Boyer-Moore 投票演算法
Boyer-Moore Voting Algorithm 是解決「多數元素」問題的最優解,能在 O(n) 時間、O(1) 空間內找出答案。 它的核心思想可以用一個比喻來理解:
⚔️ 戰爭消消樂比喻
把陣列看成戰場,每個數字是一個士兵。同一方的士兵互相支援(count +1),不同方的士兵互相抵消(count -1)。 當一個陣營的士兵全數陣亡(count = 0),換一個新的候選人繼續。 由於多數元素的數量嚴格超過一半,不管怎麼抵消,最後存活的候選人一定是它。
延伸:#229 Majority Element II(找超過 n/3 的元素)
超過 n/3 的元素最多只有 2 個,所以同時維護兩個候選人即可。 這是 Boyer-Moore 的推廣形式——超過 n/k 的元素最多有 k-1 個,就維護 k-1 個候選人。
2-2 鏈表虛擬頭節點(dummy_head)模式
在學習 #2 Add Two Numbers 時,我一開始想不通為什麼要額外建一個 dummy 節點—— 直接用 head 不行嗎?
為什麼要用 dummy_head?
❌ 無 dummy
沒有 dummy_head:第一個節點需要特別處理
✅ 有 dummy
有 dummy_head:所有節點都用同一套邏輯 current.next = ListNode(val)
❌ 無 dummy
程式碼需要 if head is None 分支,容易出 bug
✅ 有 dummy
不需要任何特殊判斷,程式碼更簡潔一致
dummy = ListNode(0)、current = dummy,最後 return dummy.next。 這個模式能消除大量邊界判斷。2-3 哈希表應用:計數、分組、查找
#242 Valid Anagram 和 #49 Group Anagrams 都是哈希表的應用,但我在實作時遇到了兩個 Python 語法細節。
Python dict 計數的標準寫法
defaultdict(list) 代表 「幫我建一個 dict,當 key 不存在時自動建一個空 list 作為預設值」。 這在分組問題(Group Anagrams)裡特別方便,不需要先 if key not in group: group[key] = []。經典例題與程式碼復盤
#169 Majority Element
Easy給定一個陣列,保證有一個元素出現次數超過 ⌊n/2⌋,找出它。 從最暴力的解法一路演進到 O(1) 空間的 Boyer-Moore。
| 解法 | 時間複雜度 | 空間複雜度 | 備註 |
|---|---|---|---|
| 暴力排序取中 | O(n log n) | O(log n) | 排序後中間必是多數元素 |
| HashMap 計數 | O(n) | O(n) | 直觀,適合先理解問題 |
| Boyer-Moore 投票 | O(n) | O(1) | 最優,需理解原理 |
解法 1:排序(暴力但直觀)
解法 2:HashMap 計數
解法 3:Boyer-Moore(最優)
#347 Top K Frequent Elements
Medium給定一個陣列和整數 k,回傳出現頻率最高的 k 個元素。 Follow-up 要求:時間複雜度必須優於 O(n log n)。 這道題完美展示了演算法的三個層次。
| 解法 | 時間複雜度 | 空間複雜度 | 備註 |
|---|---|---|---|
| 全部排序 | O(n log n) | O(n) | 不符合 Follow-up 要求 |
| Min-Heap(堆) | O(n log k) | O(n + k) | k << n 時顯著優於排序 |
| 桶排序(Bucket Sort) | O(n) | O(n) | 最優,利用頻率範圍固定 |
解法 1:全部排序(不符合 Follow-up)
解法 2:Min-Heap(符合 Follow-up)
解法 3:桶排序(O(n),最優)
一般排序無法突破 O(n log n) 是因為「元素大小的範圍不確定」。 但這題的「頻率」範圍是固定的——最大只可能是 n(全部相同)。 範圍固定就能開一個大小為 n+1 的桶陣列,直接映射,不需要比較排序。
易錯點與避坑指南
Counter 迭代 ≠ 按頻率排序
我在做 #347 時,以為直接迭代 Counter 就能按頻率從高到低取,結果取到的是插入順序,不是頻率順序。
from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
result = []
for num in count: # ← 迭代順序是插入順序,不是頻率
if k > 0:
result.append(num)
k -= 1
return result # ❌ 答案錯誤from collections import Counter
def topKFrequent(nums, k):
count = Counter(nums)
# most_common(k) 才是按頻率由高到低
return [num for num, _ in count.most_common(k)]Counter 繼承自 dict,Python 3.7+ dict 的迭代順序是插入順序。如果需要按頻率排序,必須用 .most_common(k) 或搭配 heapq.nlargest。
range(nums) 忘記 len()
# 在 #26 Remove Duplicates 中犯的錯
def removeDuplicates(nums):
slow = 0
for fast in range(1, nums): # ← TypeError: 'list' object cannot be
... # interpreted as an integerdef removeDuplicates(nums):
slow = 0
for fast in range(1, len(nums)): # ← len() 才能取得陣列長度
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1range() 需要整數,不能傳入 list 本身。這個錯誤在 Python 新手中非常常見,好在 Python 會立刻拋出 TypeError 而不是悄悄給錯誤答案。
defaultdict 用了錯誤的 key
在 #49 Group Anagrams 中,我計算了 key 之後,grouping 時卻把原始字串 i 當成了 key, 導致每個字串都自成一組。
from collections import defaultdict
def groupAnagrams(strs):
group = defaultdict(list)
for i in strs:
key = ''.join(sorted(i))
group[i].append(i) # ❌ 用 i 當 key,每個字串都進自己的桶
return list(group.values())from collections import defaultdict
def groupAnagrams(strs):
group = defaultdict(list)
for word in strs:
key = ''.join(sorted(word))
group[key].append(word) # ✅ 用排序後的 key 當作分組依據
return list(group.values())計算 key 之後忘記用 key 分組——這種「算了但沒用到」的 bug 在 review 時很難發現,因為程式碼邏輯看起來是完整的。
Two Pointer 的指針更新順序
在 #26 中,我把 slow 的更新順序搞反了——應該「先移動 slow、再賦值」, 否則會覆蓋掉 slow 指向的有效元素。
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
nums[slow] = nums[fast] # ❌ 先蓋掉 slow 的值
slow += 1 # 再移動已經太晚了for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1 # ✅ 先移動到下一個空位
nums[slow] = nums[fast] # 再填入新的不重複值Two Pointer 的 slow 指針語義是「下一個有效值要放的位置」。賦值之前要先讓 slow 前進一格,讓出空間。
Boyer-Moore 用在「相對多數」問題上
Boyer-Moore 有一個不能突破的前提:目標元素必須嚴格超過 n/2(或 n/k)。 如果題目只要求「找出現最多次的元素」,Boyer-Moore 可能給出錯誤答案。
總結與延伸
這次學習的三個核心收穫
Boyer-Moore 投票演算法
「消消樂戰爭」的比喻讓我真正理解它為什麼正確,而不只是背下程式碼。關鍵是記住它的限制:只適用於嚴格絕對多數。
dummy_head 是鏈表題的萬用模板
不只 #2,凡是需要建構或修改鏈表的題目,dummy_head + current + dummy.next 這三行幾乎是固定開頭。消除邊界條件的代價只是一個額外節點。
桶排序突破 O(n log n) 的思路
當排序對象的範圍固定時,可以用空間換時間,用下標直接映射代替比較排序。#347 的桶是「頻率」,未來遇到類似的固定範圍問題可以套用。
推薦的延伸練習題
Sort Colors
三指針 Boyer-Moore 排序思想的直接應用,進一步鞏固多指針操作。
Kth Largest Element in an Array
同樣是 Top-K 問題,加入了 QuickSelect 解法,是 Heap 方法的進化版。
Merge Two Sorted Lists
dummy_head 模式的最基本練習,把兩條鏈表合成一條。
Majority Element II
雙候選人的 Boyer-Moore,加上第二輪驗證,是 #169 的絕佳延伸。