(一)基础理论
(1)哈希表用来快速判断一个元素是否出现集合里。
(2)哈希函数,把某一元素直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这个元素是否出现在表中。
(3)**哈希碰撞:**不同元素被映射到同一数组位置
解决方法
-
拉链法:发生冲突的元素都被存储在链表中。
-
线性探测法:一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
(4)常见的三种哈希结构:
-
数组
-
set (集合)
-
map(映射)
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
(二)有效的字母异位
abbc bbac 相同的字母组成
a-z 26个字母,ASCII连续
hash[26],对应于第一个字符串中每个字母出现的次数
第二次字符串中每个字母出现则在相应位置做减法
最后如果hash[26]所有都为0,则为有效的字母异位
数组在哈希表中的经典应用
int hash[26] = {0};
// 遍历第一个字符串s的每个字母
for(i = 0; i< s.size; i++){
// 将每个字母映射到0-25
// 统计每个字母的出现频率
hash[s[i] - 'a'] ++;
}
// 遍历第二个字符串t的每个字母
for(i = 0; i< t.size; i++){
//充分利用一个哈希表
hash[t[i] - 'a'] --;
}
// 遍历哈希数组
for(i=0; i< 26; i++){
if(hash[i] != 0){
return false;
}
return true;
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'] ++;
}
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;
}
};
(三)两个数组的交集
注:如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费!
返回交集:需要去重(2,2)→(2)
(1)使用set解决
(限制数组中数值1000以内)→数组解决(浪费时间)
哈希表解法:给一个元素,判断在集合中是否出现过
将num1放入哈希表,遍历num2,在哈希表中查,是否出现过
出现过则放入result集合中
使用unordered_set,底层为哈希表,映射和取值操作效率最高
unordered_set result; //可以自动去重
//将num1直接放入s哈希表中
unordered_set num_set(num1);
for(int i = 0;i < num2.size; i++){
if(num_set.find(num2[i]) != num_set.end()){
//对于unordered_set来说,数值不可以重复,自动去重
result.insert(num2[i]);
}
//将set直接转换为数组
return vector(result);
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 = 0 ; i < nums2.size() ; i ++){
if(nums_set.find(nums2[i]) != nums_set.end()){
result.insert(nums2[i]);
}
}
return vector<int>(result.begin(),result.end());
}
};
(2)数组法解决
建立在
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
条件基础上
这种方法效率更高
unordered_set result;
int hash[1005] = {0};
for(int i = 0; i< nums1.size; i++){
// 记录所有出现过的元素
hash[nums1[i]] = 1;
}
for(int i = 0; i < nums2.size; i++){
if(hash[nums2[i]] == 1){
result.insert(nums2[i]);
}
}
return vector(result);
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result;
int nums[1005] = {0};
for(int i = 0 ; i < nums1.size() ; i ++){
nums[nums1[i]] = 1;
}
for(int i = 0 ; i < nums2.size() ; i ++){
if(nums[nums2[i]] ==1){
result.insert(nums2[i]);
}
}
return vector<int>(result.begin(), result.end());
}
};
(四)快乐数
可能最终得到1,可能陷入无限循环
无限循环的情况:各位数字的平方和重复出现
判断某一数字是否在集合中重复出现 → 哈希表
应为大小不确定 → 使用 set
不需要重复存储 → 使用 unordered_set
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> sum_set;
while(true){
int sum = 0;
while(n){
int temp = n % 10;
sum += temp*temp;
n = n/10;
}
if(sum == 1){
return true;
}
if(sum_set.find(sum) != sum_set.end()){
return false;
}else{
sum_set.insert(sum);
}
n = sum;
}
}
};
(五)两数之和
哈希法,使用map解决
哈希表存放遍历过的元素
每遍历一个元素num[i],查询是否遍历过(target - nums[i])的元素
存放:元素+下标 → 使用map(key, value)
将元素作为key ,因为是要找到这个元素是否在map中出想过,而map可以快速找到某个key是否出现过
由于不需要存放重复的数据,使用unordered_map,存和读的效率更高
unordered_map(int, int) map; //存放遍历过的元素
for(i = 0; i < nums.size; i++){
s = target - nums[i]; // s为待查询的值
iter = map.find(s);
//找到待查值
if(iter != map.end()){
return {iter -> value, i}
}else{ //未找到
map.insert(nums[i],i);
}
return NULL; //未找到
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> nums_map;
for(int i = 0;i < nums.size(); i++){
int s = target - nums[i];
auto iter = nums_map.find(s);
if(iter != nums_map.end()){
return {iter->second, i};
}else{
nums_map.insert(pair<int,int>(nums[i], i));
}
}
return {};
}
};
(六)四数相加
不需要去重,在不同位置就可以
A[i] + B[j] + C[k] + D[l] = 0
先遍历A、B数组,计算a+b,存入哈希表
再遍历C、D数组,计算c+d的值,再查找哈希表中有没有需要的值,即-(c+d)的数值
由于元素是int型,范围较大,不能用数组存储哈希表
由于哈希表中不仅要存储(a+b)的值(key),还要存储该值出现了几次(value),因此选用map(这个容易忘!!!)
在哈希表中找到需要的值后,cout要加上value的值,即在(a+b)中出现过的次数
通过将四个数组分成两组,将时间复杂度从n4降低到n2
unordered_map(int,int) map; //注:默认value=0
for(a:A){
for(b:B){
map[a+b]++; //找到a+b对应的键值,value++;没有出现过自动Insert
}
}
for(c:C){
for(d:D){
target = 0-(c+d);
if(map.find(target)!=map.end()){
//cout加上出现次数
cout+=map[target];
}
}
}
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> map;
int sum = 0;
for(int i = 0; i < nums1.size(); i++){
for(int j = 0; j < nums2.size(); j++){
map[nums1[i] + nums2[j]] ++;
}
}
for(int i = 0; i < nums3.size(); i++){
for(int j = 0; j < nums4.size(); j++){
int target = -(nums3[i]+nums4[j]);
if(map.find(target) != map.end()){
sum += map[target];
}
}
}
return sum;
}
};
(七)赎金信
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
if(ransomNote.size() > magazine.size()){
return false;
}
for(int i = 0; i < magazine.size(); i++){
record[magazine[i] - 'a'] ++;
}
for(int j = 0; j < ransomNote.size(); j++){
record[ransomNote[j] - 'a'] --;
if(record[ransomNote[j] - 'a'] < 0){
return false;
}
}
return true;
}
};
(八)三数之和
注意:答案中不可以包含重复的三元组
哈希法:
a+b+c = 0
对数组两次循环,分别确定a和b,然后寻找0-(a+b)是否出现在数组中出现过(c),但是有很多去重的细节
双指针法:
先对数组进行排序,因为不需要返回下表,因此可以排序后返回值
一个for循环确定a
left指针对应b, right指针对应c
如果nums[i]+nums[left]+nums[right] >0,
此时a的值固定,要想和变小,则需要right左移,right –
反之,则需要left右移,left ++
如果一直这样移动后,三数之和为0,则加入result中
难点:a,b,c不能有重复,因为不能有重复的结果集
sort(nums); //对数组排序
for(int i = 0; i < nums.size(); i++){
// 由于数组由小到大排序,因此如果nums[i]已经大于0了,则三数之和必然大于0
if(nums[i] > 0) return;
//对a去重,注意是比较是否和前一位重复,而不是nums[i] == nums[i+1],因为nums[i+1]的值是left可能取值,而在结果集里面a,b,c可以有重复
if(i > 0 && nums[i] == nums[i-1]) continue;
left= i + 1;
right = nums.size - 1;
// 移动left和right,得到a和b,注意left和right不能为同一个数,因此不能相对
while(left < right){
if(nums[i] + nums[left] + nums[right] > 0) right --;
else if(nums[i] + nums[left] + nums[right] < 0) left ++;
else{
//得到结果
result.push(nums[i], nums[left], nums[right]);
// 对left指向的b和right指向的c去重
// 注意:去重逻辑应该放在找到一个三元组之后,对b 和 c去重,否则收获不到结果
while((right > left) && (right[i] == right[i-1])) right --;
while((right > left) && (left[i] == left[i+1])) left ++;
left ++;
right --;
}
return result;
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int i = 0; i< nums.size(); i++){
if(nums[i] > 0) return result;
if(i>0 && nums[i] == nums[i-1]) continue;
int left = i+1;
int right = nums.size() -1;
while(left < right){
if((nums[i] + nums[left] + nums[right]) < 0) left ++;
else if((nums[i] + nums[left] + nums[right]) > 0 ) right --;
else{
//注意:对于vector的insert函数,要指定位置指针
result.insert(result.begin(),vector<int>{nums[i],nums[left],nums[right]});
while(left < right && nums[left] == nums[left+1]) left++;
while(left < right && nums[right] == nums[right -1]) right--;
left ++;
right --;
}
}
}
return result;
}
};
(九)四数之和
在三数之和的基础上,再加上一层循环
for(k )
for(i )
left ++; right --;
注意:这里的target是输入值,不是三数之和的恒为0
nums[k] + nums[i] + nums[left] + nums[right] = target
剪枝操作
当nums[k] > target时,不能剪枝,因为target可能为正数,可能为负数
对于负数来说,两数相加可能变得更小
//第一重循环
for(int k = 0; k < nums.size; k++){
//一级剪枝
//因为对于负数来说,两数相加可能变得更小,所以该剪枝必须强调两数都大于0
if(nums[k] > target && nums[k] > 0 && target > 0) return result;
//对k去重
//k和k-1相等,参考上一题
if(k > 0 && nums[k] == nums[k-1]) continue;
//三数之和的逻辑
//第二重逻辑
for(i = k+1; i < nums.size; i++){
//二级剪枝,这里将nums[k] + nums[i]看做整体
if((nums[k] + nums[i] > target) && (nums[k] + nums[i] > 0) && (target > 0)) return result;
//对i去重,注意这里i是要大于k+1,这样i-1大于等于k+1
if(i > k+1 && nums[i] == nums[i-1]) continue;
//移动left和right,收集结果
//对left和right去重,向中间移动指针
//循环结束,得到最终结果result
//参考三数之和
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int k = 0; k < nums.size(); k++){
if(nums[k] > target && nums[k] >= 0){
return result;
}
if(k > 0 && nums[k] == nums[k-1]) continue;
for(int i = k+1; i < nums.size(); i++){
if((nums[i]+nums[k] > target) && (nums[i]+nums[k] >= 0)){
//这里不能break,会导致返回的结果有缺失呢,比如输入【-3,-2,-1,0,0,1,2,3】,target=0;输出缺少【-1,0,0,1】
//如果二级剪枝直接return,第一层循环也提前终止了,这里就是当k=-2时,i遍历到了3,然后直接程序终止了,导致k都没有遍历到后面的-1。
continue;
}
if(i > k+1 && nums[i] == nums[i-1]) continue;
int left = i+1;
int right = nums.size() - 1;
while(left < right){
if((long)nums[k] + nums[i] + nums[left] + nums[right] > target) right --;
else if((long)nums[k] + nums[i] + nums[left] + nums[right] < target) left++;
else{
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
while(left < right && (nums[left] == nums[left + 1])) left ++;
while(left < right && (nums[right] == nums[right -1])) right --;
left ++;
right --;
}
}
}
}
return result;
}
};