哈希表理论基础
哈希表
哈希表(Hash table)是根据关键码的值二直接进行访问的数据结构,其一般用来快速判断一个元素是否出现在集合里。
数组是一张哈希表
哈希函数
哈希函数是一种将任意大小的输入数据映射到固定大小的输出(通常称为哈希值或哈希码)的算法。
哈希碰撞
哈希碰撞是指两个不同的输入通过同一个哈希函数计算得到了相同的哈希值。如图所示,小王和小李都映射到索引下标 1 的位置,这种现象就是哈希碰撞。
哈希碰撞一般有两种解决方法,即拉链法和线性探测法。
拉链法
将发生冲突的元素储存在链表中
拉链法要选择适当的哈希表大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间
线性探测法
线性探测法需要保证 tableSize 大于 dataSize,依靠哈希表中的空位来解决碰撞问题。
常见的三种哈希结构
常见的三种哈希结构:
- 数组
- set(集合)
- map(映射)
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
std::unordered_set 的底层实现为哈希表,std::set 和 std::multiset 的底层实现为红黑树,红黑树是一种平衡二叉搜索树,其 key 值是有序的,但 key 不可以修改,改动 key 值会导致整棵树的错乱,只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | 是 | 否 | 否 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::multimap | 红黑树 | 是 | 是 | 否 | O ( log n ) O(\log n) O(logn) | O ( log n ) O(\log n) O(logn) |
std::unordered_map | 哈希表 | 否 | 否 | 否 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
std::map 和 std::multimap 的底层实现为红黑树,其 key 是有序的,而 std::unordered_map 的底层实现为哈希表,其 key 无序。
在哈希结构的选取上,当题目中数据的大小较小且固定时,可以选用数组求解;当数据较大时选用 set 求解;当题目中存在 key 和 value 的对应关系时选用 map 求解。
Leetcode 242-有效的字母异位词
题目描述:
https://leetcode.cn/problems/valid-anagram/description/
解题思路
由于 26 个字母的 ASCII 码是连续的,可以通过减’a’将 26 个字母映射为一个数组
class Solution {
public:
bool isAnagram(string s, string t) {
int hash[26] = {0};
for (int i = 0; i < s.size(); i++) {
hash[s[i] - 'a'] ++;//26个小写字母的ASCII的值是连续的,通过-'a'的操作可以求出其在以a为首项的数组的位置
}
for (int i = 0; i < t.size(); i++) {
hash[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (hash[i] != 0) {
return false;
}
}
return true;
}
};
Leetcode 349-两个数组的交集
题目描述:
https://leetcode.cn/problems/intersection-of-two-arrays/description/
解题思路
采用 set 解决这道题目,set 主要分为 set,multiset 和 unordered_set,其中前两种的底层实现是红黑树,unordered_set 的底层实现是哈希表,unordered_set 相比于另外两种 set 在查找上具有更低的复杂度。
同时,unordered_set 本身在储存数据时会进行去重,直接实现了题目中要求的去重。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result;
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int i : nums2) {//遍历nums2容器中的所有元素
if (nums_set.find(i) != nums_set.end()) {//如果在nums2容器中存在与nums1相同的数值
result.insert(i);//将该数值添加到result中
}
}
return vector<int>(result.begin(), result.end());
}
};
Leetcode 202-快乐数
题目描述:
https://leetcode.cn/problems/happy-number/description/
解题思路
如果进入了无限循环,即在求和过程中 sum 重复出现,则可以判断该数不是快乐数。判断 sum 是否重复出现可以使用 unordered_set。
class Solution {
public:
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while (1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
if (set.find(sum) != set.end()) {
return false;
}
else {
set.insert(sum);
}
n = sum;
}
}
};
Leetcode 1-两数之和
题目描述:
https://leetcode.cn/problems/two-sum/description/
解题思路
将遍历过的元素加到集合中,当遍历到新的元素时,判断与其相加等于目标值的元素是否在前面出现过。
在这里我们需要使用哈希结构储存之前遍历过的元素,选择 map 是因为在这道题目中需要返回元素的值以及元素在数组的 index。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
int val = target - nums[i];
if (map.find(val) != map.end()) {
return { map.find(val)->second,i };
}
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};