Hash Map Cheatsheet(雜湊表速查)

雜湊、堆疊與佇列優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備 更新於 Nov 12, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 鍵→值的題型模式:查找、分組、索引表、前綴和表、重新映射。 另見 — 題目集:hash_map_examples.md — 經典題目寫成題目卡,各自歸在它所歸約到的下列模板底下。 鄰近的 cheatsheet:hashing.md — 雜湊本身怎麼運作,以及計數與 rolling hash 的慣用寫法;set.md — 只管有沒有,不存值;Collection.md — 一開始該選哪個容器。

LeetCode 題目清單

時間複雜度

資料結構 搜尋 插入 刪除 最小/最大
雜湊表(平均) O(1) O(1) O(1) O(n)

表中是平均情況。最壞情況(所有鍵都碰撞):O(n)。 求最小/最大值必須全掃,因為雜湊不保證任何順序。

總覽

雜湊表(Hash Table/Dictionary)是最基本的資料結構之一,提供高效率的鍵值儲存與取用。

關鍵性質

  • 複雜度:見上面的時間複雜度表;空間是 O(n)
  • 底層實作:陣列 + 鏈結串列/紅黑樹(Java HashMap)
  • 雜湊碰撞:用鏈接法(chaining)或開放定址法處理

什麼時候會發生雜湊碰撞

  • 負載因子 > 0.75:效能開始退化
  • 雜湊函式不好:很多鍵落到同一個 bucket
  • Java HashMap:鏈結串列長度 > 8 時會轉成紅黑樹
  • 解法:鏈接法(每個 bucket 一條串列或一棵樹)或開放定址 —— 見 hash_map_collision.md

為什麼查找是 O(1)

  • FAQ
    • 為什麼 hashmap 的搜尋時間複雜度 ~= O(1)?怎麼解釋?
      • 一句話:O(1) 是平均與最好情況。最壞情況可能是 O(N)(雜湊碰撞)
      • 雜湊函式很關鍵 -> 決定資料怎麼存、以及碰撞會不會發生
      • 操作
        • 插入
          • 拿到 key,用雜湊函式算出 hash 值
          • 依 hash 值在記憶體中找到對應 bucket
          • 把 key 和 value 存進該 bucket
        • 查詢
          • 依 key 算出 index
          • 依 index 找到 bucket 位置
            • Java 用位元運算 int pos = (n - 1) & hash 算出它(表的大小 n 是 2 的冪),所以找 bucket 是 O(1) —— 不必掃過所有元素
          • 走訪該 key 底下的所有元素(只有一個的話就一次)
          • 回傳 value

什麼時候該用/不該用

基本操作

  • get:從 dict 取值,鍵不存在時給預設值
python
d = {'a': 1, 'b': 2}
d['a']  # -> 1

d.get('a')  # -> 1

d.get('c', 0)  # -> 0

d.get('z')

python
#-------------------------------------------------------------------------------
# setdefault : will creatte key if key NOT existed (with value as well if defined)
#-------------------------------------------------------------------------------

# syntax
d.setdefault(new_key)
d.setdefault(new_key, new_value)

# 662 Maximum Width of Binary Tree
car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964
}

# example 1) insert key "my_key", since my_key not existed, -> make it as new key and value as None (since not defined)
car.setdefault("my_key")
print (car)
# In [18]: car
# Out[18]: {'brand': 'Ford', 'model': 'Mustang', 'year': 1964, 'my_key': None}

# example 2) insert key "color", since my_key not existed, -> make it as new key and value as white
car.setdefault("color", "white")
print (car)
# Out[22]:
# {'brand': 'Ford',
#  'model': 'Mustang',
#  'year': 1964,
#  'my_key': None,
#  'color': 'white'}
  • 對 hashmap (dict) 做排序
python
# https://stackoverflow.com/questions/613183/how-do-i-sort-a-dictionary-by-value

x = {1: 2, 3: 4, 4: 3, 2: 1, 0: 0}
x.items()  # -> dict_items([(1, 2), (3, 4), (4, 3), (2, 1), (0, 0)])

#----------------------------------
# Sort hashMap by key/value !!!
#----------------------------------
x = {1: 2, 3: 4, 4: 3, 2: 1, 0: 0}
# note : have to use sorted(xxx, key=yyy), instead of xxx.sorted(....)
### NOTE this !!! : x.items()
sorted_x = sorted(x.items(), key=lambda kv: kv[1])
print (sorted_x)
# [(0, 0), (2, 1), (1, 2), (4, 3), (3, 4)]

x = {1: 2, 3: 4, 4: 3, 2: 1, 0: 0}
sorted_x = sorted(x.items(), key=lambda kv: kv[0])
print (sorted_x)
# [(0, 0), (1, 2), (2, 1), (3, 4), (4, 3)]

# 451  Sort Characters By Frequency
import collections
class Solution(object):
    def frequencySort(self, s):
        count = collections.Counter(s)
        count_dict = dict(count)
        """
        NOTE this !!!
            1. use sorted()
            2. count_dict.items()
        """
        count_tuple_sorted = sorted(count_dict.items(), key=lambda kv : -kv[1])
        res = ''
        for item in count_tuple_sorted:
            res += item[0] * item[1]
        return res
python
# dict values -> array
mydict = {'a':['a1','a2','a3'], 'b':['b1','b2','b3']}

res = [mydict[x] for x in mydict]

print (res)  # -> [['a1', 'a2', 'a3'], ['b1', 'b2', 'b3']]

參考資料

辨識

題目敘述裡的訊號 限制條件線索 該用什麼 不是這個的情況
「回傳兩個加起來等於 target 的數的索引」 未排序,n ≤ 10⁴–10⁵ → O(n) 模板 1 查補數 輸入已排序 → 雙指標,O(1) 空間(2_pointers.md)
「anagram」、「出現最多/最少次」、「第一個不重複的」、「a 能不能用 b 的字母拼出來」 只有 26 個小寫字母 → 一個 int[26] 就是 map 模板 2 頻率計數 只問存不存在、不管次數 → set.md
「把在 … 意義下相同的字串/點分成一組」 n 個長度 k 的字 → O(n·k log k) 模板 3 用算出來的鍵分組 關係不具遞移性(「只差一個字母」)→ 圖/BFS
「和等於 k/可被 k 整除的子陣列有幾個」,值可能是負的 n ≤ 2·10⁴–10⁵ → O(n),不是枚舉所有配對 模板 4 前綴和計數表 所有值都是正的,且問「最長/最短」→ sliding_window.md
「p 的所有 anagram 在 s 中的起點」、「s2 裡有沒有 s1 的排列」 視窗長度固定為 len(p) 模板 5 固定視窗的字元計數 視窗長度會變(「最多 k 種字元的最長子字串」)→ sliding_window.md
「在這套外星語言中是否已排序」、「依照 … 給定的順序」 字母表 ≤ 26 → 每次查排名 O(1) 模板 6 排名表
「isomorphic」、「符合同一個 pattern」、「不能有兩個字元對到同一個」 長度相同,O(n) 模板 7 雙射
「O(1) 的 get 和 put」、「淘汰最久沒用到的鍵」 最多 2·10⁵ 次呼叫 → 每個操作都要 O(1) linked_list § Template 7 map + 雙向鏈結串列 淘汰的是使用次數最少的 → LC 460,每個頻率一條串列
「回傳深拷貝」、「clone 這張圖/這條串列」 V, E ≤ 10⁴ → O(V + E) 模板 8 舊節點對新節點的表 只問連不連得到 → 一個 visited 集合(graph.md)
「從 [0, n) 均勻隨機挑,但絕不能挑到黑名單裡的值」 n ≤ 10⁹、黑名單 ≤ 10⁵ → 不可能把合法值列出來 模板 9 虛擬重新映射
「出現次數前 k 名,同次數依字母序」 k ≪ n → 堆積勝過整個排序 模板 10 map 加堆積 沒有同分規則(LC 347)→ O(n) 的桶排序

限制條件線索依照 complexity_cheatsheet.md 裡的尺規。

模板與演算法

模板對照表

# 模板 map 的形狀 怎麼認出來 代表 LC
1 查補數 {value: index} 「找一組配對」、「湊出 target」、補數 1, 15, 532, 1010
2 頻率計數 {item: count},或 {item: (first, last, count)} 「count」、「frequency」、「anagram」、「top-K」、「所有出現位置都在同一個連續區塊」 242, 49, 347, 451, 4038
3 用算出來的鍵分組 {canonical_key: [items]},或 {(min, max): count} 「group」、「同一條線」、「同一個特徵」、「(x, y) 等同 (y, x)」 49, 149, 609, 987, 4066
4 前綴和計數表 {prefixSum: count} / {prefixSum: firstIndex} / {startValue: minPrefix} 「子陣列和等於 K/可被 K 整除」、「兩端滿足 … 的最大和」 560, 974, 525, 325, 3026
5 固定視窗的字元計數 {char: count in window} 「p 在 s 裡的 anagram」、「permutation in string」 438, 567, 30
6 排名表 {value: rank} 「依照 … 給定的順序」 953, 791, 105
7 雙射(兩個 map) {x: y} 和 {y: x} 「一對一」、「isomorphic」、「符合這個 pattern」 205, 290
8 舊節點對新節點的表 {original node: clone}、{child: parent} 「clone」、「深拷貝」、「父指標」 133, 138, 652
9 虛擬映射(重新映射) {badIndex: goodIndex} 「隨機挑,但要排除 …」 710
10 map 加堆積 {item: count} + 依次數排序的堆積 「top-K」、「同分時依 …」 692, 347, 451

每個模板的實戰題都在 hash_map_examples.md,依模板分組。其他地方:有序 map(TreeMap / SortedDict)→ 下面的 Language APIs;O(n) top-K 的桶排序 → heap § Template 2;rolling hash → string_matching_kmp_rolling_hash.md;拆半探測的配對查找 → palindrome § Template 9;最大頻率算術(LC 621)→ heap_examples.md。

模板 1:查補數 —— LC 1

辨識 —— 「回傳兩個加起來等於 target 的數的索引」、「和/差/和取餘數為 … 的一組配對」,輸入未排序,而且索引要保留。n ≤ 10⁴–10⁵ → O(n²) 的雙層配對迴圈就是要打敗的暴力解。

關鍵 —— 在記錄索引 i 之前,seen 裝的剛好是所有索引 < i 的值,所以一次查詢 target - x in seen 就回答了「我左邊有沒有搭檔?」每一組配對都恰好被找到一次,由它右邊那個元素找到。

python
# python
# LC 1 - Two Sum
# IDEA: one pass; look up the complement among the values already seen, THEN record this value
# time = O(n), space = O(n)
def twoSum(nums, target):
    seen = {}                          # {value: index}, only indices < i
    for i, x in enumerate(nums):
        need = target - x
        if need in seen:               # the partner sits strictly to the left
            return [seen[need], i]
        seen[x] = i                    # record after the lookup: x never pairs with itself
    return []
java
// java
// LC 1 - Two Sum
// IDEA: one pass; look up the complement among the values already seen, then record this value
// time = O(n), space = O(n)
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> seen = new HashMap<>();   // {value: index}, only indices < i
    for (int i = 0; i < nums.length; i++) {
        int need = target - nums[i];
        if (seen.containsKey(need)) {
            return new int[]{seen.get(need), i};
        }
        seen.put(nums[i], i);
    }
    return new int[0];
}

複雜度

  • 時間 O(n):for 迴圈每個索引只走一次,need in seen/seen[x] = i 平均都是 O(1) 的雜湊操作。
  • 空間 O(n):當答案是最後兩個元素時,seen 會裝下每一個值。

試跑

text
nums = [3,2,4], target = 6
i=0  x=3  need=3  not in {}            seen={3:0}
i=1  x=2  need=4  not in seen          seen={3:0, 2:1}
i=2  x=4  need=2  seen[2]=1            -> [1, 2]

陷阱

  • 先記錄 x 再查詢,元素就會跟自己配對:[3,2,4]、target 6 會回傳 [0, 0]。
  • 重複值:[3,3]、target 6 —— 覆寫 seen[3] 之所以無害,只是因為查詢先跑;答案是 [0, 1]。
  • 回傳 [i, seen[need]] 會把較晚的索引放前面;較早的索引是 seen[need]。

不適用時 —— 輸入已經排序(LC 167)→ 雙指標,O(1) 空間(2_pointers.md);題目是在相異值上數配對(LC 532)→ 每個相異鍵只探一次,免得一組配對從兩端各數一次。

練習 —— LC 1 ⭐ · LC 1010 (鍵 = 除以 60 的餘數,改成累加次數而不是回傳) · LC 532 (相異值,只探 x + k;k = 0 需要次數) · LC 15 ⭐ (固定一個值,剩下的做 two-sum —— n_sum.md)

模板 2:頻率計數 —— LC 242

辨識 —— 「anagram」、「相同的字母、相同的次數」、「出現最多/最少次」、「第一個不重複的字元」、「ransomNote 能不能用 magazine 拼出來」。只有小寫英文字母 → 一個固定的 int[26] 就是 map,空間 O(1)。

關鍵 —— 兩個字串互為 anagram,若且唯若它們的計數表相等。在同一個 map 裡把 s 往上數、t 往下數,多重集合相同時每個項目剛好都是零,所以檢查只要一趟加上掃一次計數。

python
# python
# LC 242 - Valid Anagram
# IDEA: count s up and t down in one map; every count back at 0 <=> same multiset
# time = O(n), space = O(k)   k = distinct characters (26 for lowercase)
def isAnagram(s, t):
    if len(s) != len(t):
        return False
    count = {}
    for a, b in zip(s, t):
        count[a] = count.get(a, 0) + 1
        count[b] = count.get(b, 0) - 1
    for c in count.values():
        if c != 0:
            return False
    return True
java
// java
// LC 242 - Valid Anagram
// IDEA: lowercase only -> an int[26] is the count map; s counts up, t counts down
// time = O(n), space = O(1)   26 counters
public boolean isAnagram(String s, String t) {
    if (s.length() != t.length()) {
        return false;
    }
    int[] count = new int[26];
    for (int i = 0; i < s.length(); i++) {
        count[s.charAt(i) - 'a']++;
        count[t.charAt(i) - 'a']--;
    }
    for (int c : count) {
        if (c != 0) {
            return false;
        }
    }
    return true;
}

複雜度

  • 時間 O(n):zip 迴圈把 n 對字元各碰一次;最後只掃最多 k 個相異鍵。
  • 空間 O(k):count 每個相異字元一個項目,對固定的 26 個字母來說就是 O(1)。

陷阱

  • 跳過長度檢查:"a" 對 "ab" —— zip 在較短的字串就停了,每個計數都是 0,於是回傳 True。
  • Java 的 count[c - 'a'] 遇到大寫或 Unicode 輸入("Ä")就會丟例外。追問「如果是 Unicode 呢?」要的是 HashMap<Character, Integer>。
  • Counter 相減會丟掉非正的次數:Counter("a") - Counter("ab") 是空的,所以「差是空的」會說 "a" 和 "ab" 相符。改用 Counter(s) == Counter(t)。

不適用時 —— 只在乎存不存在、不在乎次數(LC 217)→ 用集合(set.md);在乎的是值出現在哪裡而不是幾次 → 見下面的索引跨度變形。

練習 —— LC 242 ⭐ · LC 383 (數 magazine,花在 note 上) · LC 387 (依字串順序再掃一次,第一個次數為 1 的) · LC 451 (依次數排序鍵) · LC 347 ⭐ (依次數取前 k —— 堆積或桶排序)

變形:索引跨度表 —— span == count 證明連續 優先度 3/5 — 值得會 — 多半是必備模式的變形

模式:有時候題目問的不是某個值出現幾次,而是它出現在哪裡。改成記錄位置而不是次數,一條算術恆等式就能回答「這個值的所有出現位置是不是同一個連續區塊?」:

text
last_idx - first_idx + 1 == number_of_occurrences

關鍵想法:span 是「如果中間沒夾雜別的東西,這個值的區塊會佔幾格」。只要有任何別的值夾在兩次出現之間,span 就會變大、count 卻不變,所以 span > count ⇔ 區塊斷掉了。不用排序、不用走訪索引清單 —— 每個不同的值 O(1)。

python
# python
# LC 4038 - Count Integers Appearing in a Single Block
# IDEA: {val: [idx, ...]}, then a value is special iff its indices are consecutive
# time = O(n), space = O(n)
from collections import defaultdict

def countSpecialIntegers(nums: list) -> int:
    my_map = defaultdict(list)
    for i, v in enumerate(nums):
        my_map[v].append(i)

    cnt = 0
    for k in my_map:
        idxs = my_map[k]
        # span == count  <=>  indices are consecutive
        if idxs[-1] - idxs[0] + 1 == len(idxs):
            cnt += 1
    return cnt

# [1,1,2,2,3] -> 1:[0,1] 2:[2,3] 3:[4]     -> every span == count -> 3
# [1,2,1]     -> 1:[0,2] -> 2-0+1 = 3 != 2 -> broken -> only 2 is special -> 1

一次掃描的改良版 —— 檢查只會讀三個數字,所以根本不需要索引清單。時間一樣 O(n),但只用 O(distinct) 格而不是 O(n):

python
# python
# LC 4038 - keep only (first_idx, last_idx, count) per value
# IDEA: the span check reads only the first index, the last index and the count
# time = O(n), space = O(k), k = number of distinct values
def countSpecialIntegers(nums: list) -> int:
    info = {}                                  # {val: [first, last, cnt]}
    for i, v in enumerate(nums):
        if v not in info:
            info[v] = [i, i, 1]
        else:
            info[v][1] = i
            info[v][2] += 1
    return sum(1 for first, last, cnt in info.values() if last - first + 1 == cnt)

四個陷阱:

  1. 特判只出現一次的情況 —— 不需要任何分支:單一索引給出 span = last - first + 1 = 1 且 count = 1,跟其他區塊一樣通過同一條恆等式。
  2. 走訪(或排序)索引清單來檢查是否連續 —— 索引本來就是遞增進來的,span == count 只讀兩端。
  3. 數的是出現次數而不是值 —— 答案是通過檢查的不同鍵的數量,所以一個在同一區塊裡出現 5 次的值只算 1。
  4. 把連續的索引跟連續的值搞混 —— 這條恆等式講的是陣列中的位置。連續值的序列(LC 128)是另一份 cheatsheet 的招式(一個 set,再加上「v-1 在不在?」)。

變形(同一招「存位置,而不只是次數」):

題目 LC# 變化點 —— map 存的是什麼
Partition Labels 763 {char: last index};由左往右掃,把區塊的結尾延伸到目前看過最遠的 last —— 恰好在 i == end 的地方切一刀
Contains Duplicate II 219 {value: last index};命中的條件是 i - last <= k,所以只有最近一次的位置有意義
Count Pairs of Equal Substrings With Minimum Difference 1794 兩個字串各一張 map:一邊存 {char: first index},另一邊存 {char: last index};把 first - last 壓到最小,再數有幾個字元達到這個最小值 —— 完整解法在 string_examples.md
Longest Consecutive Sequence 128 對照組:跨度是在值上而不是索引上 —— 一個 set,加上「只從 v-1 不存在的地方開始」

模板 3:用算出來的鍵分組 —— LC 49

辨識 —— 「把在 … 意義下相同的字串/點/元素分成一組」(重排、平移、旋轉、斜率),「以任意順序回傳各組」。n ≤ 10⁴ 個長度 k ≤ 100 的字 → 用排序當鍵是 O(n·k log k),用計數當鍵是 O(n·k)。

關鍵 —— 拿元素的正規形式去雜湊,而不是元素本身:鍵函數剛好在同一個等價類上相等(anagram 就是排序後的字母),所以一趟 groups[key(x)].append(x) 就把每一組都分好,兩個元素落在同一個 bucket,若且唯若它們屬於同一組。

python
# python
# LC 49 - Group Anagrams
# IDEA: canonical key = the word's letters sorted; anagrams share it, nothing else does
# time = O(n * k log k), space = O(n * k)   n words of length <= k
def groupAnagrams(strs):
    groups = {}                          # {sorted letters: [words]}
    for word in strs:
        key = "".join(sorted(word))
        if key not in groups:
            groups[key] = []
        groups[key].append(word)
    return list(groups.values())
java
// java
// LC 49 - Group Anagrams
// IDEA: canonical key = the word's letters sorted; anagrams share it, nothing else does
// time = O(n * k log k), space = O(n * k)
public List<List<String>> groupAnagrams(String[] strs) {
    Map<String, List<String>> groups = new HashMap<>();
    for (String word : strs) {
        char[] letters = word.toCharArray();
        Arrays.sort(letters);
        String key = new String(letters);     // a String, not the char[]: arrays hash by identity
        if (!groups.containsKey(key)) {
            groups.put(key, new ArrayList<>());
        }
        groups.get(key).add(word);
    }
    return new ArrayList<>(groups.values());
}

複雜度

  • 時間 O(n·k log k):每個字跑一次 sorted(word),這是主要成本;改用 26 格計數 tuple 當鍵就變成 O(n·k)。
  • 空間 O(n·k):每個字在它的組裡存一次,每組再加一把鍵。

陷阱

  • 拿 list 當 dict 的鍵:groups[sorted(word)] 會丟 TypeError: unhashable type: 'list'。把它接成字串,或轉成 tuple。
  • 在 Java 拿 char[] 當鍵:陣列是依身分雜湊的,所以 "eat" 和 "tea" 會落到不同的 bucket。要建一個 String。
  • 計數鍵接起來時沒有分隔符:計數 [1, 11] 和 [11, 1] 印出來都是 "111"。用 # 接起來(分隔符陷阱)。

不適用時 —— 這個關係不是等價關係,例如「只差一個字母」(LC 127):它不具遞移性,所以沒有正規鍵 → 圖/BFS。

練習 —— LC 49 ⭐ · LC 249 (鍵 = 字母間差距取 26 的餘數) · LC 609 (鍵 = 檔案內容) · LC 149 (鍵 = 約分後的斜率,見下方) · LC 4066 (鍵 = 無序配對,見下方)

一般化:鍵就是一個正規化的不變量 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

模式:map 的鍵不是原始的值 —— 它是值與值之間某種關係的正規形式。兩個元素會在 map 裡撞在一起,剛好就是它們共享你在意的那個性質。

關鍵想法(幾何):「通過錨點 P 的同一條線」⇔「同一個方向向量 (dx, dy)」。原始的 (dx, dy) 不能當鍵((1,2) 和 (2,4) 是同一條線),而 dy/dx 用浮點數會掉精度、遇到垂直線還會除以零。正規化:先除以 gcd,再強制統一符號。

python
# python
# LC 149 - Max Points on a Line
# IDEA: anchor at each point, group the others by a gcd-normalized slope key
# time = O(n^2), space = O(n)
from collections import defaultdict
from math import gcd

def maxPoints(points: list) -> int:
    n = len(points)
    if n <= 2:
        return n
    best = 1
    for i in range(n):
        slope_count = defaultdict(int)
        x1, y1 = points[i]
        for j in range(i + 1, n):
            dx, dy = points[j][0] - x1, points[j][1] - y1
            g = gcd(abs(dx), abs(dy))
            if g:
                dx, dy = dx // g, dy // g
            if dx < 0 or (dx == 0 and dy < 0):     # canonical sign
                dx, dy = -dx, -dy
            slope_count[(dx, dy)] += 1
            best = max(best, slope_count[(dx, dy)] + 1)   # +1 = anchor point
    return best
java
// java
// LC 149 - Max Points on a Line
// IDEA: anchor at each point, group the others by a gcd-normalized slope key
// time = O(n^2), space = O(n)
public int maxPoints(int[][] points) {
    int n = points.length;
    if (n <= 2) return n;
    int best = 1;
    for (int i = 0; i < n; i++) {
        Map<String, Integer> slopeCount = new HashMap<>();
        for (int j = i + 1; j < n; j++) {
            int dx = points[j][0] - points[i][0];
            int dy = points[j][1] - points[i][1];
            int g = gcd(Math.abs(dx), Math.abs(dy));
            if (g != 0) { dx /= g; dy /= g; }
            // canonical direction: force dx > 0, or dx == 0 && dy > 0
            if (dx < 0 || (dx == 0 && dy < 0)) { dx = -dx; dy = -dy; }
            String key = dx + "/" + dy;
            int cnt = slopeCount.merge(key, 1, Integer::sum);
            best = Math.max(best, cnt + 1);   // +1 for the anchor point itself
        }
    }
    return best;
}

private int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }

三個陷阱(都是面試常見的追問):

  1. 浮點斜率 dy/dx —— 精度會掉,垂直線還會 ZeroDivisionError。改用約分後的整數配對。
  2. 忘了正規化符號 —— (1,2) 和 (-1,-2) 是同一條線,卻變成兩個不同的鍵。要強制成同一個方向。
  3. 忘了 +1 —— map 數的是錨點的夥伴,錨點自己不在 map 裡。

變形(都是同一招「自己發明一個正規化的鍵」):

題目 LC# 變化點 —— 這個鍵編碼了什麼
Minimum Area Rectangle 939 鍵 = 放進 set 的點本身;走訪對角線配對 (x1,y1),(x2,y2)(x1!=x2 && y1!=y2),再檢查另外兩個角在不在
Most Stones Removed with Same Row or Column 947 鍵 = row 與 ~col(位元取反讓列與行落在互不重疊的 id 空間)→ 在 map 上跑併查集
Vertical Order Traversal of a Binary Tree 987 鍵 = 行偏移量 col(root = 0、左 = col-1、右 = col+1);值 = 待排序的 (row, val) 清單

模式:無序配對鍵 —— 用 (min, max) 不分順序地計數 {x, y} 優先度 3/5 — 值得會 — 多半是必備模式的變形

模式:當你要數的關係是對稱的 —— (x, y) 和 (y, x) 意思一樣 —— 就用排好順序的配對 (min(x, y), max(x, y)) 當 map 的鍵。兩個方向會落進同一個桶,桶裡的計數就是你真正要的數字。這是最小的一種正規化不變量:無序配對的正規形式。

python
pair = (min(x, y), max(x, y))      # (2, 5) and (5, 2) -> the same key (2, 5)
pair_count[pair] += 1
best = max(best, pair_count[pair]) # running max: counts only go up, so no second pass

關鍵想法(LC 4066):最多一次,把所有 x 換成 y,讓相等的相鄰對數最大。先問:一次替換 x -> y 對每一組相鄰對做了什麼?

text
(x, y) or (y, x)   -> (y, y)   unequal -> equal       +1
(x, x)             -> (y, y)   equal   -> equal        0   (never lost)
(x, a), a != y     -> (y, a)   unequal -> unequal      0
anything without x -> untouched                        0

所以原本相等的對永遠不會失去,而增加的量剛好等於「一邊是 x、一邊是 y」的相鄰位置數 —— 不分左右。這就是一個無序配對的計數:

text
answer = (adjacent pairs already equal) + max over x != y of (adjacent {x, y} pairs)
python
# python
# LC 4066 - Maximum Equal Adjacent Pairs After at Most One Replacement
# IDEA: already-equal pairs survive any replacement; x -> y gains one per adjacent {x, y},
#       so count adjacent unequal pairs under an unordered (min, max) key and take the max
# time = O(n), space = O(n)
from collections import defaultdict

def maxEqualAdjacentPairs(nums: list) -> int:
    already_equal = 0
    unequal_pair_cnt = defaultdict(int)   # {(smaller, larger): adjacent occurrences}
    max_newly_equal = 0                   # 0 = do no operation (e.g. [1, 1, 1])

    for i in range(len(nums) - 1):
        x, y = nums[i], nums[i + 1]
        if x == y:
            already_equal += 1
        else:
            # (2, 5) and (5, 2) are fixed by the same replacement -> one key
            pair = (min(x, y), max(x, y))
            unequal_pair_cnt[pair] += 1
            max_newly_equal = max(max_newly_equal, unequal_pair_cnt[pair])

    return already_equal + max_newly_equal
java
// java
// LC 4066 - Maximum Equal Adjacent Pairs After at Most One Replacement
// IDEA: same unordered (min, max) key, packed into one long (values <= 10^9 fit in 32 bits)
// time = O(n), space = O(n)
public int maxEqualAdjacentPairs(int[] nums) {
    int alreadyEqual = 0;
    int maxNewlyEqual = 0;
    Map<Long, Integer> unequalPairCnt = new HashMap<>();

    for (int i = 0; i + 1 < nums.length; i++) {
        int x = nums[i];
        int y = nums[i + 1];
        if (x == y) {
            alreadyEqual++;
        } else {
            long small = Math.min(x, y);
            long large = Math.max(x, y);
            long pairKey = (small << 32) | large;   // canonical order, one key per {x, y}
            int cnt = unequalPairCnt.merge(pairKey, 1, Integer::sum);
            maxNewlyEqual = Math.max(maxNewlyEqual, cnt);
        }
    }
    return alreadyEqual + maxNewlyEqual;
}
text
nums = [1,2,1,2,1]
  i  pair    key     cnt   max_newly_equal
  0  (1,2)   (1,2)   1     1
  1  (2,1)   (1,2)   2     2      <- an ordered key would have started (2,1) at 1
  2  (1,2)   (1,2)   3     3
  3  (2,1)   (1,2)   4     4
  already_equal = 0  ->  answer = 0 + 4 = 4   (replace 1 -> 2: [2,2,2,2,2])

nums = [1,2,3,2]  -> {1,2}: 1, {2,3}: 2  -> 0 + 2 = 2
nums = [1,1,1]    -> already_equal = 2, map empty -> 2 + 0 = 2   (no operation)

四個陷阱:

  1. 用有序的鍵 —— 直接用 (x, y) 會把 [1,2,1,2,1] 拆成 (1,2): 2 和 (2,1): 2,答案變 2 而不是 4。替換不在乎 x 在哪一邊,鍵也不能在乎。
  2. 擔心替換會破壞原本相等的對 —— (x, x) 變成 (y, y),依然相等。沒有任何東西要扣掉,所以答案就是單純相加。
  3. 最佳增量從 1 開始,或強制一定要操作 —— 「最多一次」代表可以不做;map 是空的時候貢獻必須是 0。
  4. Java 用字串當鍵 —— 沒有分隔符時 "1" + "23" 和 "12" + "3" 會撞在一起(見分隔符陷阱)。值放得進 32 bits 時就打包成一個 long,不然用 x + "," + y。

為什麼邊走邊取 max 是安全的:每次遞增後做 max(best, cnt[pair]),會等於最終各計數的最大值,前提是計數永遠不會減少。在滑動視窗裡計數會下降,就要另外再掃一次,或用支援刪除的結構。

變形(同一種無序配對鍵):

題目 LC# 變化點 —— (min, max) 鍵數的是什麼
Number of Equivalent Domino Pairs 1128 [a, b] ≡ [b, a];每個鍵數骨牌張數,再加 k*(k-1)/2 —— 或在每次遞增前先加上目前的計數
Maximal Network Rank 1615 把無向邊存成 (min, max) 鍵的 set,「u 和 v 有沒有相連?」就是一次 O(1) 查找,不管順序

模板 4:前綴和計數表 —— LC 560 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

辨識 —— 「和等於 k/可被 k 整除/0 和 1 一樣多的(連續)子陣列有幾個」,而且值可能是負的,所以滑動視窗沒辦法安全地收縮。n ≤ 2·10⁴–10⁵ → O(n),不是枚舉所有 (i, j) 的 O(n²)。

關鍵 —— sum(nums[i..j]) = pre[j+1] - pre[i],所以以 j 結尾、和為 k 的子陣列,剛好對應先前等於 pre[j+1] - k 的那些前綴。map 數的是每個先前的前綴出現過幾次,並以 {0: 1} 代表空前綴作為種子,所以一次查詢就把所有合法起點一起加進來。

python
# python
# LC 560 - Subarray Sum Equals K
# IDEA: count the earlier prefix sums equal to (prefix - k); look up BEFORE recording the current prefix
# time = O(n), space = O(n)
def subarraySum(nums, k):
    count_of_prefix = {0: 1}             # the empty prefix: subarrays that start at index 0
    prefix = 0
    total = 0
    for x in nums:
        prefix += x
        total += count_of_prefix.get(prefix - k, 0)   # every earlier start that closes here
        count_of_prefix[prefix] = count_of_prefix.get(prefix, 0) + 1
    return total
java
// java
// LC 560 - Subarray Sum Equals K
// IDEA: count the earlier prefix sums equal to (prefix - k); look up before recording the current prefix
// time = O(n), space = O(n)
public int subarraySum(int[] nums, int k) {
    Map<Integer, Integer> countOfPrefix = new HashMap<>();
    countOfPrefix.put(0, 1);             // the empty prefix
    int prefix = 0;
    int total = 0;
    for (int x : nums) {
        prefix += x;
        total += countOfPrefix.getOrDefault(prefix - k, 0);
        countOfPrefix.put(prefix, countOfPrefix.getOrDefault(prefix, 0) + 1);
    }
    return total;
}

複雜度

  • 時間 O(n):對 nums 掃一趟;每一步是一次 get 加一次 map 更新。
  • 空間 O(n):count_of_prefix 最多裝 n + 1 個相異的前綴和。

試跑

text
nums = [1,1,1], k = 2
x  prefix  look up prefix-k   total  map after
1  1       -1 -> 0            0      {0:1, 1:1}
1  2        0 -> 1            1      {0:1, 1:1, 2:1}
1  3        1 -> 1            2      {0:1, 1:1, 2:1, 3:1}
-> 2

陷阱

  • 沒有 {0: 1} 這個種子,就會漏掉每一個從索引 0 開始的子陣列:[1,1,1]、k = 2 會回傳 1 而不是 2。
  • 先記錄前綴再查詢,k = 0 時會把空子陣列也數進去:[1]、k = 0 會回傳 1 而不是 0。
  • 用 {prefix: index} 取代次數,會丟掉重複出現的前綴:[1,-1,1,-1]、k = 0 有 4 個子陣列,但每個前綴只記一個索引,每個前綴最多只找得到一個起點。
  • 取餘數當鍵(LC 974)又有負值:Java 的 -7 % 5 == -2;要用 ((p % k) + k) % k 正規化。Python 的 % 本來就不會是負的。

不適用時 —— 每個值都是正的,且問的是和 ≤ 或 ≥ k 的最長/最短視窗 → 滑動視窗,O(1) 空間(sliding_window.md);和為 k 的最長子陣列 → 每個前綴只記第一次出現的索引,以 {0: -1} 當種子(LC 325、LC 525)。

練習 —— LC 560 · LC 930 (值只有 0/1,程式碼一樣) · LC 974 (鍵 = 前綴取 k 的餘數) · LC 525 (0 → −1,每個前綴記第一次的索引) · LC 325 (最長:記第一次的索引) · LC 3026 (見下方:以起點的值為鍵,存最小前綴)

依題目問的東西,map 該存什麼:

題型 map 的值 例子 特別注意
數出所有子陣列 count LC 560, 930, 974 先查再更新
計數(帶取餘) count LC 974 用餘數當鍵;處理負數!
找最長子陣列 index(第一次出現) LC 325, 525 只存第一次出現
找最長(需先轉換) index(第一次出現) LC 525 把 0→-1、1→+1;初始化 {0:-1}
判斷是否存在 boolean/index LC 523 任一次出現都可以
找最大和,條件落在兩端 min prefix,以起點的值為鍵 LC 3026 鍵是 nums[i],不是前綴和 —— 見下面的變形

變形:以起點的值為鍵,存最小前綴 — LC 3026 優先度 3/5 — 值得會 — 多半是必備模式的變形

關鍵 —— 這就是 Kadane 的「減掉目前最小的前綴」(LC 53),只是按起點的值拆成一個值一個桶。計數表回答「有幾個」;最小前綴表回答「最多多少」。

歸約到:模板 4 —— 一樣是對前綴和掃一趟,只是鍵換成起點的值,值換成最小前綴。

模式:條件落在子陣列的兩端,而不是它的和上,問的又是最大和。所以鍵換了,值也換了:

  • 鍵 = 起點的值 nums[i],因為「good」(|nums[i] - nums[j]| == k)限定的是起點的值,不是它的前綴和;
  • 值 = 這個值目前見過的最小 pre[i],因為以 j 結尾的和是 pre[j+1] - pre[i],pre[i] 越小,和就越大。
text
sum(nums[i..j]) = pre[j+1] - pre[i]
fixed j  ->  maximise  = minimise pre[i]   over every i with nums[i] in {nums[j]-k, nums[j]+k}
best[v]  = min pre[i] over every i seen so far with nums[i] == v
python
# python
# LC 3026 - Maximum Good Subarray Sum
# IDEA: best[v] = min prefix sum just before an index holding v; close at x from v = x-k or x+k
# time = O(n), space = O(n)
def maximumSubarraySum(nums: list, k: int) -> int:
    best = {}                       # {start value: min prefix sum before it}
    res = float('-inf')
    pre = 0                         # pre = sum(nums[:j]), i.e. before x
    for x in nums:
        for v in (x - k, x + k):    # the two start values that make [i..j] good
            if v in best:
                res = max(res, pre + x - best[v])
        # record x as a future START with the prefix BEFORE it
        if x not in best or pre < best[x]:
            best[x] = pre
        pre += x
    return 0 if res == float('-inf') else res
java
// java
// LC 3026 - Maximum Good Subarray Sum
// IDEA: same map; long, because n * |nums[i]| reaches 1e14
// time = O(n), space = O(n)
public long maximumSubarraySum(int[] nums, int k) {
    Map<Integer, Long> best = new HashMap<>();   // {start value: min prefix before it}
    long res = Long.MIN_VALUE, pre = 0;
    for (int x : nums) {
        for (int v : new int[]{x - k, x + k}) {      // |x ± k| <= 2e9 still fits an int
            Long p = best.get(v);
            if (p != null) res = Math.max(res, pre + x - p);
        }
        best.merge(x, pre, Math::min);
        pre += x;
    }
    return res == Long.MIN_VALUE ? 0 : res;
}

走查 —— nums = [-1,3,2,4,5], k = 3:

text
x    pre(before x)  look up x-k, x+k          candidate          best after
-1    0             -4, 2   -> none            -                  {-1:0}
 3   -1              0, 6   -> none            -                  {-1:0, 3:-1}
 2    2             -1, 5   -> -1: 2+2-0 = 4   4  ([-1,3,2])      {..., 2:2}
 4    4              1, 7   -> none            -                  {..., 4:4}
 5    8              2, 8   -> 2: 8+5-2 = 11   11 ([2,4,5])       {..., 5:8}
-> 11

四個陷阱:

  1. res 從 0 開始 —— good 子陣列的和可以是負的([-1,-2,-3,-4], k = 2 → -6)。用 -inf,只有在完全沒有 good 子陣列時才回傳 0。
  2. 存了包含起點的前綴 —— 子陣列包含 nums[i],所以要減的是 pre[i],也就是它之前的和。best[x] 要在 pre += x 之前記下。
  3. 只存某個值最新或最早的前綴 —— 兩者都不一定是最好的起點。要存最小值,就像 LC 325 存第一次出現的索引那樣。
  4. Java 用 int —— 1e5 個 1e9 會溢位;前綴和答案都要用 long(鍵 x ± k 落在 ±2e9 內,int 放得下)。

變形(同一招「每個鍵存最好的起點」):

題目 LC# map 存什麼,為什麼
Maximum Subarray 53 每個起點都合格,所以整張表縮成一個全域最小前綴 —— 就是 Kadane 演算法
Maximum Good Subarray Sum 3026 {起點的值: 最小前綴};查 x - k 和 x + k
Maximum Size Subarray Sum Equals k 325 {前綴和: 第一次出現的索引} —— 這裡「最好的起點」是最早的,因為要的是最長

模板 5:固定視窗的字元計數 —— LC 438

辨識 —— 「找出 p 的所有 anagram 在 s 中的起點」、「s2 裡有沒有 s1 的排列」:一個長度固定為 len(p) 的視窗,用字母計數來比較。n ≤ 3·10⁴ 個小寫字母 → O(26·n)。

關鍵 —— 第 right 步之後,視窗剛好是 s[right-m+1..right]:每一步在右邊加一個字元、移掉從左邊掉出去的那個,所以 window 永遠是目前這個長度 m 的視窗的計數表,而 window == need 就是 anagram 的判斷。

python
# python
# LC 438 - Find All Anagrams in a String
# IDEA: fixed window of len(p); add the right char, drop the one that fell off the left, compare count maps
# time = O(26 * n), space = O(26)
def findAnagrams(s, p):
    m = len(p)
    if m > len(s):
        return []
    need = {}
    for ch in p:
        need[ch] = need.get(ch, 0) + 1
    window = {}
    starts = []
    for right in range(len(s)):
        ch = s[right]
        window[ch] = window.get(ch, 0) + 1
        if right >= m:                       # one too long: drop s[right - m]
            out = s[right - m]
            window[out] -= 1
            if window[out] == 0:
                del window[out]              # a zero entry would make window != need
        if right >= m - 1 and window == need:
            starts.append(right - m + 1)
    return starts
java
// java
// LC 438 - Find All Anagrams in a String
// IDEA: two int[26] count arrays; slide by one, compare with Arrays.equals
// time = O(26 * n), space = O(1)
public List<Integer> findAnagrams(String s, String p) {
    List<Integer> starts = new ArrayList<>();
    int m = p.length();
    if (m > s.length()) {
        return starts;
    }
    int[] need = new int[26];
    int[] window = new int[26];
    for (int i = 0; i < m; i++) {
        need[p.charAt(i) - 'a']++;
    }
    for (int right = 0; right < s.length(); right++) {
        window[s.charAt(right) - 'a']++;
        if (right >= m) {
            window[s.charAt(right - m) - 'a']--;    // drop the char that left the window
        }
        if (right >= m - 1 && Arrays.equals(window, need)) {
            starts.add(right - m + 1);
        }
    }
    return starts;
}

複雜度

  • 時間 O(26·n):對 s 掃一趟;每次 window == need 最多比 26 個鍵。
  • 空間 O(26):兩個 map 都只裝小寫字母。

陷阱

  • 在 dict 裡留下次數為零的項目:{'a': 0, 'b': 1} != {'b': 1},所以 s = "ab", p = "b" 會漏掉索引 1。次數降到 0 就把鍵刪掉(Java 的 int[26] 沒有這個問題)。
  • 從 right >= m 才開始比、而不是 right >= m - 1,會跳過第一個完整的視窗:s = "ab", p = "ab" 回傳 []。
  • 固定視窗不需要「matched」計數器:比較兩個 map 就是全部的檢查。LC 567 在第一次相等時就回傳 True。

不適用時 —— 視窗長度會變(「最多 k 種字元的最長子字串」,LC 3、LC 76)→ sliding_window.md 的先擴張再收縮;計數表一樣,視窗規則不一樣。

練習 —— LC 438 · LC 567 ⭐ (第一次相符就回傳) · LC 30 (「字元」換成整個單字) · LC 76 ⭐ (可變視窗加上 matched 計數器,在 sliding_window)

模板 6:排名表 —— 值對應到位置 —— LC 953 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

辨識 —— 「在這套外星語言中是否依字典序排好」、「依照 order 給定的順序排序 s」、「從前序和中序建出這棵樹」:題目自己定義了一套順序。順序字串 ≤ 26 個字母 → 每次查排名都是 O(1)。

關鍵 —— 先一次算好 rank[c] = c 在給定順序裡的位置;之後比較排名就等於用外星順序比較字母,每次比較都是 O(1) 的整數比較,而不是 O(m) 的 order.index(c) 掃描。

python
# python
# LC 953 - Verifying an Alien Dictionary
# IDEA: char -> rank map, then compare adjacent words rank by rank
# time = O(total chars), space = O(1)   26 keys
def isAlienSorted(words, order):
    rank = {}
    for i, c in enumerate(order):
        rank[c] = i

    def in_order(a, b):
        for ca, cb in zip(a, b):
            if rank[ca] != rank[cb]:
                return rank[ca] < rank[cb]   # the first differing letter decides
        return len(a) <= len(b)              # common prefix: the shorter word must come first

    for i in range(len(words) - 1):
        if not in_order(words[i], words[i + 1]):
            return False
    return True

# python
# LC 791 - Custom Sort String
# IDEA: rank map as a sort key; unranked chars get rank len(order) (stable sort keeps them last)
# time = O(n log n + m), space = O(n)
def customSortString(order: str, s: str) -> str:
    rank = {c: i for i, c in enumerate(order)}
    return "".join(sorted(s, key=lambda c: rank.get(c, len(order))))
java
// java
// LC 953 - Verifying an Alien Dictionary
// IDEA: char -> rank map turns an arbitrary alphabet into comparable ints
// time = O(total chars), space = O(1)  (26 keys)
public boolean isAlienSorted(String[] words, String order) {
    int[] rank = new int[26];                       // char -> position in `order`
    for (int i = 0; i < order.length(); i++) rank[order.charAt(i) - 'a'] = i;
    for (int i = 0; i + 1 < words.length; i++) {
        if (!inOrder(words[i], words[i + 1], rank)) return false;
    }
    return true;
}

private boolean inOrder(String a, String b, int[] rank) {
    int n = Math.min(a.length(), b.length());
    for (int i = 0; i < n; i++) {
        int ra = rank[a.charAt(i) - 'a'], rb = rank[b.charAt(i) - 'a'];
        if (ra != rb) return ra < rb;
    }
    return a.length() <= b.length();                // prefix must come first: "app" < "apple"
}

// LC 791 - Custom Sort String  (counting sort, no comparator needed)
// time = O(n + m), space = O(1)
public String customSortString(String order, String s) {
    int[] cnt = new int[26];
    for (char c : s.toCharArray()) cnt[c - 'a']++;
    StringBuilder sb = new StringBuilder();
    for (char c : order.toCharArray()) {            // ranked chars first, in rank order
        while (cnt[c - 'a'] > 0) { sb.append(c); cnt[c - 'a']--; }
    }
    for (char c = 'a'; c <= 'z'; c++) {             // unranked chars: any order
        while (cnt[c - 'a'] > 0) { sb.append(c); cnt[c - 'a']--; }
    }
    return sb.toString();
}

複雜度

  • 時間 O(total chars):建 rank 是 O(26);每對相鄰的字最多比到較短的那個長度,所以每個字元最多被讀兩次。
  • 空間 O(1):rank 最多 26 個項目。

陷阱

  • 前綴規則:["apple", "app"] 不是排序好的。共同前綴比完之後,較短的字必須排在前面;字母迴圈跑完就回傳 True 會漏掉這個情況。
  • 在比較裡呼叫 order.index(c) 雖然正確,但每個字元都要 O(26);排名要事先算好一次。
  • LC 791:不在 order 裡的字元,rank[c] 會丟 KeyError;給它一個排在最後面的排名,rank.get(c, len(order))。

練習 —— LC 953 · LC 791 (把排名當排序鍵,或用計數排序) · LC 1122 (relative sort array) · LC 105 ⭐ (排名 = 在中序裡的位置,見下方) · LC 106

變形 —— 用「值 → 索引」表切分陣列(LC 105 / LC 106):同一個 map,只是這裡的「排名」是在中序序列中的位置,把 O(n²) 的「掃中序找 root」變成 O(1),整個建樹變成 O(n)。

python
# python
# LC 105 - Construct Binary Tree from Preorder and Inorder Traversal
# IDEA: {value: index in inorder} → O(1) root split
# time = O(n), space = O(n)
def buildTree(preorder: list, inorder: list):
    idx = {v: i for i, v in enumerate(inorder)}   # values are unique (given)
    pre = [0]                                     # pointer into preorder

    def build(lo, hi):
        if lo > hi:
            return None
        node = TreeNode(preorder[pre[0]])
        pre[0] += 1
        mid = idx[node.val]                       # O(1) instead of inorder.index(...)
        node.left = build(lo, mid - 1)
        node.right = build(mid + 1, hi)
        return node

    return build(0, len(inorder) - 1)

模板 7:用兩個 map 做雙射 —— LC 205

辨識 —— 「isomorphic strings」、「符合同一個 pattern」、「不能有兩個字元對到同一個字元」。兩個序列等長,n ≤ 5·10⁴ → O(n)。

關鍵 —— 一個映射是雙射,若且唯若它在兩個方向上都一致:s2t 擋掉一個 a 對到兩個不同的 b,t2s 擋掉兩個不同的 a 對到同一個 b。記錄之前兩邊都先檢查,就能讓兩個 map 在每一步都維持是部分雙射。

python
# python
# LC 205 - Isomorphic Strings
# IDEA: two maps, s->t and t->s; any disagreement in either direction breaks the bijection
# time = O(n), space = O(k)   k = distinct characters
def isIsomorphic(s: str, t: str) -> bool:
    if len(s) != len(t):
        return False
    s2t, t2s = {}, {}
    for a, b in zip(s, t):
        if s2t.get(a, b) != b or t2s.get(b, a) != a:
            return False
        s2t[a] = b
        t2s[b] = a
    return True

# python
# LC 290 - Word Pattern
# IDEA: the same two maps, letter <-> word; check the lengths first
# time = O(n), space = O(n)
def wordPattern(pattern: str, s: str) -> bool:
    words = s.split()
    if len(pattern) != len(words):
        return False
    p2w, w2p = {}, {}
    for p, w in zip(pattern, words):
        if p2w.get(p, w) != w or w2p.get(w, p) != p:
            return False
        p2w[p] = w
        w2p[w] = p
    return True
java
// java
// LC 205 - Isomorphic Strings
// IDEA: two maps, s->t and t->s; any disagreement in either direction breaks the bijection
// time = O(n), space = O(k)
public boolean isIsomorphic(String s, String t) {
    if (s.length() != t.length()) {
        return false;
    }
    Map<Character, Character> s2t = new HashMap<>();
    Map<Character, Character> t2s = new HashMap<>();
    for (int i = 0; i < s.length(); i++) {
        char a = s.charAt(i);
        char b = t.charAt(i);
        if (s2t.containsKey(a) && s2t.get(a) != b) {
            return false;
        }
        if (t2s.containsKey(b) && t2s.get(b) != a) {
            return false;
        }
        s2t.put(a, b);
        t2s.put(b, a);
    }
    return true;
}

複雜度

  • 時間 O(n):對所有配對掃一趟;每次檢查與記錄都是 O(1)。
  • 空間 O(k):每個 map 對每個相異字元(LC 290 裡則是單字)各有一個項目。

陷阱

  • 只用一個 map:s = "badc", t = "baba" 通過每一個 s → t 的檢查(b→b, a→a, d→b, c→a)而回傳 True;是 t → s 那個 map 抓到 b 同時來自 b 和 d。
  • LC 290 沒做長度檢查:pattern = "aaa", s = "dog dog dog dog" —— zip 在三個單字後就停了,回傳 True。
  • Java 的 s2t.get(a) != b 是拿包裝過的 Character 跟 char 比,會先拆箱,所以是安全的;拿兩個包裝過的 Character 用 != 比就不安全了。

練習 —— LC 205 · LC 290 (字母 ↔ 單字) · LC 890 (對每個單字都跑一次這個檢查) · LC 291 (pattern ↔ 子字串:在同樣的兩個 map 上回溯)


模板 8:舊節點對新節點的表 —— LC 133

辨識 —— 「回傳這張圖/這條串列的深拷貝」、「clone」,節點型別帶有可能形成環、或可以指向任何地方的指標(random 指標)。V, E ≤ 10⁴ → O(V + E)。

關鍵 —— copies[node] 在第一次走到 node 時就建立,而且在走訪它的鄰居之前,所以繞回 node 的環會在 map 裡找到它的副本,而不是再遞迴一次。這個 map 同時是 visited 集合,也是舊 → 新的對照表。

python
# python
# LC 133 - Clone Graph
# IDEA: copies = {original node: its clone}; register the clone BEFORE visiting neighbours, so cycles hit the map
# time = O(V + E), space = O(V)
class Node:
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []

def cloneGraph(node):
    if node is None:
        return None
    copies = {}

    def clone(original):
        if original in copies:
            return copies[original]
        copy = Node(original.val)
        copies[original] = copy                  # register first: a cycle back here stops
        for neighbor in original.neighbors:
            copy.neighbors.append(clone(neighbor))
        return copy

    return clone(node)
java
// java
// LC 133 - Clone Graph
// IDEA: copies = {original node: its clone}; register the clone before visiting neighbours
// time = O(V + E), space = O(V)
private Map<Node, Node> copies = new HashMap<>();

public Node cloneGraph(Node node) {
    if (node == null) {
        return null;
    }
    if (copies.containsKey(node)) {
        return copies.get(node);
    }
    Node copy = new Node(node.val, new ArrayList<>());
    copies.put(node, copy);                       // before the neighbours: cycles stop here
    for (Node neighbor : node.neighbors) {
        copy.neighbors.add(cloneGraph(neighbor));
    }
    return copy;
}

複雜度

  • 時間 O(V + E):clone 的本體每個節點只跑一次(if original in copies 的 return 擋掉重複),每條鄰接串列也只走一次。
  • 空間 O(V):copies 每個節點一個項目,遞迴堆疊在一條路徑圖上可以深到 V。

陷阱

  • 在鄰居迴圈之後才登記副本:兩個節點的圖 1 — 2 會遞迴 1 → 2 → 1 → …,直到堆疊爆掉。
  • 用 val 當鍵在 LC 133 行得通(值都不同),但在值會重複的 LC 138 就壞了。要用節點本身當鍵。
  • cloneGraph(None) 必須回傳 None,而不是一個值為 0 的節點 —— 空圖本身就是 LC 133 的範例之一。

不適用時 —— 只問連不連得到或數量(LC 200)→ 一個 visited 集合,不需要 clone 表(graph.md)。

練習 —— LC 133 ⭐ · LC 138 (random 指標:同一個 map,兩趟或交錯著做) · LC 1490 (N 元樹,沒有環) · LC 652 (map 存的是子樹特徵 → 次數)


模板 9:虛擬映射(重新映射)—— LC 710

辨識 —— 「在 [0, n) 裡隨機挑一個不在 blacklist 裡的整數」、「盡量少呼叫亂數函式」。n ≤ 10⁹、黑名單 ≤ 10⁵ → 沒辦法把合法值列出來,而 pick() 必須是 O(1)。

關鍵 —— 合法的值剛好有 bound = n - B 個,所以只從 [0, bound) 裡抽。這個範圍內的每個黑名單位置,都映射到尾端 [bound, n) 裡一個相異的合法值 —— 尾端的合法值個數剛好等於前面壞位置的個數 —— 所以這個 map 是對合法集合的雙射,每個合法值被抽到的機率都一樣。

python
# python
# LC 710 - Random Pick with Blacklist
# IDEA: draw from [0, bound); remap each blacklisted slot below bound to a valid tail value
# time = O(B) to build, O(1) per pick, space = O(B)
import random

class Solution:
    def __init__(self, n: int, blacklist: list):
        self.bound = n - len(blacklist)
        black_set = set(blacklist)
        self.mapping = {}

        last = n - 1
        for b in blacklist:
            if b < self.bound:
                while last in black_set:         # skip tail values that are blacklisted too
                    last -= 1
                self.mapping[b] = last
                last -= 1

    def pick(self) -> int:
        idx = random.randrange(self.bound)
        return self.mapping.get(idx, idx)
java
// java
// LC 710 - Random Pick with Blacklist
// IDEA: draw from [0, bound); remap each blacklisted slot below bound to a valid tail value
// time = O(B) to build, O(1) per pick, space = O(B)
class Solution {
    private Map<Integer, Integer> mapping = new HashMap<>();
    private Random random = new Random();
    private int bound;

    public Solution(int n, int[] blacklist) {
        bound = n - blacklist.length;

        Set<Integer> blackSet = new HashSet<>();
        for (int b : blacklist) blackSet.add(b);

        int last = n - 1;
        for (int b : blacklist) {
            if (b < bound) {
                // Skip tail values that are also blacklisted
                while (blackSet.contains(last)) last--;
                mapping.put(b, last);
                last--;
            }
        }
    }

    public int pick() {
        int idx = random.nextInt(bound);
        return mapping.getOrDefault(idx, idx);  // remap if blacklisted, else return directly
    }
}

複雜度

  • 建構 O(B)、每次挑選 O(1):建構子的迴圈每個黑名單值走一次,last 只會在 B 個尾端位置上往下移;pick 是一次抽籤加一次 map 查詢。
  • 空間 O(B):black_set 和 mapping 各最多 B 個項目。

試跑

text
n=10, blacklist=[2,3,5,8]   →   bound = 10 - 4 = 6

RANDOM RANGE  [0, bound)
|----|----|----|----|----|----|
  0    1    2    3    4    5
             X    X         X
             ↑bad inside range — must remap

TAIL RANGE  [bound, n)
|----|----|----|----|
  6    7    8    9
             X            ← also blacklisted, skip it

Remapping (last starts at 9, walks left skipping blacklisted):
  b=2  →  last=9 (valid)  → map 2→9,  last=8
  b=3  →  last=8 (blacklisted, skip) → last=7 (valid) → map 3→7, last=6
  b=5  →  last=6 (valid)  → map 5→6,  last=5

Final mapping: { 2→9, 3→7, 5→6 }

pick() result for each index in [0,5]:
  0 → 0   (not mapped, return directly)
  1 → 1
  2 → 9   (remapped)
  3 → 7   (remapped)
  4 → 4
  5 → 6   (remapped)

Valid numbers returned: {0,1,4,6,7,9} ✓ uniformly distributed

陷阱

  • 從 [0, n) 抽、抽到黑名單就重抽:期望的抽籤次數是 n / (n − B),當黑名單逼近 n 時沒有上限。
  • 映射到 last 時沒跳過也在黑名單裡的尾端值:n = 3, blacklist = [0, 2] 會把 0 映射到 2,一個黑名單值。
  • 為 b ≥ bound 的黑名單值做映射:它永遠不會被抽到,在它身上花掉一個尾端值,可能讓 last 跑到 bound 以下。

練習 —— LC 710 · LC 398 (蓄水池抽樣) · LC 528 (前綴和 + 二分搜尋) · LC 384 (Fisher–Yates 洗牌)


模板 10:map 加堆積 —— LC 692

辨識 —— 「回傳出現次數最多的 k 個單字,依次數排序,同次數依字母序」、「依次數取前 k」:map 負責計數,第二個結構負責把 map 數出來的東西排序。k ≪ n → 堆積勝過把每個鍵都排序。

關鍵 —— map 一趟就把輸入變成 (item, count) 配對;依 (-count, word) 排序的堆積,pop 出來的順序就剛好是題目要的 —— 次數高的先出,同分時字母序較小的先出 —— 所以前 k 次 pop 就是答案。

python
# python
# LC 692 - Top K Frequent Words
# IDEA: count with a map, heapify (-count, word) so ties pop alphabetically, pop k times
# time = O(n + k log n), space = O(n)
import heapq

def topKFrequent(words, k):
    count = {}
    for word in words:
        count[word] = count.get(word, 0) + 1
    heap = []
    for word, freq in count.items():
        heap.append((-freq, word))           # negate: heapq is a min-heap
    heapq.heapify(heap)                      # O(n), cheaper than n pushes
    result = []
    for _ in range(k):
        result.append(heapq.heappop(heap)[1])
    return result
java
// java
// LC 692 - Top K Frequent Words
// IDEA: count with a map; a heap ordered by count desc, then word asc; poll k times
// time = O(n log n), space = O(n)
public List<String> topKFrequent(String[] words, int k) {
    Map<String, Integer> count = new HashMap<>();
    for (String word : words) {
        count.put(word, count.getOrDefault(word, 0) + 1);
    }
    PriorityQueue<String> heap = new PriorityQueue<>((a, b) -> {
        int byCount = Integer.compare(count.get(b), count.get(a));   // higher count first
        if (byCount != 0) {
            return byCount;
        }
        return a.compareTo(b);                                      // tie: alphabetical
    });
    heap.addAll(count.keySet());
    List<String> result = new ArrayList<>();
    for (int i = 0; i < k; i++) {
        result.add(heap.poll());
    }
    return result;
}

第二種解法 —— 依次數分桶,不用堆積。 次數最多是 len(words),所以可以拿來當串列的索引:buckets[c] 放剛好出現 c 次的單字。從高到低走一遍次數,頻率順序就自動排好了,而同分規則只會比較次數相同的單字。這就是 LC 347 的 O(n) 桶排序(heap § Template 2)再加上每個桶內排序 —— 面試官要你不用堆積做 top-K 時,值得會。

python
# python
# LC 692 - Top K Frequent Words (buckets by count)
# IDEA: buckets[c] = words seen exactly c times; walk counts high -> low, sorting each bucket for the tie rule
# time = O(n log n) worst case (every word in one bucket), O(n) when the counts are distinct, space = O(n)
def topKFrequent_buckets(words, k):
    count = {}
    for word in words:
        count[word] = count.get(word, 0) + 1
    buckets = [[] for _ in range(len(words) + 1)]   # a count is at most len(words)
    for word, freq in count.items():
        buckets[freq].append(word)
    result = []
    for freq in range(len(words), 0, -1):
        for word in sorted(buckets[freq]):          # the tie rule: alphabetical within one count
            result.append(word)
            if len(result) == k:
                return result
    return result

複雜度

  • Python 是 O(n + k log n):計數一趟,heapify 是 O(n),k 次 heappop 每次 O(log n)。Java 的 addAll 是一個一個插入,所以是 O(n log n)。分桶解法是 O(n),再加上它讀到的每個桶的排序。
  • 空間 O(n):count 和堆積(或桶)對每個相異單字各一個項目。

陷阱

  • 堆積裡放 (freq, word) 卻沒加負號,會先 pop 出最少出現的單字。
  • 大小為 k 的 min-heap(O(n log k) 的追問)需要反過來的同分順序,讓字母序較大的單字留在根部、最先被淘汰;沿用 (-count, word) 的順序會淘汰錯的單字。
  • Java 比較器寫成 count.get(b) - count.get(a) 就是減法比較器;要用 Integer.compare。

不適用時 —— 沒有同分規則、只要出現最多的 k 個值(LC 347)→ 依頻率做桶排序,O(n)(heap § Template 2);要的是第 k 大的值而不是次數 → heap.md。

練習 —— LC 692 · LC 347 ⭐ (沒有同分規則:堆積,或 O(n) 的桶) · LC 451 (每個鍵,依次數排序) · LC 1636 (次數遞增,同分時值遞減)

沒有同分規則時,top-K 不用堆積的 O(n) 解法見 heap § Template 2;用單調堆疊算出 next greater 再存進 map(LC 496),見 monotonic_stack § Template 1。

各語言 API —— 有序 map(TreeMap / SortedDict)

雜湊表不保留順序。題目需要最接近的鍵(floor / ceiling)、最小或最大的鍵,或某個範圍內的所有鍵時,改用有序 map:Java 的 TreeMap,或 Python 的 SortedDict。Python 標準函式庫沒有有序 map;SortedDict 來自第三方套件 sortedcontainers,LeetCode 有預先安裝(本機用 pip install sortedcontainers)。

你需要的 Java TreeMap Python SortedDict(d)
最小/最大的鍵 firstKey() / lastKey() d.keys()[0] / d.keys()[-1]
最小/最大的項目 firstEntry() / lastEntry() d.peekitem(0) / d.peekitem(-1) → (k, v)
≤ x 的最大鍵(floor) floorKey(x) → 鍵或 null i = d.bisect_right(x) - 1,i >= 0 時取 d.keys()[i]
≥ x 的最小鍵(ceiling) ceilingKey(x) → 鍵或 null i = d.bisect_left(x),i < len(d) 時取 d.keys()[i]
< x 的最大鍵/> x 的最小鍵 lowerKey(x) / higherKey(x) bisect_left(x) - 1 / bisect_right(x),同樣要檢查邊界
[lo, hi] 內的鍵 subMap(lo, true, hi, true) d.irange(lo, hi)
移除最小/最大的項目 pollFirstEntry() / pollLastEntry() d.popitem(0) / d.popitem(-1)
第 k 小的鍵 不支援(只能逐一走訪) d.keys()[k],O(log n)
單純查值 get(k) O(log n),沿樹往下走 O(1),雜湊查找
自訂順序 Comparator SortedDict(key_func) —— 是鍵的轉換,不是比較器

能擋掉大多數 bug 的規則:Java 的 floorKey / ceilingKey 回傳的是鍵或 null;Python 的 bisect_* 回傳的是索引,可能是 -1 或 len(d)。取值前先檢查索引,因為 d.keys()[-1] 會默默回傳最大的鍵,而不是「沒有」。另外要呼叫 d.bisect_left(x),不要寫 bisect.bisect_left(list(d.keys()), x):list(...) 的複製是 O(n)。

python
# python
# floor / ceiling on a SortedDict (pip install sortedcontainers; preinstalled on LeetCode)
# IDEA: bisect_* returns an index into the sorted keys; guard it, then read the key
# time = O(log n) per query, space = O(n)
from sortedcontainers import SortedDict

def floor_key(d, x):                       # Java floorKey(x): largest key <= x, or None
    i = d.bisect_right(x) - 1
    return d.keys()[i] if i >= 0 else None

def ceiling_key(d, x):                     # Java ceilingKey(x): smallest key >= x, or None
    i = d.bisect_left(x)
    return d.keys()[i] if i < len(d) else None

d = SortedDict({10: "a", 20: "b", 30: "c"})
print(floor_key(d, 25), ceiling_key(d, 25))   # 20 30
print(floor_key(d, 5), ceiling_key(d, 35))    # None None
print(list(d.irange(15, 30)))                 # [20, 30]
java
// java
// floor / ceiling / range on a TreeMap
// IDEA: a red-black tree keyed in order; floorKey / ceilingKey return a key or null
// time = O(log n) per query, space = O(n)
TreeMap<Integer, String> d = new TreeMap<>();
d.put(10, "a");
d.put(20, "b");
d.put(30, "c");
Integer floor = d.floorKey(25);            // 20
Integer ceiling = d.ceilingKey(25);        // 30
Integer none = d.floorKey(5);              // null: guard it before unboxing
SortedMap<Integer, String> range = d.subMap(15, true, 30, true);   // {20=b, 30=c}

用在哪裡:訂房與重疊檢查(LC 729、731、732)在 design_examples.md;剩下最小的牌(LC 846)和區間串流(LC 352)在 hash_map_examples.md;時間戳的 floor 查找(LC 981)在 design_examples.md。如果鍵只需要排序一次,就排序一個串列 —— 有序 map 為了保持有序,每次更新都要付 O(log n)。

練習路徑

階段 題目(由易到難) 新增的能力
核心 ⭐ LC 1 ok · LC 242 未評定 · LC 49 ok · LC 347 again · LC 146 again 查補數、計數、依鍵分組、依次數取前 K、map + 串列做快取(linked_list § Template 7)
練習 LC 205 ok · LC 290 未評定 · LC 383 未評定 · LC 953 ok · LC 438 again · LC 560 again · LC 133 ok 雙射、把計數當預算、排名表、固定視窗計數、前綴和計數、clone 表
延伸 LC 692 未評定 · LC 3026 未評定 · LC 149 未評定 · LC 710 未評定 帶同分規則的 top-K、每個鍵存最小前綴、正規化的斜率鍵、虛擬重新映射

總結與速查

題目 → 模式決策表

怎麼認出來 模式 模板 時間/空間 題目
湊出 target 的配對、三元組或補數;「two sum」、「k-diff」、「可被 60 整除」 查補數 T1 O(n) / O(n) 1, 15, 16, 18, 167, 532, 653, 1010, 1679, 1711, 2006
元素、字元或 pattern 的出現次數;「出現最多次」、「anagram」、重複值,或某個值出現在哪裡(「同一個連續區塊」) 計數/頻率表,含索引跨度變形 T2 O(n) / O(n) 242, 49, 451, 347, 692, 387, 819, 811, 1207, 383, 299, 349, 350, 4038, 763, 219, 1794
依某種推導出來的形式歸為一類;「group」、「同一條線」、「同一列或同一行」;(x, y) 算作 (y, x) 的對稱配對 用算出來的鍵分組,含無序配對鍵 T3 O(n·k) / O(n) 49, 149, 609, 939, 947, 987, 4066, 1128, 1615
子陣列的某個性質:和等於 k、和可被 k 整除、0 和 1 一樣多、剛好 k 個奇數;或兩端滿足某條件的子陣列的最大和 前綴和 → 計數表,含每個起點值存最小前綴的變形 T4 O(n) / O(n) 560, 325, 523, 525, 930, 974, 1248, 724, 3026
用字母計數比較的字元視窗(這裡是固定長度;先擴張再收縮的放在 sliding_window.md) 視窗 + 字元計數 T5 O(n) / O(k) 3, 76, 424, 438, 567, 159, 340, 904, 1004, 1208, 1234
「依照 … 給定的順序」、自訂字母表、某個排列、依索引切分 排名表 T6 O(n) / O(n) 953, 791, 105, 106
一個雙向都必須是一對一的映射 雙射(兩個 map) T7 O(n) / O(n) 205, 290
「設計一個 cache」、O(1) get + put、淘汰機制,或值得記憶化的遞迴 快取/記憶化 linked_list § T7 攤還 O(1) / O(n) 146, 460, 705, 706, 380, 381, 432, 355, 981, 1244, 139, 322, 1048
節點之間的關係:clone、父指標、子樹特徵、把等式當成邊 圖/樹的節點表 T8 O(n) / O(n) 133, 138, 652, 721, 734, 399, 947, 1257
「均勻隨機挑一個,但絕不能挑到這些值」 虛擬映射(重新映射) T9 每次挑 O(1) / O(B) 710, 398, 528, 384
光有 map 不夠 —— 你還需要順序、堆積或堆疊 map + 另一個結構 T10 視情況 347, 496, 503, 739, 853, 729, 846, 352

上表每一列的最壞情況,在所有鍵都碰撞時都是每次操作 O(n)。每一題實戰題,連同難度和它被詳解的那份文件,都在 hash_map_examples.md → Index。

關鍵洞見與辨識訊號

  1. 空間換時間:雜湊表用額外的 O(n) 空間換平均 O(1) 的查找
  2. 前綴和的魔法:subarray[i,j] = prefixSum[j] - prefixSum[i-1]
  3. 視窗狀態:用雜湊表有效率地維護視窗的性質
  4. 補數思維:不要窮舉所有配對,改成把元素存起來再查它的補數
  5. 索引還是值:想清楚 map 的值要存索引、存值,還是兩個都存
  6. 頻率計數:大部分字串/陣列題都能靠頻率分析解掉

面試時要留意的訊號:

  • 「找重複/重複出現的子字串」→ rolling hash,或二分搜尋 + 雜湊
  • 「把一組值一致地映射到另一組」→ 雙射(兩個 map)
  • 「把每個 x 換成 y」/「(a, b) 等同 (b, a)」→ 無序配對鍵 (min, max)
  • 「把快取最佳化」→ 用 OrderedDict/雙向鏈結串列做 LRU
  • 追問「如果陣列非常大呢?」→ 省空間的雜湊(rolling hash、座標壓縮)

實作最佳實務

Python 最佳實務

python
# 1. Use defaultdict for cleaner counting code
from collections import defaultdict
count = defaultdict(int)  # No need for get(key, 0)

# 2. Use Counter for frequency problems
from collections import Counter
freq = Counter(arr)  # Automatically counts frequencies

# 3. Handle edge cases with dict.get()
value = my_dict.get(key, default_value)

# 4. Clean up zero counts to save space
if count[key] == 0:
    del count[key]

# 5. Use enumerate when you need both index and value
for i, val in enumerate(arr):
    ...        # use both i and val

Java 最佳實務

java
// 1. Use getOrDefault to avoid null checks
map.put(key, map.getOrDefault(key, 0) + 1);

// 2. Use containsKey for existence checks
if (map.containsKey(key)) { /* ... */ }

// 3. Initialize with appropriate capacity
Map<String, Integer> map = new HashMap<>(expectedSize);

// 4. Use putIfAbsent for first occurrence
map.putIfAbsent(key, index);  // Only puts if key doesn't exist

效能:

  1. 選對雜湊函式:Python 內建的 hash 通常已經夠好
  2. 避免不必要的 rehash:能預先給定大小就給
  3. 記得清理:頻率表裡計數歸零的項目要移除
  4. 用合適的負載因子:預設的 0.75 通常就是最佳解

該避開的常見錯誤

  1. 以為不會碰撞:記住最壞情況的時間複雜度是 O(n),不是 O(1)

  2. 索引越界:

    python
    # Wrong: Can cause index errors
    if target - nums[i] in seen:
        return [i, seen[target - nums[i]]]
    seen[nums[i]] = i
    
    # Right: Check existence first
    if target - nums[i] in seen:
        return [seen[target - nums[i]], i]
    seen[nums[i]] = i
    
  3. 在走訪過程中修改 dict:

    python
    # Wrong: Can cause runtime errors
    for key in my_dict:
        if condition:
            del my_dict[key]
    
    # Right: Collect keys first
    to_delete = [k for k, v in my_dict.items() if condition]
    for k in to_delete:
        del my_dict[k]
    
  4. 忽略邊界情況:

    • 空陣列
    • 只有一個元素的陣列
    • 所有元素都相同
    • target 根本湊不出來
  5. 選錯資料結構:

    • 只要判斷存不存在就用 set()
    • 需要鍵值映射才用 dict()
    • 純粹算次數就用 Counter()

面試準備檢查清單

  • [ ] 熟練上面的每一份模板,以及各自的使用時機
  • [ ] 每個分類練 3-5 題
  • [ ] 搞懂每個模式的時間/空間複雜度
  • [ ] 知道常見的邊界情況,以及怎麼處理
  • [ ] 能把雜湊碰撞的解法講清楚
  • [ ] Python dict 和 Java HashMap 的 API 都要用得順
  • [ ] 知道什麼時候不該用雜湊表(已排序的資料、範圍查詢等等)