1.简介
哈希表(Hash Table)是一种常用的数据结构,用于实现键值对之间的映射关系。它基于哈希函数(Hash Function)来快速定位和访问数据,具有高效的查找、插入和删除操作。
以下是哈希表的理论基础:
-
哈希函数:哈希表的核心在于哈希函数。哈希函数接受一个键(key)作为输入,并将其映射到一个固定大小的整数值,该值称为哈希码(hash code)。好的哈希函数应当具有以下特点:
- 易于计算:哈希函数应当能够快速计算出哈希码。
- 均匀分布:哈希函数应当使不同的键均匀地映射到哈希表中的不同位置,以减少冲突。
- 最小冲突:尽可能避免不同的键映射到相同的哈希码,从而减少冲突。
-
解决冲突:由于哈希函数的限制,不同的键可能映射到相同的哈希码,导致冲突。解决冲突的常见方法包括:
- 链地址法(Chaining):将冲突的键值对存储在同一个位置上,并使用链表、树等数据结构来处理碰撞。
- 开放寻址法(Open Addressing):在发生冲突时,通过探测序列来寻找下一个可用的位置。
-
时间复杂度:哈希表的查找、插入和删除操作的平均时间复杂度为 O(1),即常数时间复杂度。但在最坏情况下,时间复杂度可能会达到 O(n),取决于哈希函数的质量和解决冲突的方法。
-
空间利用率:哈希表可以提供高效的空间利用率,因为它根据哈希码直接定位数据的存储位置,而不需要像数组那样按顺序存储。
2.例题
两数之和
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6 输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6 输出:[0,1]
- 暴力枚举
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int n = nums.size();
for(int i = 0;i < n;i++){
for(int j = i + 1;j < n;j++){
if(nums[i] + nums[j] == target){
return {i,j};
}
}
}
return {};
}
};
- 哈希表
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> hashTable;
for(int i = 0;i < nums.size();i++){
auto it = hashTable.find(target - nums[i]);
if(it != hashTable.end()){
return {it->second,i};
}
hashTable[nums[i]] = i;
}
return {};
}
};
最长连续序列
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1] 输出:9
要求时间复杂度为0(n)
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> hash;
for(auto num:nums){
hash.insert(num);
}
int res = 0;
for(auto num:hash){
if(!hash.count(num - 1)){
int y = num;
while(hash.count(y + 1))
y++;
res = max(res,y-num+1);
}
}
return res;
}
};
3.总结
- 算法场景
一般哈希表是用来快速判断一个元素是否出现集合中。
哈希表比起暴力枚举,牺牲了空间,换取了时间。
- 哈希函数
哈希函数 hashFunction = hashCode(类型) % tableSize
hashCode(类型)是针对特定类型的键计算得到的哈希码,tableSize表示哈希表的大小,%是取模运算符。这个公式的作用是将哈希码对哈希表的大小取模,以确定键应该放置在哈希表的哪个位置。取模运算有助于确保哈希码分布均匀,避免出现哈希冲突(多个键映射到同一个位置)。
- 哈希碰撞
即使哈希函数运行的再均匀,当键值大于哈希表大小时,仍然会映射到哈希表同一个索引位置。
解决哈希碰撞:拉链法、线性探测法。
拉链法:插入时,如果发生碰撞,新的键值对会被插入到对应桶的链表中。查找时,需要遍历链表来查找对应的键值对。拉链法相对简单且容易实现,适用于大多数情况下哈希碰撞不频繁的场景。
线性探测法:当发生哈希碰撞时,算法会线性地探测下一个可用的空槽,如果目标槽已经被占用,则按照一定的探测序列向后查找空槽,直到找到一个空槽或者遍历完整个哈希表。线性探测法可能会导致聚集(clustering)问题,即连续的槽被占用,影响了查找的性能。为了解决这个问题,可以采用二次探测法、双重散列等方法。
- 数据结构
哈希表常见的三种数据结构:数组、set(集合)、map(映射)。
集合 | 底层 | 是否有序 | 是否重复 | 能否更改数值 | 查询效率 | 增删效率 |
set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
multiset | 红黑树 | 有序 | 是 | 否 | O(log n) | O(log n) |
unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
映射 | 底层 | 是否有序 | 是否重复 | 能否更改数值 | 查询效率 | 增删效率 |
map | 红黑树 | 有序 | key否 | key否 | O(log n) | O(log n) |
multimap | 红黑树 | 有序 | key是 | key否 | O(log n) | O(log n) |
unordered_map | 哈希表 | 无序 | key否 | key否 | O(1) | O(1) |