实用优先 :高频容器在前,哈希底层原理在后。先能干活,再懂原理。
目录
容器选型 30 秒决策
std::vector — 你的默认容器
std::string — 特殊的 vector<char>
std::unordered_map — O(1) 哈希表
std::unordered_set — 去重 + 快速判断
std::map / std::set — 有序的红黑树
第二梯队容器:deque / array / list
容器适配器:stack / queue / priority_queue
全容器复杂度速查
哈希底层原理:6 种构造 + 4 种冲突解决
自实现哈希表
面试高频考点
1. 容器选型 30 秒决策
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 顺序存储一堆东西? ├── 需要随机访问 [i]? │ ├── 大小编译期已知 → std::array │ ├── 只在尾部增删 → std::vector ← 90% 的情况 │ └── 需要在头部增删 → std::deque └── 频繁在中间插入/删除 → std::list(一般别用,见 §7) 键值查找? ├── 需要按 key 排序遍历 → std::map ├── 只需要 O(1) 查找 → std::unordered_map ├── 允许重复 key → std::multimap / unordered_multimap └── 只存 key 无 value → std::set / unordered_set 特殊行为? ├── 始终取最大/最小 → std::priority_queue (Top-K、Dijkstra) ├── 先进先出 → std::queue (BFS、消息队列) ├── 后进先出 → std::stack (DFS、括号匹配) └── 连续内存 + 字符串语义 → std::string
一句话速记
容器
一句话
内部结构
vector
默认容器,连续内存
动态数组
string
vector<char> + 字符串操作
动态数组 + SSO
unordered_map
O(1) 键值查找
哈希表(链地址法)
unordered_set
O(1) 去重/判存在
哈希表
map
按键排序的键值对
红黑树
set
有序去重集合
红黑树
deque
头尾都能快速增删
分块数组
array
安全版 C 数组
栈上定长数组
priority_queue
始终取最值
堆(默认大顶堆)
list
双向链表(别用)
双向链表
2. std::vector — 你的默认容器
内部结构: 三指针 —— begin / end / capacity_end
1 2 3 [1][2][3][?][?][?][?][?] |<--size-->| |<------capacity------>|
2.1 声明
1 2 3 4 5 6 7 #include <vector> vector<int > v1; vector<int > v2 (10 ) ; vector<int > v3 (10 , -1 ) ; vector<int > v4 = {1 , 2 , 3 , 4 , 5 }; vector<vector<int >> mat (3 , vector <int >(4 , 0 ));
2.2 核心操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 vector<int > v = {1 , 2 , 3 }; v.push_back (4 ); v.emplace_back (5 ); v.pop_back (); v.erase (v.begin () + 1 ); v.clear (); int a = v[2 ]; int b = v.at (2 ); int c = v.front (); int d = v.back (); v.size (); v.capacity (); v.empty ();
2.3 size vs capacity(必考)
1 2 3 4 5 6 7 8 vector<int > v; v.reserve (100 ); v[0 ] = 42 ; v.push_back (42 ); v.resize (5 ); v.resize (10 , -1 ); v.resize (3 );
2.4 遍历删元素(经典坑)
1 2 3 4 5 6 7 8 9 10 11 12 vector<int > v = {10 , 20 , 30 , 40 , 50 }; for (auto it = v.begin (); it != v.end (); ) { if (*it % 20 == 0 ) it = v.erase (it); else ++it; } v.erase (remove (v.begin (), v.end (), 30 ), v.end ());
2.5 扩容机制
1 2 3 4 GCC/libstdc++: 1 → 2 → 4 → 8 → 16 ... (×2) MSVC: 1 → 2 → 3 → 4 → 6 → 9 ... (×1.5) 均摊分析:插入 n 个元素,总拷贝 < 2n → 每次 push_back 均摊 O(1)
操作
复杂度
push_back / emplace_back
O(1) 均摊
pop_back
O(1)
operator[] / at() / front / back
O(1)
insert(pos) / erase(pos)
O(n) ⚠️
find()
O(n)
sort()
O(n log n)
reserve() / resize()
O(n)
3. std::string — 特殊的 vector<char>
内部: 和 vector 一样连续存储 + SSO(短字符串优化) ——短字符串不分配堆内存。
3.1 常用操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <string> string s = "hello" ; string s2 ("world" ) ;string s3 (10 , 'x' ) ; string t = s + " " + s2; s += "!" ; s.append (" world" ); string sub = s.substr (0 , 3 ); string_view sv = s; size_t pos = s.find ("lo" ); if (s.find ("xx" ) == string::npos) { }bool start = s.starts_with ("he" ); bool end = s.ends_with ("lo" ); int n = stoi ("42" );double d = stod ("3.14" );string s = to_string (123 ); const char * cstr = s.c_str ();
3.2 string_view(C++17)— 零拷贝参数
1 2 3 4 5 6 7 void process (string_view sv) { cout << sv.substr (0 , 3 ); } process ("literal" ); process (string ("hello" )); process (s);
原则: 函数参数用 string_view,返回值用 string(别返回 string_view 指向临时对象)。
4. std::unordered_map — O(1) 哈希表
内部结构: vector<list<pair<K,V>>> — 桶数组 + 链表(链地址法)
负载因子默认 1.0,超过后自动 rehash(扩容 + 重新分配所有元素)
4.1 增删查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <unordered_map> unordered_map<string, int > m; m["alice" ] = 25 ; m.insert ({"bob" , 30 }); m.emplace ("charlie" , 22 ); m.insert_or_assign ("alice" , 27 ); m.try_emplace ("diana" , 28 ); auto it = m.find ("alice" ); if (m.contains ("alice" )) { ... } if (m.count ("alice" )) { ... } int x = m["nonexistent" ]; int y = m.at ("nonexistent" ); m.erase ("alice" ); m.erase (it); m.clear ();
4.2 遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 for (const auto & [key, value] : m) { cout << key << " → " << value << '\n' ; } for (auto it = m.begin (); it != m.end (); ) { if (it->second < 20 ) it = m.erase (it); else ++it; }
4.3 自定义键的哈希
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Person { string name; int age; };struct PersonHash { size_t operator () (const Person& p) const { return hash<string>{}(p.name) ^ (hash<int >{}(p.age) << 1 ); } }; struct PersonEq { bool operator () (const Person& a, const Person& b) const { return a.name == b.name && a.age == b.age; } }; unordered_map<Person, string, PersonHash, PersonEq> m;
操作
平均
最坏
insert / find / erase(k)
O(1)
O(n)
operator[] / at()
O(1)
O(n)
遍历全部
O(n)
O(n)
rehash / reserve
O(n)
O(n)
最坏 O(n):所有 key 哈希到同一个桶(恶意构造 / 极差哈希函数)
5. std::unordered_set — 去重 + 快速判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <unordered_set> unordered_set<int > s = {1 , 5 , 6 , 7 , 8 }; s.insert (9 ); s.emplace (10 ); if (s.contains (6 )) { ... } if (s.find (6 ) != s.end ()) { ... } if (s.count (6 )) { ... } s.erase (5 ); unordered_set<int > a = {1 ,2 ,3 }, b = {3 ,4 ,5 }, r; for (int x : a) if (b.count (x)) r.insert (x); for (int x : a) r.insert (x);for (int x : b) r.insert (x); for (int x : a) if (!b.count (x)) r.insert (x);
6. std::map / std::set — 有序的红黑树
一句话选型: 需要按 key 排序遍历 → map/set;只需 O(1) 查找 → unordered 版本。
1 2 3 4 5 6 7 8 内存中的红黑树: [4] / \ [2] [6] / \ / \ [1] [3] [5] [7] 中序遍历 = 1, 2, 3, 4, 5, 6, 7 ← 自动有序
6.1 基本操作
1 2 3 4 5 6 7 8 9 10 #include <map> #include <set> map<string, int > m = {{"c" ,3 }, {"a" ,1 }, {"b" ,2 }}; set<int > s = {3 , 1 , 2 };
6.2 map 独有的有序操作
1 2 3 4 5 6 7 8 9 10 11 map<int , string> m = {{1 ,"a" }, {3 ,"c" }, {5 ,"e" }}; auto lb = m.lower_bound (3 ); auto ub = m.upper_bound (3 ); auto [lo, hi] = m.equal_range (3 );for (auto rit = m.rbegin (); rit != m.rend (); ++rit) { }map<int , string, greater<int >> desc;
6.3 哈希 vs 红黑树 对比
1 2 3 4 5 6 7 8 9 unordered_map map ─────────────────────────────────────────────────── 底层 哈希表 红黑树 查找/插入/删除 O(1) 平均 O(log n) 稳定 遍历顺序 随机 按键排序 内存 较大(桶+链表) 较小 迭代器稳定性 rehash 时全失效 只有被删元素失效 自定义比较器 hash + == 只需 < 适用 "查得快" "要有序"
7. 第二梯队容器:deque / array / list
7.1 std::deque — 双端队列
内部: 分块数组。头尾增删 O(1),中间 O(n)。内存不如 vector 紧凑,但比 list 快。
1 2 3 4 5 6 7 8 9 #include <deque> deque<int > dq = {2 , 3 , 4 }; dq.push_front (1 ); dq.push_back (5 ); dq.pop_front (); dq.pop_back (); int a = dq[2 ];
1 2 3 4 使用场景: ✅ 需要头尾都增删 + 偶尔随机访问 → deque ✅ 实现 BFS 的队列底层(queue 默认用 deque) ❌ 大部分情况用 vector 就够了
7.2 std::array — 安全版 C 数组
编译期定长,零开销,不退化指针,有 size()
1 2 3 4 5 6 7 8 9 #include <array> array<int , 5> arr = {1 , 2 , 3 , 4 , 5 }; int x = arr[2 ]; int y = arr.at (2 ); size_t n = arr.size (); int * raw = arr.data ();
1 2 3 4 5 array vs C 数组 ✅ 有 size(),不退化指针 ✅ 可以按值传入/返回函数(C 数组不行) ✅ .at() 有边界检查 ❌ 大小必须编译期确定
7.3 std::list — 双向链表
大部分时候不该用的容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <list> list<int > l = {2 , 3 , 4 }; l.push_front (1 ); l.push_back (5 ); auto it = next (l.begin ());l.insert (it, 99 ); l.erase (it); l.sort (); l.unique ();
1 2 3 4 5 6 7 8 什么时候用 list? ✅ 元素很大(如 string of 1KB),且频繁在中间插入删除 ✅ 需要插入时其他迭代器绝对不失效 什么时候不用? ❌ 遍历比 vector 慢 10-50×(cache miss) ❌ 每个元素有 2 个指针开销(16 字节在 64 位系统) ❌ 不能随机访问
8. 容器适配器:stack / queue / priority_queue
它们不是独立的数据结构,而是给底层容器套了一层接口限制。
1 2 3 stack → 封掉头端,只留尾部 push/pop → LIFO queue → 封掉头端 push + 尾端 pop → FIFO priority_queue → 每次 pop 取最大值 → 堆
8.1 std::stack — 后进先出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stack> stack<int > st; st.push (1 ); st.push (2 ); st.push (3 ); int top = st.top (); st.pop (); while (!st.empty ()) { cout << st.top () << ' ' ; st.pop (); }
8.2 std::queue — 先进先出
1 2 3 4 5 6 7 8 9 10 11 12 #include <queue> queue<int > q; q.push (1 ); q.push (2 ); q.push (3 ); int front = q.front (); int back = q.back (); q.pop (); while (!q.empty ()) { cout << q.front () << ' ' ; q.pop (); }
8.3 std::priority_queue — 始终取最值
默认大顶堆(最大的在顶上) 。你的 Game AI 里 A* 的 open set 就用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <queue> priority_queue<int > pq; pq.push (3 ); pq.push (1 ); pq.push (5 ); pq.push (2 ); while (!pq.empty ()) { cout << pq.top () << ' ' ; pq.pop (); } priority_queue<int , vector<int >, greater<int >> min_pq; auto cmp = [](int a, int b) { return a > b; };priority_queue<int , vector<int >, decltype (cmp)> pq_custom (cmp); struct Node { int id, cost; };priority_queue<pair<int , int >, vector<pair<int ,int >>, greater<>> dijkstra_pq;
9. 全容器复杂度速查
9.1 容器操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 操作 vector string deque list array ──────────────────────────────────────────────────────────── [i] O(1) O(1) O(1) - O(1) at(i) O(1) O(1) O(1) - O(1) push_back O(1)† O(1)† O(1) O(1) - push_front - - O(1) O(1) - pop_back O(1) O(1) O(1) O(1) - pop_front - - O(1) O(1) - insert(mid) O(n) O(n) O(n) O(1) - erase(mid) O(n) O(n) O(n) O(1) - find O(n) O(n) O(n) O(n) O(n) front/back O(1) O(1) O(1) O(1) O(1) size/empty O(1) O(1) O(1) O(1) O(1) 遍历 O(n) O(n) O(n) O(n) O(n) † 均摊 O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 操作 u_map u_set map set ─────────────────────────────────────────────────── [ ] / at() O(1)* - O(log n) - find / count O(1)* O(1)* O(log n) O(log n) contains(C++20) O(1)* O(1)* O(log n) O(log n) insert O(1)* O(1)* O(log n) O(log n) erase(key) O(1)* O(1)* O(log n) O(log n) erase(it) O(1) O(1) O(1)†† O(1)†† lower_bound - - O(log n) O(log n) 遍历 O(n) O(n) O(n) O(n) * 平均,最坏 O(n) †† 均摊
9.2 适配器
1 2 3 4 5 操作 stack queue priority_queue ──────────────────────────────────────────────────── push O(1) O(1) O(log n) pop O(1) O(1) O(log n) top/front O(1) O(1) O(1)
9.3 空间开销
1 2 3 4 5 6 7 8 9 结构 每个元素额外开销 ──────────────────────────────────── vector / string 0(连续紧凑) array 0(栈上) deque 极少(分块控制块) list 2 个指针 (16B) forward_list 1 个指针 (8B) set / map 3 个指针 + 颜色 (≈24B per node) u_set / u_map 1 个指针 + 桶数组开销
10. 哈希底层原理:6 种构造 + 4 种冲突解决
这部分是理论深度。面试可能问,实际工程中 std::unordered_map 已经帮你做好了。
只在需要自实现 或极端优化 时才需要深入。
10.1 哈希函数的 6 种构造方法
① 直接定址法
1 H(key) = key 或 H(key) = a×key + b
key 即地址,零冲突。仅当 key 范围小且连续时可行。
1 2 int freq[26 ] = {0 };freq[c - 'a' ]++;
② 除留余数法(最常用)
1 H(key) = key % p p 取 ≤ 表长的最大素数
std::unordered_map 实际使用的就是这个(组合哈希后取模)。
1 2 3 53 , 97 , 193 , 389 , 769 , 1543 , 3079 , 6151 , 12289 , 24593 , 49157 , 98317 , ...
③ 数字分析法
取 key 分布最均匀的若干位作为哈希地址。
1 学号 20240101 → 前 4 位相同 → 丢弃,取后 4 位
适用:已知全部 key 的静态表(编译器的关键字表)。
④ 平方取中法
1 key=1234 → key²=1522756 → 取中间 3 位 → 227
平方使原 key 每一位都对中间位产生混合。
⑤ 折叠法
1 key=9876543210 分割为 987|654|321|0 → 移位折叠求和 → 1962
适用:超长 key(身份证号、ISBN)。
⑥ 随机数法
1 H(key) = random(key) // 确定性伪随机函数
几乎不用,被生产级哈希(SipHash / xxHash)取代。
1 2 3 4 5 6 7 8 方法 核心思路 冲突 适用场景 ───────────────────────────────────────────────────────── 直接定址法 key 即地址 零 key 范围小且连续 除留余数法 ⭐ key % 素数 靠素数 通用,最常用 数字分析法 取均匀位 少 静态 key 集合 平方取中法 平方后取中间位 较少 key 位数不多 折叠法 分组求和 中等 key 超长 随机数法 伪随机函数 靠质量 几乎不用
10.2 哈希冲突的 4 种解决方法
① 链地址法(Separate Chaining)⭐ C++ STL 标准做法
1 2 3 4 5 6 桶 0: [26] → [39] → [13] → nullptr 桶 1: [15] → nullptr 桶 2: nullptr 桶 3: [37] → nullptr 本质:vector<list<pair<K,V>>>
优点
缺点
实现简单
每个节点有指针开销
负载因子可 >1
缓存不友好(指针跳转)
删除简单
极端情况退化到 O(n)
② 开放定址法 — 三种探测策略
探测
公式
问题
线性探测
h + i
一次聚集(连续占用块越积越长)
平方探测
h + i²
二次聚集,需素数表长 + load<0.5
双哈希 ⭐
h₁ + i×h₂
理论最优,彻底消除聚集
1 2 3 4 size_t h1 = hash<K>{}(key);size_t h2 = 1 + (hash<K>{}(key) % (tableSize - 1 ));size_t idx = (h1 + i * h2) % tableSize;
③ 再哈希法
准备多个哈希函数,冲突时换一个重算。不单独使用,出现在 rehash 阶段(扩容后重新分配所有 key)。
④ 公共溢出区
基本表和溢出表分离。冲突全进溢出区(退化为顺序搜索)。仅适合冲突极少的场景。
1 2 3 4 5 6 7 8 方法 C++ STL 采用? 实际地位 ──────────────────────────────────────────────── 链地址法 ✅ 唯一标准做法 主流 开放定址·线性探测 ❌ 少用 开放定址·平方探测 ❌ 少用 开放定址·双哈希 ❌ Robin Hood / SwissTable 变体 再哈希法 ❌ 扩容时必用 公共溢出区 ❌ 几乎不用
11. 自实现哈希表
理解底层的最佳方式。面试手写首选链地址法 。
11.1 链地址法(80 行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 template <typename K, typename V>class SimpleHashMap { vector<list<pair<K, V>>> buckets; size_t num_elements = 0 ; float max_load = 0.75f ; size_t idx (const K& k) const { return hash<K>{}(k) % buckets.size (); } void rehash () { auto old = move (buckets); buckets.resize (old.size () * 2 ); num_elements = 0 ; for (auto & b : old) for (auto & [k, v] : b) buckets[idx (k)].emplace_back (k, move (v)), ++num_elements; } public : SimpleHashMap (size_t cap = 16 ) : buckets (cap) {} void put (const K& k, const V& v) { for (auto & [key, val] : buckets[idx (k)]) if (key == k) { val = v; return ; } buckets[idx (k)].emplace_back (k, v); if (++num_elements > buckets.size () * max_load) rehash (); } V* get (const K& k) { for (auto & [key, val] : buckets[idx (k)]) if (key == k) return &val; return nullptr ; } bool erase (const K& k) { auto & b = buckets[idx (k)]; for (auto it = b.begin (); it != b.end (); ++it) if (it->first == k) { b.erase (it); --num_elements; return true ; } return false ; } size_t size () const { return num_elements; } };
11.2 开放定址法(核心差异)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 template <typename K, typename V>class OpenAddrHashMap { enum State { EMPTY, OCCUPIED, DELETED }; struct Slot { State state = EMPTY; pair<K, V> kv; }; vector<Slot> table; size_t n = 0 ; size_t probe (const K& k) const { size_t h = hash<K>{}(k) % table.size (); while (table[h].state == OCCUPIED && table[h].kv.first != k) h = (h + 1 ) % table.size (); return h; } void rehash () { auto old = move (table); table.resize (old.size () * 2 ); n = 0 ; for (auto & s : old) if (s.state == OCCUPIED) put (s.kv.first, s.kv.second); } public : OpenAddrHashMap (size_t cap = 16 ) : table (cap) {} void put (const K& k, const V& v) { if (n * 1.0 / table.size () >= 0.7 ) rehash (); size_t h = probe (k); if (table[h].state != OCCUPIED) ++n; table[h] = {OCCUPIED, {k, v}}; } V* get (const K& k) { size_t h = hash<K>{}(k) % table.size (), start = h; do { if (table[h].state == EMPTY) return nullptr ; if (table[h].state == OCCUPIED && table[h].kv.first == k) return &table[h].kv.second; h = (h + 1 ) % table.size (); } while (h != start); return nullptr ; } bool erase (const K& k) { size_t h = hash<K>{}(k) % table.size (), start = h; do { if (table[h].state == EMPTY) return false ; if (table[h].state == OCCUPIED && table[h].kv.first == k) { table[h].state = DELETED; --n; return true ; } h = (h + 1 ) % table.size (); } while (h != start); return false ; } };
12. 面试高频考点
12.1 Two Sum(必考)
1 2 3 4 5 6 7 8 9 10 vector<int > twoSum (vector<int >& nums, int target) { unordered_map<int , int > seen; for (int i = 0 ; i < nums.size (); ++i) { int need = target - nums[i]; if (seen.contains (need)) return {seen[need], i}; seen[nums[i]] = i; } return {}; }
12.2 哈希 vs 数组:选哪个
1 2 3 4 5 6 7 int freq[26 ] = {0 };for (char c : s) freq[c - 'a' ]++;unordered_map<string, int > word_count; for (auto & w : words) word_count[w]++;
12.3 易错点速记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (m["key" ] == 0 ) { } if (m.find ("key" ) == m.end ()) for (auto it = m.begin (); it != m.end (); ) { if (bad (it)) it = m.erase (it); else ++it; } v.reserve (100 ); v[0 ] = 42 ; vector<bool > vb = {true }; auto b = vb[0 ];
总结:
90% 的情况:vector + unordered_map + string
需要排序:map / set
双端操作:deque
最值:priority_queue
哈希底层原理(§10-11)是理解 unordered_map 为什么 O(1) 的基础,但日常编码不需要自己实现。
选容器的第一原则:先确定访问模式,再选数据结构 。没有万能容器,只有最匹配的容器。