Collection(容器選擇)
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。
範圍 — 跨容器的選型指南:什麼工作該用哪個 Java/Python 容器,以及各自的 API 陷阱。 另見:hash_map.md;set.md;heap.md;queue.md;stack.md。
LeetCode 題目清單
0) 概念
0-1) 題型分類
-
演算法
- dict/collections 操作
- collections.Counter
- collections.Counter().most_common()
- 排序
- 取出出現最多次的元素
- 取出只由指定字母組成的子字串
- 自訂排序
- LC 791
- dict/collections 操作
-
資料結構
- dict
- set
- 陣列
1) 一般形式
1-0) 取出容器中存在的元素(自訂排序)
python
# LC 791. Custom Sort String
# ...
s_map = Counter(s)
res = ""
for o in order:
if o in s_map:
res += (o * s_map[o])
del s_map[o]
for s in s_map:
res += (s * s_map[s])
# ...
1-1) collection.Counter
python
import collections
s = ['a','b','c','c']
c= collections.Counter(s)
print (c)
print (c.keys())
print (c.values())
1-2) 重要方法 - most_common()
python
# 451 Sort Characters By Frequency
import collections
s = ['a','b','c','c']
count = collections.Counter(s).most_common()
for item, freq in count:
print (item, freq)
#c 2
#a 1
#b 1
1-3) collection.defaultdict (int, list…)
python
import collections
s = ['a','b','c','c']
count = collections.defaultdict(int)
for i in s:
count[i] += 1
print (count)
print (dict(count))
python
import collections
s=[('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
count = collections.defaultdict(list)
for k, v in s:
count[k].append(v)
print (count)
print (count.keys())
print (count.values())
print(count.items())
1-4) collection.update()
python
# LC # 554 rick Wall
import collections
_counter = collections.Counter()
_counter # -> Counter()
_counter.update([1])
_counter # -> Counter({1: 1})
_counter.update([1])
_counter # -> Counter({1: 2})
_counter.update([2])
_counter # -> Counter({1: 2, 2: 1})
1-5) OrderedDict(雜湊表 + 鏈結串列)— LC 146
python
# LC 146 LRU Cache
# OrderedDict = hash map + doubly linked list (Java: LinkedHashMap)
# https://docs.python.org/3/library/collections.html#collections.OrderedDict
#
# popitem(last=True) pop the newest (last=True, LIFO) or oldest (last=False, FIFO) pair
# move_to_end(key, last=True) move key to the end (or the front with last=False); KeyError if absent
#
# A plain dict also keeps insertion order (Python 3.7+). OrderedDict is still the LRU tool:
# move_to_end and popitem(last=False) are O(1), and a plain dict has neither.
#----------------------------
# example 0
#----------------------------
from collections import OrderedDict
d = OrderedDict()
for k in "abcde":
d[k] = k.upper()
for k, v in d.items():
print(k, v)
# -> a A
# b B
# c C
# d D
# e E
#----------------------------
# example 1
#----------------------------
d = OrderedDict.fromkeys('abcde')
d # -> OrderedDict({'a': None, 'b': None, 'c': None, 'd': None, 'e': None})
d.move_to_end('b')
"".join(d) # -> 'acdeb'
d.move_to_end('b', last=False)
"".join(d) # -> 'bacde'
#----------------------------
# example 2
#----------------------------
class LastUpdatedOrderedDict(OrderedDict):
'Store items in the order the keys were last added'
def __setitem__(self, key, value):
super().__setitem__(key, value)
self.move_to_end(key)
#----------------------------
# example 3
#----------------------------
from time import time
class TimeBoundedLRU:
"LRU Cache that invalidates and refreshes old entries."
def __init__(self, func, maxsize=128, maxage=30):
self.cache = OrderedDict() # { args : (timestamp, result)}
self.func = func
self.maxsize = maxsize
self.maxage = maxage
def __call__(self, *args):
if args in self.cache:
self.cache.move_to_end(args)
timestamp, result = self.cache[args]
if time() - timestamp <= self.maxage:
return result
result = self.func(*args)
self.cache[args] = time(), result
if len(self.cache) > self.maxsize:
self.cache.popitem(last=False) # evict the least recently used
return result
2) LC 範例
2-1) Custom Sort String — LC 791
python
# LC 791. Custom Sort String
# V0
# IDEA : COUNTER
from collections import Counter
class Solution(object):
def customSortString(self, order, s):
s_map = Counter(s)
res = ""
for o in order:
if o in s_map:
res += (o * s_map[o])
del s_map[o]
for s in s_map:
res += s * s_map[s]
return res
3) 這題該用哪個容器? 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
這份文件是選型指南。結構選定之後,演算法的細節請看各自的專門文件 — 見 3-5) 深入文件。
3-1) 容器速查表 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
| 容器 | 依 key 查找 | 依索引查找 | 最小/最大 | 有序走訪 | Java / Python |
|---|---|---|---|---|---|
| 陣列 / List | O(N) | O(1) | O(N) | 插入順序 | ArrayList / list |
| 雜湊表 | 平均 O(1) | – | O(N) | 無順序 | HashMap / dict, Counter |
| 雜湊集合 | 平均 O(1) | – | O(N) | 無順序 | HashSet / set |
| 堆積(PQ) | O(N) | – | peek O(1),pop O(log N) | 無順序 | PriorityQueue / heapq |
| 雙端佇列 | O(N) | 兩端 O(1) | 單調時 O(1) | 插入順序 | ArrayDeque / collections.deque |
| 鏈結串列 | O(N) | O(N) | O(N) | 插入順序 | 自行寫節點 / 自行寫節點 |
| 有序 Map | O(log N) + floor/ceiling | – | O(log N) | 依 key 排序 | TreeMap / 排序陣列上的 bisect |
| 保留插入順序的 Map | 平均 O(1) | – | O(N) | 插入/存取順序 | LinkedHashMap / OrderedDict |
Key Idea:挑能力最弱但仍答得出這個查詢的結構。一個 counter 就夠卻搬出堆積(或 HashMap 就夠卻用 TreeMap),代價是多一個 log N,還有面試分數。
3-2) 題目訊號 → 容器 → 原因 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
| 題目裡的訊號 | 該用 | 原因 | LC |
|---|---|---|---|
| 「計數/頻率/anagram/能不能用 Y 拼出 X」 | 雜湊表(Counter) |
每個字元 O(1),取代一層巢狀迴圈 | 383, 819 |
| 「top K/最近的 K 個/出現最多的 K 個」 | 大小為 K 的堆積 | 只留 K 個,其餘丟掉 → O(N log K) | 973, 692, 621 |
| 「合併 K 個已排序的東西」 | K 個頭節點組成的最小堆積 | O(log K) 取得下一個全域最小值 | 23 |
| 「滿足某條件的最長/最短視窗」 | 滑動視窗 + counter map | 視窗狀態在兩端都是 O(1) 更新 | 3, 904 |
| 「子陣列和為 K/數這種子陣列有幾個」 | 前綴和 + 雜湊表 | 把「區間」變成「兩個前綴的差」 | 560 |
| 「距離 k 以內出現過同樣的值」 | 雜湊表 value → last index |
只需要最後一次的索引,不必記全部 | 219 |
| 「切開後每個字母只出現在一段裡」 | 雜湊表 char → last index |
最後索引決定了這一段最少要延伸到哪 | 763 |
| 「自訂字母表/非標準順序」 | 長度 26 的 rank 陣列 | rank 查找 O(1),比 comparator 快 | 953, 791 |
| 「複製帶有任意指標的結構」 | 雜湊表 old node → new node |
一趟就解決前向參照 | 138 |
| 「insert / delete / getRandom 全部 O(1)」 | 陣列 + value → index map |
陣列負責隨機取;跟尾端交換讓刪除保持 O(1) | 380 |
| 「有淘汰機制的快取,操作全 O(1)」 | 雜湊表 + 雙向鏈結串列 | map 負責查找,串列負責記錄新舊順序 | 146, 460 |
| 「每一列/行/宮格內是否重複」 | 雜湊集合的陣列 | 每個限制條件一個集合,一趟掃完 | 36 |
| 「每個大小為 k 的視窗的最大/最小值」 | 單調雙端佇列(堆積只有搭配延遲刪除才行) | 被壓過的元素之後永遠用不到 | 239 |
| 「兩條串列/路徑會不會相交」 | 先用節點的雜湊集合,再優化成雙指標 | 集合先證明想法;指標再把 O(N) 空間拿掉 | 160 |
| 「某個 key 的 floor/ceiling/前驅」 | 有序 map(TreeMap / bisect) |
純雜湊表答不出「最接近的 key」 | 699, 218 |
| 「掃描線:現在有哪些是活躍的」 | 堆積 + 雜湊表(延遲刪除)或有序 multiset | 沒有 decrease-key → 標記為過期,讀取時再彈出 | 218 |
| 「值 → 符號,貪婪地從大的開始」 | 兩個平行陣列(由大到小) | 順序固定又很短;用 map 反而弄丟順序 | 12 |
| 「原地合併兩個已排序陣列」 | 不用額外容器 — 從後往前雙指標 | 往回寫永遠不會蓋掉還沒讀的資料 | 88 |
| 「網格:島嶼/區域/可達性」 | visited 二維陣列 + 佇列/堆疊 |
整數 key 很密集 → 陣列勝過雜湊集合 | 200, 419 |
| 「依指定順序造訪網格上的目標」 | 排序過的目標清單 + 每段做一次 BFS | 順序是全域的,距離是局部的 | 675 |
| 「iterator 需要往前看一個元素」 | 在欄位裡快取下一個元素 | 底層的 iterator 沒辦法倒回去 | 284 |
| 「建立/合併串列,head 可能會變」 | dummy head 節點,不需額外容器 | 把 head 的特例處理全部消掉 | 21, 2 |
3-3) 面試中真的會考的組合 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
| 組合 | 要維持的不變式 | LC | 完整模板 |
|---|---|---|---|
| 雜湊表 + 雙向鏈結串列 | map 的 value 直接指向節點,所以拆鏈是 O(1) | 146, 460 | design.md 2-3 / 2-4 |
陣列 + value → index map |
刪除時:把最後一個元素搬進洞裡,接著修正它在 map 裡的索引,最後才 pop() |
380 | array.md — LC 380 模式 |
| 堆積 + 雜湊表(「延遲刪除」) | 堆積裡可能有過期項目;只在堆頂和 map 不一致時才一直往外彈 | 218 | heap.md Template 8 |
| 有序 map(floor / ceiling) | key 保持排序;floorKey(x) = 最大的 ≤ x 的 key |
699, 218 | hash_map.md, java_trick.md |
| 前綴和 + 雜湊表 | 迴圈前先塞 {0: 1},而且要先讀再寫當前前綴 |
560 | prefix_sum.md |
| 滑動視窗 + counter map | 不合法就縮;記得刪掉計數為 0 的 key,否則 len(map) 會騙你 |
3, 904 | sliding_window.md |
| 雜湊集合的陣列 | 每個獨立限制一個集合,全部在同一趟填完 | 36 | array.md |
容易踩到的坑:
defaultdict會在讀取時建 key(d[k]就插入了)。Counter不會(c[k]回傳 0 但不插入),可是c[k] -= 1減到 0 時 key 還留著。當答案取決於len(d)(相異元素計數的視窗題)時,記得刪掉歸零的 key;d.get(k, 0)只是讀計數而不插入, 不會把歸零的 key 拿掉。
3-4) 上面那些 Python 操作的 Java 對應 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
前面幾節都只寫 Python;這裡是同一套工具的 Java 版。
| Python | Java |
|---|---|
Counter(s) / defaultdict(int) |
map.merge(k, 1, Integer::sum) 或 map.getOrDefault(k, 0) + 1 |
defaultdict(list) |
map.computeIfAbsent(k, x -> new ArrayList<>()).add(v) |
Counter(...).most_common(k) |
大小為 K 的 PriorityQueue 搭配 comparator |
OrderedDict(加上 move_to_end、popitem) |
LinkedHashMap(cap, 0.75f, true)(加上 removeEldestEntry) |
排序陣列上的 bisect |
TreeMap.floorKey / ceilingKey |
set |
HashSet(要保留插入順序就用 LinkedHashSet) |
java
// java
// IDEA: Java counterparts of Counter / defaultdict / most_common / OrderedDict / bisect
import java.util.*;
public class CollectionOps {
// 1) COUNT == Counter(s)
// time = O(N), space = O(K) (K = distinct keys)
public static Map<Character, Integer> count(String s) {
Map<Character, Integer> cnt = new HashMap<>();
for (char c : s.toCharArray()) {
cnt.merge(c, 1, Integer::sum); // or cnt.put(c, cnt.getOrDefault(c, 0) + 1)
}
return cnt;
}
// 2) GROUP == defaultdict(list)
// time = O(N), space = O(N)
public static Map<String, List<Integer>> group(String[] keys, int[] vals) {
Map<String, List<Integer>> m = new HashMap<>();
for (int i = 0; i < keys.length; i++) {
m.computeIfAbsent(keys[i], k -> new ArrayList<>()).add(vals[i]);
}
return m;
}
// 3) MOST COMMON == Counter(...).most_common(k) // LC 692 Top K Frequent Words
// time = O(N + K log K), space = O(K)
public static List<String> topK(String[] words, int k) {
Map<String, Integer> cnt = new HashMap<>();
for (String w : words) cnt.merge(w, 1, Integer::sum);
// min-heap of size k: LOWEST freq at the top; tie -> lexicographically LARGER pops first
PriorityQueue<String> pq = new PriorityQueue<>(
(a, b) -> cnt.get(a).equals(cnt.get(b)) ? b.compareTo(a) : Integer.compare(cnt.get(a), cnt.get(b)));
for (String w : cnt.keySet()) {
pq.offer(w);
if (pq.size() > k) pq.poll(); // evict the worst -> heap never exceeds k
}
LinkedList<String> res = new LinkedList<>();
while (!pq.isEmpty()) res.addFirst(pq.poll()); // heap pops worst-first -> push front
return res;
}
// 4) ORDERED DICT == OrderedDict // LC 146 LRU Cache
// time = O(1) per get/put, space = O(capacity)
static class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int cap;
LRUCache(int cap) {
super(cap, 0.75f, true); // accessOrder = true -> get() moves the key to the end
this.cap = cap;
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > cap; // auto-evict the least recently used
}
}
// 5) FLOOR / CEILING == bisect on a sorted list // LC 699 / LC 218
// time = O(log N) per query, space = O(N)
public static String floorCeil(TreeMap<Integer, Integer> tm, int q) {
return tm.floorKey(q) + " / " + tm.ceilingKey(q); // greatest <= q / smallest >= q
}
}
python
# python
# IDEA: the same 5 ops in Python (this is what the Java above mirrors)
import bisect, heapq
from collections import Counter, defaultdict, OrderedDict
# 1) COUNT
# time = O(N), space = O(K)
Counter("abcc") # Counter({'c': 2, 'a': 1, 'b': 1})
# 2) GROUP
# time = O(N), space = O(N)
g = defaultdict(list)
for k, v in [('yellow', 1), ('blue', 2), ('yellow', 3)]:
g[k].append(v) # {'yellow': [1, 3], 'blue': [2]}
# 3) MOST COMMON : freq DESC, then lexicographically ASC # LC 692
# time = O(N + K log K), space = O(K)
def top_k(words, k):
cnt = Counter(words)
return heapq.nsmallest(k, cnt.keys(), key=lambda w: (-cnt[w], w))
# 4) ORDERED DICT : LRU ops # LC 146
# time = O(1) per op, space = O(capacity)
d = OrderedDict()
d[1] = 1; d[2] = 2
d.move_to_end(1) # "touch" key 1 -> most recent
d[3] = 3
d.popitem(last=False) # evict LEAST recent (key 2) -> keys are [1, 3]
# 5) FLOOR / CEILING on a sorted list (Python has no TreeMap) # LC 699 / LC 218
# time = O(log N) query, O(N) insert (use sortedcontainers.SortedList for O(log N) insert)
keys, q = [10, 20, 30], 25
i = bisect.bisect_right(keys, q)
floor_ = keys[i - 1] if i else None # 20
ceil_ = keys[i] if i < len(keys) else None # 30
3-5) 深入文件
| 文件 | 用來查 |
|---|---|
| hash_map.md | 雜湊表模式、碰撞、TreeMap 有序 map API |
| hashing.md | rolling hash、自訂雜湊 key |
| set.md | 集合專屬模式、去重、成員檢查 |
| array.md | 原地技巧、索引當雜湊、LC 380 模式 |
| linked_list.md | dummy head、雙指標、反轉 |
| heap.md | top-K、K 路合併、延遲刪除(Template 8) |
| queue.md / monotonic_queue.md | 雙端佇列、滑動視窗極值(LC 239) |
| design.md | LRU / LFU / iterator 類的設計題(LC 146, 460, 284) |
| java_trick.md / python_trick.md | 各語言的容器 API |
3-6) 延伸練習
- LC 380 Insert Delete GetRandom O(1) — 陣列 +
value → indexmap - LC 460 LFU Cache — 雜湊表 + 用有序 map 做的頻率桶
- LC 692 Top K Frequent Words — counter + 大小為 K 的堆積(同分時比字串)
- LC 763 Partition Labels —
char → last indexmap - LC 560 Subarray Sum Equals K — 前綴和 + 雜湊表
- LC 36 Valid Sudoku — 雜湊集合的陣列
- LC 138 Copy List with Random Pointer —
old node → new nodemap - LC 219 Contains Duplicate II —
value → last indexmap - LC 953 Verifying an Alien Dictionary — 字元 rank 陣列
- LC 699 Falling Squares — 有序 map/座標掃描