代码随想录——哈希表

(一)基础理论

(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 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即keyvalue。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 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;
    }
};
  • 29
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值