在学习 C++ STL 容器时,我们经常会接触到 vector、map、set 等容器。相比这些容器,unordered_map、unordered_set 这一类容器有一个非常重要的概念:bucket,也就是"桶"。
很多初学者第一次看到 bucket_count()、bucket_size()、load_factor()、rehash() 这些函数时会比较疑惑:
这些 bucket 到底是什么?
它们和哈希表有什么关系?
为什么 unordered_map 的查询效率通常是 O(1)?
本文就来系统介绍 C++ 中的 buckets。
一、bucket 是什么?
在 C++ 中,bucket 通常翻译为 桶。
在 STL 的哈希容器中,例如:
unordered_map
unordered_set
unordered_multimap
unordered_multiset
这些容器底层通常使用 哈希表 实现,而哈希表内部会把元素分布到若干个 bucket 中。
可以简单理解为:
unordered_map 内部不是把所有数据放在一个大数组里,
而是先准备很多个桶,
每个元素通过哈希函数计算后,被放入某一个桶中。
例如我们有一个 unordered_map<int, string>:
unordered_map<int, string> mp;
mp[1] = "apple";
mp[2] = "banana";
mp[10] = "orange";
mp[15] = "pear";
这些 key 会经过哈希函数计算,然后被放入不同的桶中。
大致结构可以想象成:
bucket 0:
bucket 1: (1, apple)
bucket 2: (2, banana)
bucket 3:
bucket 4:
bucket 5: (10, orange) -> (15, pear)
注意:上面只是示意图,真实分布取决于哈希函数和桶的数量。
二、bucket 和哈希表的关系
unordered_map 和 unordered_set 的核心思想是 哈希表。
哈希表的工作流程大致如下:
-
拿到一个 key;
-
使用哈希函数计算 key 的哈希值;
-
根据哈希值确定它应该放到哪个 bucket;
-
在对应的 bucket 中查找、插入或删除元素。
例如:
bucket_index = hash(key) % bucket_count;
其中:
hash(key)
表示 key 经过哈希函数得到的哈希值。
bucket_count
表示当前哈希表中桶的数量。
最终通过取模运算,确定元素应该进入哪一个 bucket。
三、为什么 unordered_map 查询效率高?
普通的 map 底层通常是红黑树,查询、插入、删除的时间复杂度是:
O(log n)
而 unordered_map 底层通常是哈希表,在哈希冲突较少的情况下,查询、插入、删除的平均时间复杂度是:
O(1)
原因就在于 bucket。
假设我们要查找 key 为 100 的元素。
对于 map 来说,它需要在树结构中一层一层比较。
而对于 unordered_map 来说,它会先通过哈希函数直接定位到某个 bucket,然后只在这个 bucket 中查找。
也就是说:
unordered_map 不需要从头到尾查找,
而是通过哈希值快速定位到目标桶。
这就是它平均效率很高的原因。
四、哈希冲突是什么?
虽然哈希表效率很高,但是有一个重要问题:哈希冲突。
所谓哈希冲突,就是多个不同的 key 被分配到了同一个 bucket 中。
例如:
key = 10 进入 bucket 3
key = 21 也进入 bucket 3
key = 37 也进入 bucket 3
这种情况就叫做哈希冲突。
示意图:
bucket 0:
bucket 1:
bucket 2:
bucket 3: 10 -> 21 -> 37
bucket 4:
当多个元素都在同一个 bucket 中时,查找效率就会下降。
如果一个 bucket 中元素过多,那么查找时就需要在这个 bucket 内部继续遍历。
所以,哈希表的性能不仅取决于元素数量,也取决于 bucket 的数量和元素分布情况。
五、C++ 中与 bucket 相关的常用函数
C++ 的 unordered_map 和 unordered_set 提供了一些和 bucket 相关的成员函数。
下面以 unordered_map 为例介绍。
1. bucket_count()
bucket_count() 用来返回当前容器中的 bucket 数量。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
mp[1] = "apple";
mp[2] = "banana";
mp[3] = "orange";
cout << "当前 bucket 数量:" << mp.bucket_count() << endl;
return 0;
}
输出结果可能是:
当前 bucket 数量:13
注意,不同编译器、不同 STL 实现中,bucket 的数量可能不同。
2. max_bucket_count()
max_bucket_count() 用来返回容器理论上最多可以拥有的 bucket 数量。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, int> mp;
cout << "最大 bucket 数量:" << mp.max_bucket_count() << endl;
return 0;
}
这个函数一般了解即可,实际开发中使用较少。
3. bucket_size(n)
bucket_size(n) 用来返回第 n 个 bucket 中有多少个元素。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
mp[1] = "apple";
mp[2] = "banana";
mp[10] = "orange";
mp[20] = "pear";
for (size_t i = 0; i < mp.bucket_count(); i++) {
cout << "bucket " << i << " 中的元素数量:"
<< mp.bucket_size(i) << endl;
}
return 0;
}
这个函数可以帮助我们观察哈希表中元素的分布情况。
如果很多元素都集中在少数几个 bucket 中,就说明哈希冲突比较严重。
4. bucket(key)
bucket(key) 用来返回某个 key 所在的 bucket 编号。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
mp[1] = "apple";
mp[2] = "banana";
mp[10] = "orange";
cout << "key = 1 所在的 bucket:" << mp.bucket(1) << endl;
cout << "key = 2 所在的 bucket:" << mp.bucket(2) << endl;
cout << "key = 10 所在的 bucket:" << mp.bucket(10) << endl;
return 0;
}
这个函数可以用来查看某个元素被哈希到了哪个桶中。
5. load_factor()
load_factor() 表示当前哈希表的负载因子。
计算公式是:
load_factor = 元素数量 / bucket 数量
也就是:
load_factor = size() / bucket_count()
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
mp[1] = "apple";
mp[2] = "banana";
mp[3] = "orange";
cout << "元素数量:" << mp.size() << endl;
cout << "bucket 数量:" << mp.bucket_count() << endl;
cout << "负载因子:" << mp.load_factor() << endl;
return 0;
}
如果负载因子过高,说明平均每个 bucket 中存放的元素变多,哈希冲突可能会增加。
6. max_load_factor()
max_load_factor() 用来查看或设置最大负载因子。
当当前负载因子超过最大负载因子时,容器可能会自动扩容,也就是重新分配 bucket。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
cout << "默认最大负载因子:" << mp.max_load_factor() << endl;
mp.max_load_factor(0.7);
cout << "修改后的最大负载因子:" << mp.max_load_factor() << endl;
return 0;
}
最大负载因子越小,哈希冲突通常越少,但需要更多内存。
最大负载因子越大,占用内存可能更少,但哈希冲突可能更多。
7. rehash(n)
rehash(n) 用来设置 bucket 的数量至少为 n。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
cout << "初始 bucket 数量:" << mp.bucket_count() << endl;
mp.rehash(100);
cout << "rehash 后 bucket 数量:" << mp.bucket_count() << endl;
return 0;
}
需要注意的是:
mp.rehash(100);
并不一定表示 bucket 数量最终刚好是 100,只能保证 bucket 数量至少满足容器的要求。
不同编译器可能会选择不同的实际 bucket 数量。
8. reserve(n)
reserve(n) 用来提前为 n 个元素分配合适的 bucket 空间。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
mp.reserve(1000);
cout << "bucket 数量:" << mp.bucket_count() << endl;
return 0;
}
如果我们提前知道要插入大量元素,使用 reserve() 可以减少扩容次数,提高性能。
例如:
unordered_map<int, int> mp;
mp.reserve(100000);
for (int i = 0; i < 100000; i++) {
mp[i] = i;
}
这样比直接插入更加稳定,因为可以减少频繁 rehash 的开销。
六、遍历 bucket 中的元素
unordered_map 还支持按 bucket 遍历元素。
可以使用:
begin(bucket_index)
end(bucket_index)
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
mp[1] = "apple";
mp[2] = "banana";
mp[10] = "orange";
mp[20] = "pear";
mp[30] = "grape";
for (size_t i = 0; i < mp.bucket_count(); i++) {
cout << "bucket " << i << ": ";
for (auto it = mp.begin(i); it != mp.end(i); it++) {
cout << "(" << it->first << ", " << it->second << ") ";
}
cout << endl;
}
return 0;
}
这个代码会打印每个 bucket 中的元素。
通过这个方法,可以更直观地观察哈希表内部的元素分布。
七、完整示例:观察 unordered_map 的 bucket 分布
下面写一个完整程序,观察 unordered_map 的 bucket 变化。
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> mp;
mp[1] = "apple";
mp[2] = "banana";
mp[3] = "orange";
mp[10] = "pear";
mp[20] = "grape";
mp[30] = "watermelon";
cout << "元素数量 size: " << mp.size() << endl;
cout << "bucket 数量 bucket_count: " << mp.bucket_count() << endl;
cout << "负载因子 load_factor: " << mp.load_factor() << endl;
cout << "最大负载因子 max_load_factor: " << mp.max_load_factor() << endl;
cout << endl;
cout << "每个 key 所在的 bucket:" << endl;
for (auto &p : mp) {
cout << "key = " << p.first
<< ", value = " << p.second
<< ", bucket = " << mp.bucket(p.first)
<< endl;
}
cout << endl;
cout << "每个 bucket 中的元素:" << endl;
for (size_t i = 0; i < mp.bucket_count(); i++) {
cout << "bucket " << i << ": ";
for (auto it = mp.begin(i); it != mp.end(i); it++) {
cout << "(" << it->first << ", " << it->second << ") ";
}
cout << endl;
}
return 0;
}
这段代码可以帮助我们理解:
1. unordered_map 内部有多少个 bucket;
2. 每个元素被放到了哪个 bucket;
3. 每个 bucket 中有多少元素;
4. 哈希表负载因子的变化。
八、bucket 过多或过少会有什么影响?
bucket 的数量会直接影响哈希表的性能和内存占用。
1. bucket 太少
如果 bucket 数量太少,很多元素就会被放入同一个 bucket 中。
这样会导致哈希冲突增多。
例如:
bucket 0: 1 -> 5 -> 10 -> 20 -> 31 -> 42
bucket 1:
bucket 2:
这种情况下,虽然使用的是 unordered_map,但是查询时可能需要在一个很长的链表或结构中查找,效率会下降。
极端情况下,时间复杂度可能退化到:
O(n)
2. bucket 太多
如果 bucket 数量太多,虽然哈希冲突会减少,但是会占用更多内存。
例如只有 10 个元素,却分配了 10000 个 bucket,这就会造成空间浪费。
所以 bucket 数量不是越多越好,而是要根据元素数量合理分配。
九、rehash 和 reserve 的区别
很多人容易混淆 rehash() 和 reserve()。
它们都可以影响 bucket 数量,但含义不同。
1. rehash(n)
rehash(n) 的意思是:
设置 bucket 的数量至少为 n。
例如:
mp.rehash(100);
表示希望哈希表至少有 100 个 bucket。
2. reserve(n)
reserve(n) 的意思是:
为至少能容纳 n 个元素提前分配空间。
例如:
mp.reserve(100);
表示希望容器能够比较高效地存放 100 个元素。
它会根据当前的最大负载因子自动计算需要多少 bucket。
可以简单理解为:
rehash 管的是 bucket 数量;
reserve 管的是元素容量。
实际开发中,如果提前知道要插入多少个元素,通常更推荐使用:
reserve()
例如:
unordered_map<int, int> mp;
mp.reserve(100000);
这样代码含义更明确。
十、bucket 和 unordered_set
unordered_set 中也有 bucket 的概念。
示例:
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
unordered_set<int> s;
s.insert(10);
s.insert(20);
s.insert(30);
s.insert(40);
cout << "元素数量:" << s.size() << endl;
cout << "bucket 数量:" << s.bucket_count() << endl;
for (int x : s) {
cout << "元素 " << x << " 在 bucket "
<< s.bucket(x) << endl;
}
return 0;
}
unordered_set 只存储 key,而 unordered_map 存储的是 key-value 键值对。
但是它们底层都依赖哈希表和 bucket。
十一、bucket 和桶排序中的 bucket
除了 STL 哈希容器中的 bucket,算法中也有一个经典概念叫 桶排序 ,英文是 Bucket Sort。
桶排序的思想是:
把数据按照一定规则分到不同的桶中,
每个桶内部再进行排序,
最后按桶的顺序把结果合并起来。
例如要排序下面这些数:
29, 25, 3, 49, 9, 37, 21, 43
可以按照范围分桶:
0~9: 3, 9
10~19:
20~29: 29, 25, 21
30~39: 37
40~49: 49, 43
然后每个桶内部排序:
0~9: 3, 9
20~29: 21, 25, 29
30~39: 37
40~49: 43, 49
最后合并:
3, 9, 21, 25, 29, 37, 43, 49
桶排序适合数据分布比较均匀的情况。
十二、桶排序 C++ 示例
下面给出一个简单的桶排序代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void bucketSort(vector<int>& nums) {
if (nums.empty()) return;
int maxVal = *max_element(nums.begin(), nums.end());
int minVal = *min_element(nums.begin(), nums.end());
int bucketCount = 5;
vector<vector<int>> buckets(bucketCount);
int range = maxVal - minVal + 1;
for (int num : nums) {
int index = (num - minVal) * bucketCount / range;
buckets[index].push_back(num);
}
nums.clear();
for (int i = 0; i < bucketCount; i++) {
sort(buckets[i].begin(), buckets[i].end());
for (int num : buckets[i]) {
nums.push_back(num);
}
}
}
int main() {
vector<int> nums = {29, 25, 3, 49, 9, 37, 21, 43};
bucketSort(nums);
for (int x : nums) {
cout << x << " ";
}
return 0;
}
输出结果:
3 9 21 25 29 37 43 49
这里的 bucket 是算法意义上的"桶",和 unordered_map 中的 bucket 思想类似,都是将数据分组管理。
不过二者的使用场景不同:
unordered_map 中的 bucket:用于哈希表存储和查找;
桶排序中的 bucket:用于排序算法中的分组。
十三、实际开发中需要关心 bucket 吗?
大多数情况下,我们使用 unordered_map 或 unordered_set 时,不需要手动管理 bucket。
例如:
unordered_map<string, int> mp;
mp["apple"] = 10;
mp["banana"] = 20;
mp["orange"] = 30;
正常写代码即可,STL 会自动帮我们管理 bucket。
但是在以下情况中,了解 bucket 很有用:
1. 需要优化 unordered_map 的性能;
2. 需要插入大量数据;
3. 需要分析哈希冲突;
4. 需要理解 unordered_map 的底层原理;
5. 面试中被问到哈希表实现原理。
如果提前知道数据量很大,可以使用:
unordered_map<int, int> mp;
mp.reserve(100000);
这样可以减少扩容和 rehash,提高插入效率。
十四、面试中如何解释 bucket?
如果面试官问:
unordered_map 中的 bucket 是什么?
可以这样回答:
unordered_map 底层通常使用哈希表实现。哈希表内部会维护若干个 bucket,也就是桶。
当插入一个 key-value 时,会先通过哈希函数计算 key 的哈希值,然后根据哈希值映射到某个 bucket 中。
如果多个 key 被映射到同一个 bucket,就会产生哈希冲突。
bucket 的数量和负载因子会影响 unordered_map 的性能。
当元素数量增加导致负载因子过高时,容器可能会进行 rehash,增加 bucket 数量,并重新分布元素。
这个回答基本可以覆盖核心点。
十五、常见函数总结
| 函数 | 作用 |
|---|---|
bucket_count() |
返回当前 bucket 数量 |
max_bucket_count() |
返回最大 bucket 数量 |
bucket_size(n) |
返回第 n 个 bucket 中的元素数量 |
bucket(key) |
返回 key 所在的 bucket 编号 |
load_factor() |
返回当前负载因子 |
max_load_factor() |
获取或设置最大负载因子 |
rehash(n) |
设置 bucket 数量至少为 n |
reserve(n) |
为容纳 n 个元素提前分配空间 |
begin(n) |
返回第 n 个 bucket 的起始迭代器 |
end(n) |
返回第 n 个 bucket 的结束迭代器 |
十六、总结
bucket 是理解 C++ 哈希容器的重要概念。
在 unordered_map、unordered_set 等容器中,bucket 用来存放经过哈希函数映射后的元素。每个元素会根据 key 的哈希值进入某个 bucket。
合理的 bucket 数量可以减少哈希冲突,提高查询、插入和删除效率。