目录
一、哈希表是什么?
1.1 基本概念
1.2 哈希表的工作原理
二、哈希表的使用方法
2.1 C++ 标准库中的哈希表
示例:std::unordered_map 的基本用法
2.2 自定义哈希函数
示例:自定义哈希函数
三、什么时候使用哈希表?
3.1 适用场景
3.2 不适用场景
四、哈希表的优缺点
4.1 优点
4.2 缺点
五、哈希表的实现细节
5.1 冲突解决策略
5.2 动态扩容
示例:动态扩容的实现
六、总结
一、哈希表是什么?
1.1 基本概念
哈希表是一种基于键值对(Key-Value Pair)存储的数据结构,通过哈希函数将键映射到表中的某个位置,从而实现快速访问。哈希表的核心组成部分包括:
-
键(Key):用于唯一标识数据的值。
-
值(Value):与键关联的数据。
-
哈希函数(Hash Function):将键映射到哈希表索引的函数。
-
桶(Bucket):存储键值对的容器,通常是一个数组或链表。
1.2 哈希表的工作原理
-
插入:
-
计算键的哈希值:
hash_value = hash(key)
。 -
根据哈希值确定存储位置:
index = hash_value % table_size
。 -
将键值对存储到对应的桶中。
-
-
查找:
-
计算键的哈希值。
-
根据哈希值定位桶。
-
在桶中查找键对应的值。
-
-
删除:
-
计算键的哈希值。
-
根据哈希值定位桶。
-
在桶中删除键值对。
-
二、哈希表的使用方法
2.1 C++ 标准库中的哈希表
C++ 标准库提供了 std::unordered_map
和 std::unordered_set
,分别用于存储键值对和键集合。
示例:std::unordered_map
的基本用法
#include <iostream>
#include <unordered_map>
#include <string>
int main() {
// 创建一个哈希表
std::unordered_map<std::string, int> map;
// 插入键值对
map["apple"] = 1;
map["banana"] = 2;
map["cherry"] = 3;
// 查找键
if (map.find("banana") != map.end()) {
std::cout << "banana found: " << map["banana"] << std::endl;
}
// 删除键
map.erase("cherry");
// 遍历哈希表
for (const auto& pair : map) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
输出:
banana found: 2
apple: 1
banana: 2
2.2 自定义哈希函数
C++ 标准库的 std::hash
支持基本类型和字符串,但对于自定义类型,需要特化 std::hash
。
示例:自定义哈希函数
#include <iostream>
#include <unordered_map>
#include <string>
struct MyKey {
int id;
std::string name;
bool operator==(const MyKey& other) const {
return id == other.id && name == other.name;
}
};
namespace std {
template<>
struct hash<MyKey> {
size_t operator()(const MyKey& k) const {
return hash<int>()(k.id) ^ (hash<string>()(k.name) << 1);
}
};
}
int main() {
std::unordered_map<MyKey, int> map;
map[{1, "apple"}] = 10;
map[{2, "banana"}] = 20;
MyKey key = {1, "apple"};
if (map.find(key) != map.end()) {
std::cout << "Found: " << map[key] << std::endl;
}
return 0;
}
输出:
Found: 10
三、什么时候使用哈希表?
3.1 适用场景
-
快速查找:
-
当需要频繁查找数据时,哈希表可以提供接近常数时间复杂度的性能。
-
示例:字典、电话簿、缓存系统。
-
-
去重:
-
哈希表可以高效地检测和删除重复元素。
-
示例:统计唯一单词数量。
-
-
关联数据存储:
-
当需要存储键值对时,哈希表是一种理想的数据结构。
-
示例:数据库索引、配置文件解析。
-
3.2 不适用场景
-
范围查询:
-
哈希表不支持范围查询(如查找大于某个值的所有键)。
-
适用数据结构:平衡二叉搜索树(如
std::map
)。
-
-
内存受限环境:
-
哈希表需要额外的内存存储哈希值和桶结构。
-
在内存受限的环境中,可能需要选择更紧凑的数据结构。
-
四、哈希表的优缺点
4.1 优点
-
高效的操作:
-
插入、查找、删除的平均时间复杂度为 O(1)O(1)。
-
-
灵活性:
-
支持任意类型的键和值(需提供哈希函数)。
-
-
易于实现:
-
C++ 标准库提供了现成的实现(
std::unordered_map
)。
-
4.2 缺点
-
冲突问题:
-
不同的键可能映射到相同的哈希值,导致性能下降。
-
-
内存开销:
-
需要额外的内存存储哈希值和桶结构。
-
-
无序性:
-
哈希表中的元素是无序的(
std::unordered_map
)。
-
五、哈希表的实现细节
5.1 冲突解决策略
-
链地址法(Separate Chaining):
-
每个桶存储一个链表,冲突的元素被添加到链表中。
-
优点:简单易实现,适合高装载因子。
-
缺点:链表过长时性能下降。
-
-
开放寻址法(Open Addressing):
-
所有元素存储在哈希表的数组中,冲突时通过探测函数寻找下一个空闲位置。
-
优点:缓存友好,适合低装载因子。
-
缺点:删除操作复杂,容易产生聚集现象。
-
5.2 动态扩容
当哈希表的装载因子超过阈值时,会触发扩容操作:
-
创建一个更大的桶数组。
-
重新计算所有键的哈希值并插入新数组。
-
释放旧数组。
示例:动态扩容的实现
template<typename K, typename V>
class HashTable {
private:
std::vector<std::list<std::pair<K, V>>> buckets;
size_t bucket_count;
size_t element_count;
double max_load_factor = 0.75;
size_t hash(const K& key) const {
return std::hash<K>{}(key) % bucket_count;
}
void rehash() {
size_t new_bucket_count = bucket_count * 2;
std::vector<std::list<std::pair<K, V>>> new_buckets(new_bucket_count);
for (const auto& chain : buckets) {
for (const auto& pair : chain) {
size_t new_index = std::hash<K>{}(pair.first) % new_bucket_count;
new_buckets[new_index].push_back(pair);
}
}
buckets = std::move(new_buckets);
bucket_count = new_bucket_count;
}
public:
HashTable(size_t count = 10) : bucket_count(count), element_count(0) {
buckets.resize(bucket_count);
}
void insert(const K& key, const V& value) {
if ((double)element_count / bucket_count > max_load_factor) {
rehash();
}
size_t index = hash(key);
auto& chain = buckets[index];
chain.emplace_back(key, value);
element_count++;
}
};
六、总结
哈希表是一种高效的数据结构,适用于需要快速查找、插入和删除的场景。C++ 标准库提供了 std::unordered_map
和 std::unordered_set
,可以方便地使用哈希表。在实际开发中,需要根据具体需求选择合适的哈希函数和冲突解决策略,并注意哈希表的装载因子和内存开销。通过深入理解哈希表的原理和实现细节,可以更好地利用其优势解决实际问题。