1. 什么是哈希表?
哈希表(Hash Table)是一种高效的数据结构,用于存储键值对(Key-Value Pairs)。它通过哈希函数(Hash Function)将键(Key)映射到一个固定大小的数组(称为散列表)中的某个位置,从而实现快速的插入、查找和删除操作。
哈希表的核心思想是通过哈希函数将键转换为数组的索引,从而直接访问对应的值。理想情况下,哈希表的插入、查找和删除操作的时间复杂度都是 O(1)。
2. 哈希表的核心概念
2.1 哈希函数(Hash Function)
哈希函数是哈希表的核心,它负责将任意大小的键映射到一个固定范围的整数(通常是数组的索引)。一个好的哈希函数应满足以下条件:
- 均匀分布:哈希值应尽可能均匀地分布在数组中,以减少冲突。
- 高效计算:哈希函数的计算应尽可能快。
- 一致性:相同的键应始终映射到相同的哈希值。
2.2 散列表(Hash Table)
散列表是一个数组,用于存储键值对。哈希函数将键映射到数组的某个索引,然后将值存储在该索引对应的位置。
2.3 冲突(Collision)
由于哈希函数的输出范围有限,而键的范围可能很大,因此不同的键可能会映射到相同的索引,这种现象称为冲突。常见的冲突解决方法包括:
- 链地址法(Chaining):将冲突的键值对存储在同一个索引位置的链表中。
- 开放地址法(Open Addressing):通过探测方法(如线性探测、二次探测)在散列表中寻找下一个可用的位置。
3. 哈希表的实现
下面我们使用 链地址法 来实现一个简单的哈希表,并用 C++ 代码演示其操作。
3.1 C++ 实现
#include <iostream>
#include <list>
#include <vector>
#include <utility> // for std::pairclass HashTable {
private:static const int TABLE_SIZE = 10; // 散列表的大小std::vector<std::list<std::pair<int, std::string>>> table; // 使用链表解决冲突// 哈希函数int hashFunction(int key) {return key % TABLE_SIZE;}public:HashTable() : table(TABLE_SIZE) {}// 插入键值对void insert(int key, const std::string& value) {int index = hashFunction(key);for (auto& pair : table[index]) {if (pair.first == key) {pair.second = value; // 如果键已存在,更新值return;}}table[index].emplace_back(key, value); // 否则插入新的键值对}// 查找键对应的值std::string search(int key) {int index = hashFunction(key);for (const auto& pair : table[index]) {if (pair.first == key) {return pair.second; // 找到键,返回对应的值}}return "Not Found"; // 未找到键}// 删除键值对void remove(int key) {int index = hashFunction(key);auto& chain = table[index];for (auto it = chain.begin(); it != chain.end(); ++it) {if (it->first == key) {chain.erase(it); // 删除键值对return;}}}// 打印散列表void printTable() const {for (int i = 0; i < TABLE_SIZE; ++i) {std::cout << "Bucket " << i << ": ";for (const auto& pair : table[i]) {std::cout << "(" << pair.first << ", " << pair.second << ") ";}std::cout << std::endl;}}
};int main() {HashTable hashTable;// 插入键值对hashTable.insert(1, "Alice");hashTable.insert(2, "Bob");hashTable.insert(11, "Charlie"); // 11 和 1 会发生冲突hashTable.insert(12, "David"); // 12 和 2 会发生冲突// 打印散列表std::cout << "Hash Table Contents:" << std::endl;hashTable.printTable();// 查找键std::cout << "Search for key 11: " << hashTable.search(11) << std::endl;std::cout << "Search for key 3: " << hashTable.search(3) << std::endl;// 删除键hashTable.remove(11);std::cout << "After removing key 11:" << std::endl;hashTable.printTable();return 0;
}
运行结果:
Hash Table Contents:
Bucket 0:
Bucket 1: (1, Alice) (11, Charlie)
Bucket 2: (2, Bob) (12, David)
Bucket 3:
Bucket 4:
Bucket 5:
Bucket 6:
Bucket 7:
Bucket 8:
Bucket 9:
Search for key 11: Charlie
Search for key 3: Not Found
After removing key 11:
Bucket 0:
Bucket 1: (1, Alice)
Bucket 2: (2, Bob) (12, David)
Bucket 3:
Bucket 4:
Bucket 5:
Bucket 6:
Bucket 7:
Bucket 8:
Bucket 9:
3.2 代码解析
-
哈希函数:
- 使用简单的取模运算
key % TABLE_SIZE
将键映射到散列表的索引。
- 使用简单的取模运算
-
冲突解决:
- 使用链地址法,每个索引位置存储一个链表,链表中存放所有映射到该索引的键值对。
-
插入操作:
- 计算键的哈希值,找到对应的索引。
- 如果键已存在,更新其值;否则将新的键值对插入链表。
-
查找操作:
- 计算键的哈希值,遍历对应索引位置的链表,查找键是否存在。
-
删除操作:
- 计算键的哈希值,遍历对应索引位置的链表,删除匹配的键值对。
-
打印散列表:
- 遍历散列表,打印每个索引位置的链表内容。
4. 哈希表的性能分析
-
时间复杂度:
- 插入、查找和删除的平均时间复杂度为 O(1)。
- 在最坏情况下(所有键都映射到同一个索引),时间复杂度退化为 O(n)。
-
空间复杂度:
- 空间复杂度为 O(n),其中
n
是键值对的数量。
- 空间复杂度为 O(n),其中
5. 哈希表的应用场景
- 字典:存储键值对,支持快速查找。
- 缓存:如 LRU 缓存,利用哈希表实现快速访问。
- 数据库索引:哈希表可用于实现数据库的索引结构。
- 去重:利用哈希表快速判断元素是否已存在。
6. 总结
哈希表是一种高效的数据结构,通过哈希函数将键映射到数组索引,从而实现快速的插入、查找和删除操作。链地址法是解决冲突的常见方法之一,适合处理哈希冲突的情况。
7.练习
ACWing模拟散列表
这里我们展示一种不使用STL的链表写法。使用STL可以使用map
,unordered_map
等快速解决,或者像上文中提到的vector
模拟链表。这里我们使用的是一种链式结构记录。数据结构书本上链表需要不断malloc
,在算法比赛当中是不合适的,因为会消耗大量的时间。这种链式结构的原理如下:
1. 插入操作 (insert
函数)
- 计算哈希值:通过
(num % mod + mod) % mod
计算键num
的哈希值pos
。 - 更新
head
:head
自增,表示新节点的索引。 - 链式插入:
- 将新节点的
nxt
指向当前h[pos]
(即链表的头部)。 - 更新
h[pos]
为head
,表示新节点成为链表的头部。 - 将
e[head]
赋值为num
,存储键值。
- 将新节点的
- 结束:插入操作完成。
假设现在状态如此,我们来模拟插入新来的一个编号为8的节点的过程。
- 首先计算8号节点的位置,假设经过哈希之后,需要放在5号桶。
- 接着,根据代码的操作,先将8号节点的nxt[8] 指向当前5号桶的头部节点7,并更新头部为5号桶头部节点为8号节点,再把值存入e数组的一个空位置。
- 完成操作后,链表的形式如下图所示
2. 查询操作 (query
函数)
- 计算哈希值:通过
(num % mod + mod) % mod
计算键num
的哈希值pos
。 - 初始化遍历:从
h[pos]
开始遍历链表。 - 遍历链表:
- 如果当前节点的值
e[i]
等于num
,返回true
。 - 否则,移动到下一个节点
i = nxt[i]
。
- 如果当前节点的值
- 遍历结束:如果遍历完链表仍未找到
num
,返回false
。
代码逻辑总结
h[pos]
:存储哈希值为pos
的链表的头节点索引。nxt[i]
:存储索引为i
的节点的下一个节点索引。e[i]
:存储索引为i
的节点的值。head
:表示当前可用的节点索引,每次插入时自增。
通过链式结构,哈希表可以高效地处理冲突,并支持动态插入和查询操作。
具体代码实现如下
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 1e5 + 7;
const int mod = 1e5 + 7;
int h[maxn], e[maxn], nxt[maxn], head = 0;
void insert(int num){int pos = (num % mod + mod) % mod;nxt[++head] = h[pos];h[pos] = head;e[head] = num;
}
bool query(int num){int pos = (num % mod + mod) % mod;for(int i = h[pos];i!=-1;i=nxt[i]){if(e[i] == num)return true;}return false;
}
int main() {ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);memset(h, -1, sizeof(h));int n;cin>>n;for(int i=1;i<=n;i++){string op;int num;//scanf("%s %d", op, &num);cin>>op>>num;if(op == "I"){if(!query(num))insert(num);}else{if(query(num))printf("Yes\n");elseprintf("No\n");}}return 0;
}