雜湊表 — 實戰題解
範圍 — 雜湊表家族的經典 LC 題目,寫成題目卡,各自歸在它所歸約到的 hash_map.md 模板底下;本檔沒有自己的模板。 另見 — 核心文件:hash_map.md — 這些題目歸約到的十個模板、辨識表,以及有序 map(
TreeMap/SortedDict)的 API 參考。 鄰近文件:prefix_sum.md — 前綴和自成一家的完整說明;hashing.md — 雜湊怎麼運作,以及計數與 rolling hash 的慣用寫法;set.md — 只管有沒有,不管值。
LeetCode 題目清單
總覽
這份是 hash_map.md 的題目集:模板在那邊,這裡的每一題都是一張卡,寫明它歸約到哪個模板、以及多加了哪一步。各個 ## 章節依核心文件十個模板的順序排列。最好的詳解放在別份文件的題目,在這裡只留一列指標,所以 Index 仍然列出整組題目。
索引
| LC | 題目 | 歸約到 | 難度 | 詳解在 |
|---|---|---|---|---|
| 1 | Two Sum | hash_map § Template 1 | 簡單 | hash_map.md(錨點題) |
| 1010 | Pairs of Songs With Total Durations Divisible by 60 | hash_map § Template 1 | 中等 | 下方 |
| 532 | K-diff Pairs in an Array | hash_map § Template 1 | 中等 | set_examples.md |
| 336 | Palindrome Pairs | hash_map § Template 1 | 困難 | palindrome.md § Template 9 |
| 352 | Data Stream as Disjoint Intervals | hash_map § Template 1 | 困難 | 下方 |
| 242 | Valid Anagram | hash_map § Template 2 | 簡單 | hash_map.md(錨點題) |
| 554 | Brick Wall | hash_map § Template 2 | 中等 | 下方 |
| 846 | Hand of Straights | hash_map § Template 2 | 中等 | 下方 |
| 1726 | Tuple with Same Product | hash_map § Template 2 | 中等 | 下方 |
| 187 | Repeated DNA Sequences | hash_map § Template 2 | 中等 | hashing.md § 2-5 |
| 763 | Partition Labels | string § Template 8 | 中等 | string.md |
| 49 | Group Anagrams | hash_map § Template 3 | 中等 | hash_map.md(錨點題) |
| 734 | Sentence Similarity | hash_map § Template 3 | 簡單 | 下方 |
| 560 | Subarray Sum Equals K | hash_map § Template 4 | 中等 | hash_map.md(錨點題) |
| 525 | Contiguous Array | hash_map § Template 4 | 中等 | 下方 |
| 325 | Maximum Size Subarray Sum Equals k | hash_map § Template 4 | 中等 | prefix_sum_examples.md § 1 |
| 523 | Continuous Subarray Sum | hash_map § Template 4 | 中等 | prefix_sum_examples.md § 2 |
| 974 | Subarray Sums Divisible by K | hash_map § Template 4 | 中等 | prefix_sum.md § Template 3 |
| 1248 | Count Number of Nice Subarrays | hash_map § Template 4 | 中等 | prefix_sum.md § Template 6 |
| 438 | Find All Anagrams in a String | hash_map § Template 5 | 中等 | hash_map.md(錨點題) |
| 3 | Longest Substring Without Repeating Characters | sliding_window § Template 3 | 中等 | sliding_window.md |
| 424 | Longest Repeating Character Replacement | sliding_window § Template 3 | 中等 | sliding_window_examples.md § 5 |
| 953 | Verifying an Alien Dictionary | hash_map § Template 6 | 簡單 | hash_map.md(錨點題) |
| 670 | Maximum Swap | hash_map § Template 6 | 中等 | 下方 |
| 2471 | Minimum Number of Operations to Sort a Binary Tree by Level | hash_map § Template 6 | 中等 | 下方 |
| 205 | Isomorphic Strings | hash_map § Template 7 | 簡單 | hash_map.md(錨點題) |
| 290 | Word Pattern | hash_map § Template 7 | 簡單 | hash_map.md |
| 133 | Clone Graph | hash_map § Template 8 | 中等 | hash_map.md(錨點題) |
| 1257 | Smallest Common Region | hash_map § Template 8 | 中等 | 下方 |
| 710 | Random Pick with Blacklist | hash_map § Template 9 | 困難 | hash_map.md(錨點題) |
| 692 | Top K Frequent Words | hash_map § Template 10 | 中等 | hash_map.md(錨點題,堆積與分桶兩種解法) |
| 347 | Top K Frequent Elements | heap § Template 2 | 中等 | heap.md(堆積與分桶兩種解法) |
| 621 | Task Scheduler | heap § Template 8 | 中等 | heap_examples.md |
| 767 | Reorganize String | heap § Template 7 | 中等 | heap_examples.md |
| 36 | Valid Sudoku | hashing § Template 6 | 中等 | hashing.md |
| 146 | LRU Cache | linked_list § Template 7 | 中等 | linked_list.md |
| 139 | Word Break | dp § Template 1b | 中等 | dp.md |
| 322 | Coin Change | knapsack § Template 4 | 中等 | knapsack.md |
| 1048 | Longest String Chain | dp_advanced § Longest String Chain | 中等 | dp_advanced.md |
| 1044 | Longest Duplicate Substring | string_matching_kmp_rolling_hash § 2-3 | 困難 | string_matching_kmp_rolling_hash.md |
| 204 | Count Primes | combinatorics_math_patterns § Sieve | 中等 | combinatorics_math_patterns.md |
| 496 | Next Greater Element I | monotonic_stack § Template 1 | 簡單 | monotonic_stack.md |
| 503 | Next Greater Element II | monotonic_stack § Template 4 | 中等 | monotonic_stack.md |
| 739 | Daily Temperatures | stack § Template 3 | 中等 | stack.md |
| 853 | Car Fleet | monotonic_stack § 2-10 | 中等 | monotonic_stack.md |
| 729 | My Calendar I | hash_map § Language APIs | 中等 | design_examples.md |
| 731 | My Calendar II | hash_map § Language APIs | 中等 | design_examples.md |
| 732 | My Calendar III | hash_map § Language APIs | 困難 | design_examples.md |
歸約到指向別份文件的那一列,就在那份文件詳解;列在這裡,是為了讓在本檔搜尋時還找得到它。
查補數的例題
Pairs of Songs With Total Durations Divisible by 60 — LC 1010
關鍵 — 一首歌的補數是餘數,不是值:
t會和之前每一首t % 60等於(60 - t % 60) % 60的歌配對,所以 map 依餘數計歌數,答案是把次數加上去,而不是回傳一個索引。
歸約到:hash_map § Template 1 — 鍵是 t % 60,值是之前有幾首歌是這個餘數,因為題目要數配對,而不是找出一組。
# python
# LC 1010 - Pairs of Songs With Total Durations Divisible by 60
# IDEA: count songs per remainder; each song pairs with every earlier song whose remainder completes it to 0 mod 60
# time = O(n), space = O(1) at most 60 remainders
def numPairsDivisibleBy60(time):
count_of_rem = {} # {remainder: songs seen so far}
pairs = 0
for t in time:
rem = t % 60
need = (60 - rem) % 60 # remainder 0 pairs with 0, not with 60
pairs += count_of_rem.get(need, 0) # look up BEFORE recording: a song never pairs with itself
count_of_rem[rem] = count_of_rem.get(rem, 0) + 1
return pairs
陷阱
need = 60 - rem沒有外層的% 60,遇到 60 的倍數就會去查鍵 60:[60, 60, 60]回傳 0 而不是 3。- 先記錄再查找,餘數是 0 或 30 的歌會和自己配對:
[60]回傳 1 而不是 0。
Data Stream as Disjoint Intervals — LC 352
關鍵 — 新值
v只可能碰到結束於v - 1的區間,以及開始於v + 1的區間,所以兩個邊界 map ——{start: end}和{end: start}—— 讓每次合併都只是兩次 O(1) 查找。這兩個 map 永遠剛好存著目前的不相交區間。
歸約到:hash_map § Template 1 — 要探查的「夥伴」是鄰居 v - 1 和 v + 1,各在一個邊界 map 裡;只有 getIntervals 需要排序。
# python
# LC 352 - Data Stream as Disjoint Intervals
# IDEA: {start: end} and {end: start}; a new value probes v-1 (an end) and v+1 (a start) and merges with each
# time = O(1) per addNum, O(k log k) per getIntervals, space = O(n) k = disjoint intervals
class SummaryRanges:
def __init__(self):
self.seen = set() # every value added; a repeat changes nothing
self.start_to_end = {}
self.end_to_start = {}
def addNum(self, value):
if value in self.seen:
return
self.seen.add(value)
start, end = value, value
if value - 1 in self.end_to_start: # an interval ends right before value
start = self.end_to_start.pop(value - 1)
if value + 1 in self.start_to_end: # an interval starts right after value
end = self.start_to_end.pop(value + 1)
self.start_to_end[start] = end # overwrites the left interval's old end
self.end_to_start[end] = start # overwrites the right interval's old start
def getIntervals(self):
result = []
for start in sorted(self.start_to_end):
result.append([start, self.start_to_end[start]])
return result
試跑
add start_to_end end_to_start getIntervals
1 {1:1} {1:1} [[1,1]]
3 {1:1, 3:3} {1:1, 3:3} [[1,1],[3,3]]
7 {1:1, 3:3, 7:7} {1:1, 3:3, 7:7} [[1,1],[3,3],[7,7]]
2 {1:3, 7:7} {3:1, 7:7} [[1,3],[7,7]] 2 joins [1,1] and [3,3]
6 {1:3, 6:7} {3:1, 7:6} [[1,3],[6,7]]
陷阱
- 沒有
seen集合:依序加入1, 3, 2之後再加一次2,第二次找不到相鄰的邊界,於是在[1, 3]裡面多出一個[2, 2]。 - 合併時只 pop 一邊,會留下過時的邊界:把
[1,1]和[3,3]接起來後,若還留著end_to_start[1],之後的2又會再合併一次。
不適用時 — getIntervals 呼叫得遠比 addNum 頻繁:把區間存在有序 map 裡,讀取就是 O(k)、不必每次重新排序(hash_map § Language APIs)。
| 也歸約到 Template 1 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 1 | Two Sum | hash_map § Template 1 | 簡單 |
| LC 532 | K-diff Pairs in an Array | set_examples.md | 中等 |
| LC 336 | Palindrome Pairs | palindrome.md § Template 9 | 困難 |
頻率計數的例題
Brick Wall — LC 554
關鍵 — 垂直線在磚縫最多的地方穿過的磚最少,所以去數磚縫的位置(每一列累積的寬度,不含牆的右邊界),答案是
列數 - 最大的次數。
歸約到:hash_map § Template 2 — 被計數的東西是前綴寬度,而不是輸入裡的元素。
# python
# LC 554 - Brick Wall
# IDEA: count every inner edge position across rows; cross where the most edges line up
# time = O(total bricks), space = O(distinct edge positions)
def leastBricks(wall):
edges_at = {} # {distance from the left: rows with an edge there}
for row in wall:
position = 0
for i in range(len(row) - 1): # the last brick ends at the wall's edge: never a gap
position += row[i]
edges_at[position] = edges_at.get(position, 0) + 1
most_edges = 0
for count in edges_at.values():
most_edges = max(most_edges, count)
return len(wall) - most_edges
陷阱
- 把每列的最後一塊磚也算進去:牆的右邊界每一列都有,
most_edges會變成len(wall),每個輸入都回傳 0。 - 每列都只有一塊磚(
[[1],[1],[1]]):沒有任何內部磚縫,map 是空的,對它取max()會丟例外 —— 把most_edges從 0 開始,答案就是 3。
Hand of Straights — LC 846
關鍵 — 剩下最小的那張牌只能當一組的開頭,所以依排序走過相異的牌,對接下來
groupSize張牌各花掉count[card]張,永遠不會做出本來可以更好的選擇。
歸約到:hash_map § Template 2 — 計數表依鍵的排序讀取;Java 的 TreeMap 免費維持這個順序(hash_map § Language APIs)。
# python
# LC 846 - Hand of Straights
# IDEA: count the cards; from the smallest card up, every copy of it starts a run of groupSize consecutive cards
# time = O(n log n), space = O(n)
def isNStraightHand(hand, groupSize):
if len(hand) % groupSize != 0:
return False
count = {}
for card in hand:
count[card] = count.get(card, 0) + 1
for card in sorted(count): # the smallest remaining card must start a group
starts = count[card] # groups that start at this card
if starts == 0:
continue
for nxt in range(card, card + groupSize):
if count.get(nxt, 0) < starts:
return False # a run would have a gap
count[nxt] -= starts
return True
複雜度 — 排序相異的牌是 O(n log n);內層迴圈只對當開頭的牌執行,而每次至少用掉 groupSize 張牌,所以總共是 O(n)。
陷阱
- 依插入順序走過鍵:
hand = [2, 1], groupSize = 2從 2 開始,找不到 3,回傳False;依排序則從 1 開始,回傳True。 - 一次只扣一組而不是
starts組:結果正確,但[1]*k + [2]*k配groupSize = 2會把同一段重走 k 次。
Tuple with Same Product — LC 1726
關鍵 — 數出每個乘積有幾組索引配對;同一個乘積有
c組配對,就有C(c, 2)種「兩組配對」的選法,而每一種(a, b), (c, d)會給出 8 個有序 tuple,所以答案是sum(8 * C(c, 2))。
歸約到:hash_map § Template 2 — 被計數的是一組配對的乘積,答案是把次數組合起來,而不是互相比較。
# python
# LC 1726 - Tuple with Same Product
# IDEA: count index pairs per product; c pairs give C(c, 2) choices, each yielding 8 orderings
# time = O(n^2), space = O(n^2)
def tupleSameProduct(nums):
pairs_with = {} # {product: index pairs i < j with that product}
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
product = nums[i] * nums[j]
pairs_with[product] = pairs_with.get(product, 0) + 1
total = 0
for c in pairs_with.values():
total += 8 * (c * (c - 1) // 2) # choose 2 pairs, then 2 * 2 * 2 orderings
return total
// java
// LC 1726 - Tuple with Same Product
// IDEA: count index pairs per product; c pairs give C(c, 2) choices, each yielding 8 orderings
// time = O(n^2), space = O(n^2)
public int tupleSameProduct(int[] nums) {
Map<Integer, Integer> pairsWith = new HashMap<>(); // values <= 10^4, so a product fits an int
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
pairsWith.merge(nums[i] * nums[j], 1, Integer::sum);
}
}
int total = 0;
for (int c : pairsWith.values()) {
total += 4 * c * (c - 1); // 8 * c * (c - 1) / 2, without the division
}
return total;
}
陷阱
- 忘了乘 8:
[2, 3, 4, 6]只有一種兩組配對(2·6 = 3·4),答案是 8 而不是 1。這些順序是:第一組內 2 種 × 第二組內 2 種 × 哪一組在前 2 種。 - 乘 8 的前提是兩組配對沒有共用元素。這只因為值都相異才成立:
a·b = a·c會推出b = c。
| 也歸約到 Template 2 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 242 | Valid Anagram | hash_map § Template 2 | 簡單 |
| LC 187 | Repeated DNA Sequences | hashing.md § 2-5 | 中等 |
| LC 763 | Partition Labels | string § Template 8 | 中等 |
用算出來的鍵分組的例題
Sentence Similarity — LC 734
關鍵 — 相似關係是對稱但不遞移的,所以每一組配對都用無序鍵
(min, max)只存一次,每個字的位置就是一次 O(1) 的成員查詢;不需要圖。
歸約到:hash_map § Template 3 — 無序配對鍵 — 標準鍵讓 ["great", "fine"] 和 ["fine", "great"] 成為同一筆;因為不需要計數,用集合就夠了。
# python
# LC 734 - Sentence Similarity
# IDEA: store each similar pair under the key (min, max); compare the sentences word by word
# time = O(n + p), space = O(p) n words, p pairs
def areSentencesSimilar(sentence1, sentence2, similarPairs):
if len(sentence1) != len(sentence2):
return False
similar = set()
for a, b in similarPairs:
similar.add((min(a, b), max(a, b))) # {a, b} in either order -> one key
for a, b in zip(sentence1, sentence2):
if a != b and (min(a, b), max(a, b)) not in similar:
return False
return True
陷阱
- 在原始配對串列裡查
[a, b]:結果正確,但每個字都是 O(p),而且還得再試一次[b, a]。 - 一個字和自己相似,不需要列出配對:沒有任何配對時
["great"]對["great"]是True,a != b的檢查會給出這個結果。 - 沒有遞移性:
great ~ fine和fine ~ good在這裡不會推出great ~ good。LC 737 加上了遞移性,那就需要 union-find(dfs_examples.md)。
| 也歸約到 Template 3 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 49 | Group Anagrams | hash_map § Template 3 | 中等 |
前綴和計數表的例題
Contiguous Array — LC 525
關鍵 — 把每個
0當成-1,子陣列裡 0 和 1 一樣多,若且唯若它的和是 0,也就是若且唯若它兩端的累積和相等。map 記下每個累積和第一次出現的索引,並預先放入{0: -1},所以i - first[sum]就是結尾在i的最長平衡子陣列。
歸約到:hash_map § Template 4 — 目標是相同的前綴和(k = 0),而 map 存的是第一次出現的索引而不是次數,因為題目問的是最長。
# python
# LC 525 - Contiguous Array
# IDEA: 0 -> -1, then the longest subarray with sum 0 = the widest gap between two equal running sums
# time = O(n), space = O(n)
def findMaxLength(nums):
first_index = {0: -1} # running sum 0 before index 0: subarrays that start at 0
running = 0
longest = 0
for i, x in enumerate(nums):
running += 1 if x == 1 else -1
if running in first_index:
longest = max(longest, i - first_index[running])
else:
first_index[running] = i # keep the FIRST index only: it gives the longest span
return longest
// java
// LC 525 - Contiguous Array
// IDEA: 0 -> -1, then the longest subarray with sum 0 = the widest gap between two equal running sums
// time = O(n), space = O(n)
public int findMaxLength(int[] nums) {
Map<Integer, Integer> firstIndex = new HashMap<>();
firstIndex.put(0, -1); // subarrays that start at index 0
int running = 0;
int longest = 0;
for (int i = 0; i < nums.length; i++) {
running += (nums[i] == 1) ? 1 : -1;
if (firstIndex.containsKey(running)) {
longest = Math.max(longest, i - firstIndex.get(running));
} else {
firstIndex.put(running, i); // first occurrence only
}
}
return longest;
}
試跑
nums = [0,0,0,0,1,1]
i x running first_index (after) longest
0 0 -1 {0:-1, -1:0} 0
1 0 -2 {..., -2:1} 0
2 0 -3 {..., -3:2} 0
3 0 -4 {..., -4:3} 0
4 1 -3 -3 seen at 2 -> 4 - 2 = 2 2
5 1 -2 -2 seen at 1 -> 5 - 1 = 4 4 nums[2..5] = [0,0,1,1]

陷阱
- 沒有預先放入
{0: -1}:[0, 1]回傳 0 而不是 2,因為那個平衡子陣列從索引 0 開始。 - 每次重複都更新索引,留下的是最後一次出現:
[0, 1, 0, 1]回傳 2 而不是 4。 - 計數表(LC 560 的
{sum: count})回答的是「有幾個」,不是「有多長」;這裡的值必須是索引。
| 也歸約到 Template 4 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 560 | Subarray Sum Equals K | hash_map § Template 4 | 中等 |
| LC 325 | Maximum Size Subarray Sum Equals k | prefix_sum_examples.md § 1 | 中等 |
| LC 523 | Continuous Subarray Sum | prefix_sum_examples.md § 2 | 中等 |
| LC 974 | Subarray Sums Divisible by K | prefix_sum.md § Template 3 | 中等 |
| LC 1248 | Count Number of Nice Subarrays | prefix_sum.md § Template 6 | 中等 |
固定視窗字元計數的例題
錨點題在核心文件裡詳解;長度可變、同樣維護計數表的視窗,在滑動視窗的文件裡詳解。
| 歸約到 Template 5,或歸約到滑動視窗 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 438 | Find All Anagrams in a String | hash_map § Template 5 | 中等 |
| LC 3 | Longest Substring Without Repeating Characters | sliding_window § Template 3 | 中等 |
| LC 424 | Longest Repeating Character Replacement | sliding_window_examples.md § 5 | 中等 |
排名表的例題
Maximum Swap — LC 670
關鍵 —
last[d]是數字d最右邊的索引。第一個「後面還有更大數字」的位置i,是一次交換唯一能把最高位數字變大的地方,而最好的交換對象是這些數字裡最大的那個、取它的最後一個位置。
歸約到:hash_map § Template 6 — 一次建好的值 → 位置 map,所以「我右邊有沒有更大的數字、在哪裡?」最多只要 9 次查找。不用 map 的三指標掃描在 array_examples.md § 4。
# python
# LC 670 - Maximum Swap
# IDEA: {digit: last index}; at the first position beaten by a larger digit later, swap with that digit's last copy
# time = O(n), space = O(1) n digits, at most 10 keys
def maximumSwap(num):
digits = list(str(num))
last = {}
for i, ch in enumerate(digits):
last[int(ch)] = i # a later copy overwrites: the LAST index wins
for i, ch in enumerate(digits):
current = int(ch)
for d in range(9, current, -1): # the largest digit first
if last.get(d, -1) > i:
j = last[d]
digits[i], digits[j] = digits[j], digits[i]
return int("".join(digits)) # only one swap is allowed
return num # already the largest arrangement
// java
// LC 670 - Maximum Swap
// IDEA: {digit: last index}; at the first position beaten by a larger digit later, swap with that digit's last copy
// time = O(n), space = O(1) 10 digits
public int maximumSwap(int num) {
char[] digits = String.valueOf(num).toCharArray();
int[] last = new int[10];
for (int i = 0; i < digits.length; i++) {
last[digits[i] - '0'] = i;
}
for (int i = 0; i < digits.length; i++) {
int current = digits[i] - '0';
for (int d = 9; d > current; d--) {
if (last[d] > i) {
char tmp = digits[i];
digits[i] = digits[last[d]];
digits[last[d]] = tmp;
return Integer.parseInt(new String(digits));
}
}
}
return num;
}
試跑
num = 2736 last = {2:0, 7:1, 3:2, 6:3}
i=0 digit 2: try 9, 8 (absent), 7 -> last[7] = 1 > 0 -> swap 0 and 1 -> 7236, stop
陷阱
- 和較大數字的第一個位置交換:
1993會得到9193;用最後一個位置才是9913。 - 取第一個找到的較大數字,而不是最大的:
2736會把 2 和 3 交換,回傳3726。 - 沒有可以改進的位置(
9973):原封不動回傳num。
Minimum Number of Operations to Sort a Binary Tree by Level — LC 2471
關鍵 — 各層彼此獨立,所以答案是把每一層排好所需最少交換次數的總和。
{value: current index}map 讓每次交換都是 O(1):在索引i,取出該放在這裡的值目前在哪,更新 map 的兩筆,再交換。每次交換都會把一個值放到最終位置,所以次數是最少的。
歸約到:hash_map § Template 6 — 一個值 → 位置的 map,隨著陣列變動保持最新;BFS 負責提供每一層。
# python
# LC 2471 - Minimum Number of Operations to Sort a Binary Tree by Level
# IDEA: BFS per level; min swaps to sort a level = place sorted[i] at i via a {value: current index} map
# time = O(n log n), space = O(w) w = the widest level
from collections import deque
def minimumOperations(root):
queue = deque([root])
operations = 0
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
operations += min_swaps(level)
return operations
def min_swaps(arr):
target = sorted(arr)
position = {} # {value: current index}; values are unique
for i, v in enumerate(arr):
position[v] = i
swaps = 0
for i in range(len(arr)):
want = target[i]
if arr[i] != want:
j = position[want] # where the value that belongs at i sits now
position[arr[i]] = j # update the map BEFORE the swap overwrites arr[i]
position[want] = i
arr[i], arr[j] = arr[j], arr[i]
swaps += 1
return swaps
// java
// LC 2471 - Minimum Number of Operations to Sort a Binary Tree by Level
// IDEA: BFS per level; min swaps to sort a level = place sorted[i] at i via a {value: current index} map
// time = O(n log n), space = O(w)
public int minimumOperations(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int operations = 0;
while (!queue.isEmpty()) {
int size = queue.size();
int[] level = new int[size];
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level[i] = node.val;
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
operations += minSwaps(level);
}
return operations;
}
private int minSwaps(int[] arr) {
int[] target = arr.clone();
Arrays.sort(target);
Map<Integer, Integer> position = new HashMap<>(); // {value: current index}
for (int i = 0; i < arr.length; i++) {
position.put(arr[i], i);
}
int swaps = 0;
for (int i = 0; i < arr.length; i++) {
int want = target[i];
if (arr[i] != want) {
int j = position.get(want);
position.put(arr[i], j); // update the map before the swap
position.put(want, i);
arr[j] = arr[i];
arr[i] = want;
swaps++;
}
}
return swaps;
}
試跑
min_swaps([3, 1, 2]) target = [1, 2, 3] position = {3:0, 1:1, 2:2}
i=0 want 1 at j=1 position -> {3:1, 1:0} arr = [1, 3, 2] swaps = 1
i=1 want 2 at j=2 position -> {3:2, 2:1} arr = [1, 2, 3] swaps = 2
i=2 3 already in place -> 2
陷阱
- 先交換、再更新 map:
arr[i]已經是want了,被換走的值的新索引會寫到錯的鍵底下。 - 用
arr.index(want)找want而不是查 map:結果正確,但每次交換 O(w),每層 O(w²)。
| 也歸約到 Template 6 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 953 | Verifying an Alien Dictionary | hash_map § Template 6 | 簡單 |
雙射的例題
這張卡的兩題都在核心文件裡詳解。
| 歸約到 Template 7 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 205 | Isomorphic Strings | hash_map § Template 7 | 簡單 |
| LC 290 | Word Pattern | hash_map § Template 7 | 簡單 |
舊節點對新節點表的例題
Smallest Common Region — LC 1257
關鍵 — 每個串列列出一個父區域和它的子區域,所以
{child: parent}map 把這些串列變成一棵有父指標的樹,答案就是最低共同祖先:把region1的所有祖先放進集合,再讓region2往上走,直到碰到其中一個。
歸約到:hash_map § Template 8 — map 是 {child: parent} 而不是 {original: clone};往上走的那段就是 tree_lca_distance.md 的父指標 LCA。
# python
# LC 1257 - Smallest Common Region
# IDEA: {child: parent} from the lists; region1's ancestors in a set; walk region2 up until it hits one
# time = O(total names), space = O(total names)
def findSmallestRegion(regions, region1, region2):
parent = {}
for group in regions:
for child in group[1:]: # group[0] contains every other name in the list
parent[child] = group[0]
ancestors = set()
node = region1
while node is not None:
ancestors.add(node) # region1 counts as its own ancestor
node = parent.get(node) # the root has no parent -> None
node = region2
while node not in ancestors: # the root is in the set, so this stops
node = parent[node]
return node
// java
// LC 1257 - Smallest Common Region
// IDEA: {child: parent} from the lists; region1's ancestors in a set; walk region2 up until it hits one
// time = O(total names), space = O(total names)
public String findSmallestRegion(List<List<String>> regions, String region1, String region2) {
Map<String, String> parent = new HashMap<>();
for (List<String> group : regions) {
for (int i = 1; i < group.size(); i++) {
parent.put(group.get(i), group.get(0));
}
}
Set<String> ancestors = new HashSet<>();
for (String node = region1; node != null; node = parent.get(node)) {
ancestors.add(node);
}
String node = region2;
while (!ancestors.contains(node)) {
node = parent.get(node);
}
return node;
}
陷阱
- 沒把
region1放進它自己的祖先集合:當region1包含region2("North America"、"New York")時,往上走會越過"North America",答成"Earth"。 - 改存
{parent: [children]}:那是由上往下 DFS 的正確形狀,但要找祖先就得從根開始搜尋。
| 也歸約到 Template 8 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 133 | Clone Graph | hash_map § Template 8 | 中等 |
虛擬映射的例題
| 歸約到 Template 9 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 710 | Random Pick with Blacklist | hash_map § Template 9 | 困難 |
map 加堆積的例題
LC 692 在核心文件裡詳解,有堆積和依次數分桶兩種解法。其他從計數表出發的 top-K 與排程題,在堆積的文件裡詳解。
| 歸約到 Template 10,或歸約到堆積的卡 | 題目 | 詳解在 | 難度 |
|---|---|---|---|
| LC 692 | Top K Frequent Words | hash_map § Template 10 | 中等 |
| LC 347 | Top K Frequent Elements | heap § Template 2 | 中等 |
| LC 621 | Task Scheduler | heap_examples.md,歸約到 heap § Template 8 | 中等 |
| LC 767 | Reorganize String | heap_examples.md,歸約到 heap § Template 7 | 中等 |