代码随想录哈希表篇|哈希表理论基础、242.有效的字母异位词、349. 两个数组的交集、202.快乐数、1. 两数之和、454.四数相加II、383. 赎金信、15. 三数之和、18. 四数之和

本文详细介绍了哈希表的概念和应用场景,包括解决哈希碰撞的拉链法和线性探测法,以及在数组、set、map等数据结构中的运用。文章通过具体编程问题(如字母异位词、两数之和、交集计算、快乐数判断等)展示了哈希表在提高查询和处理效率上的优势。
摘要由CSDN通过智能技术生成

哈希表理论基础

1、什么是哈希表?

哈希表是一种数据结构,根据关键码值(key value)进行之直接访问,关键码值映射到表中一个位置来访问记录,以加快查找的速度。简单来说,数组就是一种哈希表。

2、什么情况下可以使用哈希表?

当我们要快速判断一个元素是否出现集合中,则可以使用哈希表。

3、哈希函数的过程

输入的name首先通过hashCode转化为数值,然后对哈希表长度取余,这就得到了在哈希表中的index值。而如果学生数量大于哈希表大小,则会出现不同名字映射到同一个索引下标的情况,这就是哈希碰撞。

哈希表2
4、哈希碰撞的解决方法
1)拉链法

简单来说,就是在发生冲突的位置,将冲突元素存储在链表中,通过索引分别找到冲突元素。

哈希表4

2)线性探测法

这种方法简单来说就是当发生冲突时,先把一个放在当前位置,另一个则向下查找空位存放,所以这种方法就要求tablesize必须要大于datasize。

5、常见的三种哈希结构

三种哈希结构:数组、set(数组)、map(映射)

集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::set红黑树有序O(log n)O(log n)
std::multiset红黑树有序O(logn)O(logn)
std::unordered_set哈希表无序O(1)O(1)
映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
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_map与unordered_set的主要区别在于:

1、set中只有键(key),而map包含键值对(key value)

2、set中的元素是无序不重复的,map中的键key是无序不重复的,但是值是可以重复的(键值对不重复即可)。

242.有效的字母异位词

题目链接:242. 有效的字母异位词 - 力扣(Leetcode)
视频链接:学透哈希表,数组使用有技巧!Leetcode:242.有效的字母异位词_哔哩哔哩_bilibili
解体思路:

​ 字母异位词的含义是:除了字母顺序不同,所有的字母个数和字母类型都一样。

​ 因为本题的元素值有限,所以可以使用数组,将长度为26的数组下标分别对应26个字母,数组值对应字母出现的次数。遍历S字符串,当出现一个字母就在数组中字母对应位置++,再遍历T字符串,同样出现一个字母就在相应位置–,最后判断hash数组是否为0,则可以知道两个字符串是否是除了顺序之外完全一样了。

​ 本题有一个关键点在于:如何将26个字母分别对应到hash数组的位置,因为字母都是小写的,所以ASCII码是连续的,那么就可以利用ASCII码的相对位置来对应。hash[s[i] - ‘a’]通过再数组下标引用的位置用字母相减的方法,就能计算出两个字母ASCII码的相对距离,这样就把字母a默认为了index=0的位置,后续字母依次对应。

总结:本题可知使用hash数组的常规用法,遍历容器,然后将容器中的元素对应的数组的每个位置,出现一次就在对应位置++;
class Solution {           //哈希表法
public:
    bool isAnagram(string s, string t) {
        int hash[26]={0};                   //数组本身就是一种hash表,创建一个26位的数组
        for(int i = 0;i<s.size();i++){      //遍历s字符串
            hash[s[i] - 'a']++;             //s[i]是s字符串第i位的字母,作为下标括号里的运算时候,两个字母编译器会自动取他们的ASCII码值计算
        }                                   //所以这里默认就把“a”当作了是index=0的位置,当s[i]=“a”,此时index=0,所以hash表的第一位+1,以此类推
        for(int j = 0;j<t.size();j++){      //遍历t字符串
            hash[t[j] - 'a']--;             //在s字符串遍历完以后的hash基础上,遇到一个字母就-1
        }
        for(int k = 0;k<26;k++){
            if(hash[k] != 0)return false;  //如果两个字符串所组成的字母和个数完全相同,那么++--之后是结果应该是hash全为0,如果不为0那么就一定不同
        }
        return true;                         //如果没有返回false,那么就肯定是相同了,所以返回true
    }
};

349. 两个数组的交集

题目链接:349. 两个数组的交集 - 力扣(Leetcode)
视频链接:学透哈希表,set使用有技巧!Leetcode:349. 两个数组的交集_哔哩哔哩_bilibili
解体思路:
方法一、数组法

​ 因为本题给出了元素格式限制,所以可以使用数组进行解决。

​ 同样的操作,先把第一个容器中所有元素对应到hash数组中的每个位置,然后遍历,对应位置+1。然后遍历另一个容器,如果对应位置有值,就把该位置对应的元素塞入结果容器,因为只需要元素内容即可,且输出元素去重,不考虑顺序和下标等问题,所以可以使用unordered_set作为结果容器。

class Solution {          //使用hash数组解决
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result;                      //定义用于存储结果的set
        int hash[1002]={0};                             //根据提示可知nums1和nums2的长度和数值都不会大于1000,所以定义一个长度比1000大一点的数组
        for(int num: nums1){                            //遍历nums1,并把当前遍历的元素赋值给num
            hash[num]=1;                                //因为nums1数值都不会大于1000,所以在定义的hash数组中必定能找到num作为下标对应的位置
        }                                               //也就是说如果nums1中出现了num这个数字,就在hash数组中index为num的位置标一个1表示有这个数
        for(int num: nums2){                            //遍历nums2,并把当前nums2赋值给num
            if(hash[num]==1){                           //如果hash数组中index=num的位置有值,则表示nums1中也有num这个数字
                result.insert(num);                     //也就是说nums1和nums2中都有num这个数,所以是交际,把num存入result中
            }
        }
        return vector<int>(result.begin(),result.end());  //将result结果利用迭代器村民如vector中
    }
};
方法二、set法

​ 如果本题的容器长度未知,可能非常大的情况下就不能使用数组了,而又因为本题中交集元素输出的结果是去重的(例如两个容器中都有两个2,输出结果只输出一个2)这时就可以使用unordered_set先把一个容器的数放入其中(因为这里直接去重也并不影响),然后遍历另一个容器,在set中查找当前遍历的这个元素是否存在,如果存在就把该元素塞入结果容器中,同样的结果容器需要去重,所以也是用unordered_set。

class Solution {        //使用set解决
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result;                                         //声明一个用于存储交集结果的set,unordered_set可以去重,且不在乎顺序
        unordered_set<int> nums(nums1.begin(),nums1.end());                //用迭代器遍历nums1容器,把所有元素存入nums这个set中
        for( auto num: nums2){               //C++11新特性:基于范围的 for 循环,遍历nums2的内容,同时将当前被遍历的元素存储到“:”前的声明num中
            if(nums.find(num) != nums.end()){  //find函数的返回值为迭代器或指针,因为end()不在查找范围,所以如果没查找到就返回end()
                result.insert(num);            //所以find函数经常使用返回是否等于end来判断是否查找成功
            }                                  //查找成功则将当前num存入result中
        }
        return vector<int>(result.begin(),result.end());         //利用迭代器将unordered_set存入vector中
    }
};

202. 快乐数

题目链接:202. 快乐数 - 力扣(Leetcode)
解体思路:

​ 快乐数定义:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。

方法一、使用hash数组

​ 第一个关键点:如果计算一个数各个数位上数字的平方和。通过对10取余得到个位数,计算平方,然后再除以10去掉个位数(因为是整形),如此循环,直到最高位/10之后变成小数,整形变换之后为0就跳出循环了。

根据本题给的数字n范围,可知其最大为10位数,而10位数的最大各位数字平方和为810,所以数组只要比810大一点,就可以存入所有的平方和数字。通过while循环迭代计算平方和,其结果根据题目所说只有两种情况:循环和等于1。每次计算后将结果存入数组中,如果当前计算的sum在数组中出现过了那么说明就进入循环了,输出false。

class Solution {            //使用数组来解决
public:
    int get_num(int n){                     //计算一个数字各个位置上的数字平方和
        int sum=0;
        while(n>0){
            sum += (n%10) *(n%10);          //n%10表示数字对10取余数也就是个位数,sum加上个位数的平方
            n /=10;                         //n/10表示去除掉个位数,这样循环,就可以从个位开始,十位百位千位,遍历所有的数位计算平方和
        }
        return sum;                         //返回本次计算和sum
    }
    bool isHappy(int n) {
        int hash[815]={0};              //因为n的范围为2^31-1,也就是10位数,取10位数上全为9,则最大的平方和为9^2*10=810;所以数组比810大一点即可
        int sum = get_num(n);           //计算sum
        while(sum !=1){                 //如果sum不为1进入循环
            if(hash[sum]==1)return false;  //如果hash数组的index=sum位置=1即sum这个数曾经出现过,则表示要进入循环了,输出false
            else hash[sum]++;              //如果该位置没有数,则把这一位填1,表示出现了sum
            sum = get_num(sum);            //迭代计算下一个sum
        }
        return true;                    //sum为1直接输出true
    }
};
方法二、set法

​ 根据进入循环的意思我们可以知道,就是同一个平方和再次出现,那么我们就可以用unordered_set存放每次平方和,然后迭代计算进行查找,如果在集合中找到了本次的sum则说明进入了循环,则输出false,如果没找到就把本次sum存入set中。

class Solution {            //使用unordered_set来解决
public:
    int get_num(int n){                     //计算一个数字各个位置上的数字平方和
        int sum=0;
        while(n>0){
            sum += (n%10) *(n%10);          //n%10表示数字对10取余数也就是个位数,sum加上个位数的平方
            n /=10;                         //n/10表示去除掉个位数,这样循环,就可以从个位开始,十位百位千位,遍历所有的数位计算平方和
        }
        return sum;                         //返回本次计算和sum
    }
    bool isHappy(int n) {
        unordered_set<int>hash;             //定义一个int类型的unordered_set
        int sum=get_num(n);                 //调用计算平方和函数
        while(sum !=1){                     //如果sum不为1,则进入循环
            if(hash.find(sum) !=hash.end())return false;   //通过find函数查找set中有没有当前的sum,如果有sum则表示sum计算重复了,后续将进入循环
            else hash.insert(sum);                         //如果没找到,则将该sum存入set中
            sum = get_num(sum);                            //迭代计算下一个sum
        }
        return true;                                       //如果最终计算sum为1,则直接返回true
    }
};

1. 两数之和

题目链接:1. 两数之和 - 力扣(Leetcode)
视频链接:梦开始的地方,Leetcode:1.两数之和,学透哈希表,map使用有技巧!_哔哩哔哩_bilibili
解题思路:
方法一、两层for循环暴力法
class Solution {            //两层for循环暴力求解法
public:
    vector<int> twoSum(vector<int>& nums, int target) 
    {
        int i,j;
        int n = nums.size();
        for(int i =0;i<n-1;i++)                 //遍历数组nums除了最后一个的所有数字
        {
            for(int j =i+1;j<n;j++)             //遍历i之后的所有数
            {
                if(nums[i]+nums[j]==target)     //如果两个数相加等于目标值
                {
                    return {i,j};               //就返回当前的i和j
                }
            }
        }
        return {i,j};                           //外层for也需要一个return
    };
};
方法二、map法

​ 因为本地需要返回下标,也就是说在容器中需要存放数值和下标,所以就可以使用unordered_map来存放key和value了。

​ 需要注意的是,unordered_map的定义方式:unordered_map<int,int> map,第一个值为key,用first指向它,第二个为value,用second指向它。

class Solution {                   //使用unordered_map解决
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> map;                                //定义一个map,由一个key元素值和其对应的下标value组成都是int型
        for(int i = 0;i<nums.size();i++){                          //遍历数组
            if(map.find(target - nums[i])!=map.end()){             //用find函数查找map中是否由和当前nums之和等于target的另一个数,这一组数对
                return{map.find(target - nums[i])->second,i};      //如果有则返回这个数对的第二个值,也就是其value下标,和当前nums[i]的i
            }
            map.insert(pair<int,int>(nums[i],i));                //如果没有的话则把当前nums的值和下标作为key和value塞进map中,pair<,>表示一个数对
        }
        return {};                                               //如果上述for循环结束都没有return则返回一个数组,因为返回类型要是vector,
    }                                                            //所以不能直接返回false或者0
};

454.四数相加II

题目链接:454. 四数相加 II - 力扣(Leetcode)
视频链接:学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili
解体思路:

​ 本题的核心思路是将四个数分为两组,这样就退化成了两数之和。因为需要同即满足要求的三元组个数,所以就可以使用unordered_map作为存储容器,它的key是数字和,而value是出现的次数,这一点和数组是十分类似的。

​ 首先两层for循环把数字a和b相加,和作为key,出现次数作为value存入map中,然后再两层for循环,在map中寻找符合与当前c+d和为0的a+b,并把a+b出现的次数也就是map的value值赋给count。

class Solution {               //用unordered_map解决
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> map;           //本题看似是四数相加,其实可以拆成两组,每两个数之和看作一个数,则退化成了两数相加=target
        for(int a :nums1){                    //本题需要统计出现次数,所以有两个量需要存储,一个是和的值另一个是和的值出现的次数,所以用map
            for(int b:nums2){
                map[a+b]++;                   //两层循环遍历两个容器,把和作为key,出现的次数作为value,简单来说要求谁谁就作为value
            }
        }
        int count = 0;
        for(int c:nums3){                     //遍历另外两个容器
            for(int d:nums4){
                if(map.find(0-(c+d)) !=map.end()){   //在map中查找与c+d之和为0的值key
                    count +=map[0-(c+d)];            //把符合的key对应的value值也就是出现的次数+=给count
                }
            }
        }return count;
    }
};

383. 赎金信

题目链接:383. 赎金信 - 力扣(Leetcode)
解题思路:

​ 本题想表达的意思就是:所有的赎金信上的字母要在magazine上的字符串中能找到,并且只能使用一次。

​ 本题与242.有效的字母异位词有相似之处,242中需要两个字符串彼此都要找到且一一对应,而本题只需要从赎金信的string到magazine的单向对应。

方法一、暴力法

​ 两层for循环遍历两个字符串,如果有相同的字符串就删除赎金信上的字母,最后如何赎金信上的字母都被删除完了就说明在mag上都能找到赎金信上需要的字母。

需要注意的点:1、必须是magazine在外层循环;2、在内层循环符合条件后删除当前的ran字母,然后break出当前内层循环

上述两点配合起来,才能避免一个maga字母对应多个ran字母的问题。

class Solution {             //暴力法
public:
    bool canConstruct(string ransomNote, string magazine) {
        for (int i = 0; i < magazine.length(); i++) {                  //这里之所以是mag在外层是因为要让mag的每个元素只能用一次
            for (int j = 0; j < ransomNote.length(); j++) {            //遍历ran
                if (magazine[i] == ransomNote[j]) {                    //如果相等
                    ransomNote.erase(ransomNote.begin() +j);          //就删除当前位置这个元素,erase函数需要用迭代器删除,或者直接删除元素值
                    break;                                            //
                }
            }
        }
        if(ransomNote.size()==0)return true;                          //如果赎金信元素为空则全都能找到
        return false;                                                 //否则就找不全
    }
};
方法二、hash数组法

​ 因为本题又是关于字母的问题是否出现在另一个集合中的问题,所以依然可以使用哈希表解决,并且这里字母个数有限所以可以使用数组法。首先把magazine的所有字母都存入数组中,key是字母的相对序号,value是出现的次数,这步操作相当于是把mag上所有的字母都剪下来备用了。然后再遍历ran字符产,当有对应的字母就–,如果出现某一位有负数,就说明ran上需要的个数在mag中不够,则返回false,不然就返回true。

class Solution {             //hash数组解法
public:
    bool canConstruct(string ransomNote, string magazine) {
        int hash[26]={0};
        if(ransomNote.size()>magazine.size())return false;      //其实这里不需要也是可以实现功能的,但是这个判据可以减少不必要的后续计算,省内存
        for(int i=0; i<magazine.size();i++){                    //跟有效字母异位词的思想差不多,都是先遍历一个将元素存入数组++,然后遍历另一个
            hash[magazine[i]-'a']++;                            //数组如果有这个元素就--
        }
        for(int i=0 ;i<ransomNote.size();i++){
            hash[ransomNote[i]-'a']--;
            if(hash[ransomNote[i]-'a']<0)return false;          //如果出现某一位上为负数,也就是说这个字母在ran中出现的比mag中的多,那必然无法组成
        }
        return true;
    }
};

15. 三数之和

题目链接:15. 三数之和 - 力扣(Leetcode)
视频链接:梦破碎的地方!| LeetCode:15.三数之和_哔哩哔哩_bilibili
解题思路:
方法一、哈希法

​ 本题乍一看似乎可以用两层for循环确定 a 和b 的数值,再使用哈希法来确定 0-(a+b) 是否在数组里出现过。该思路没有问题,但是因为题目中要求三元组不能重复,将三元组放入vector中然后进行去重操作是非常费时的,并且剪枝和去重也比较复杂,因此本题不建议使用哈希法。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[j], c = -(a + b)
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
            if (nums[i] > 0) break;
            if (i > 0 && nums[i] == nums[i - 1])continue; //三元组元素a去重
            unordered_set<int> set;
            for (int j = i + 1; j < nums.size(); j++) {
                if (j > i + 2&& nums[j] == nums[j-1]&& nums[j-1] == nums[j-2]) continue;// 三元组元素b去重
                int c = 0 - (nums[i] + nums[j]);
                if (set.find(c) != set.end()) {
                    result.push_back({nums[i], nums[j], c});
                    set.erase(c);// 三元组元素c去重
                } 
                else set.insert(nums[j]);
            }
        }
        return result;
    }
};
方法二、双指针法

​ 第一个数是外层遍历,然后再第一个数后面分别设置一个左右指针,三个数相加,如果大于0,则right指针–,如果小于0,则left指针++。

​ 本题一个关键点:如果出现了重复的数字,那么意味着后面会有基于该数字的同样的组合,所以同样的数字需要去除,那么如何去重呢?如果用nums[i] ==nums[i+1]来判断,那么会导致同一个三元组中有同样数字的这种情况被错误排除了(题目说的是不能有重复的三元组,但没有说一个三元组中不能有重复数字),所以要使用nums[i] ==nums[i-1],即前一个数已经判定过是否可以,那么后一个同样的数就没有再次判断的必要,是需要遍历到下一个相同的数字后再跳过。

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()-2;i++){                       //遍历元素,因为后面有left和right且不相等,所以到size-2
            if(nums[i]>0) break;                         //因为正向排序,所以如果当前元素都大于0了,那后续都为正,和就不可能为0,直接返回
            if(i>0 && nums[i] ==nums[i-1]) continue; //给第一个元素去重,因为第一个元素必为负,如果重复了,那么后两个元素的选择肯定
            int left = i+1;                     //存在重复,例如第一个为-3,则后两个可以为(0,3)/(1,2),那么下一个-3对应的也是一样的
            int right = nums.size()-1;         //定义左右指针,两个指针分别向中间移动
            while(right>left){                 //不可以相等,相等了则指向同一个元素,那么三元组的元素就相同了
                if(nums[i]+nums[left]+nums[right]>0)right--;         //和>0,那说明较大的数太大了,所以right--,让大数减小
                else if(nums[i]+nums[left]+nums[right]<0)left++;     //同理
                else{                                                //如果和为0,符合要求
                    result.emplace_back(vector<int>{nums[i],nums[left],nums[right]});      //先把当前第一个找到的三元组存入结果中
                    while(right>left && nums[left]==nums[left+1]) left++;        //然后去重复left
                    //while(right>left && nums[right]==nums[right-1]) right--;   //这里可以对right也去重复,但其实因为left+right和为定值
                    //right--;                                              //只要left不重复,那么满足的right也必定不重复,所以可以不用对right去重
                    left++;                                               //left走向下一位(与上一位不同)
                }
            }
        }
        return result;
    }
};

18. 四数之和

题目链接:18. 四数之和 - 力扣(Leetcode)
视频链接:难在去重和剪枝!| LeetCode:18. 四数之和_哔哩哔哩_bilibili
解题思路:

​ 本题的解题思路和三数之和是相同的,同样是使用双指针法,只不过在外层多套了一个for循环,多一次剪枝和去重的操作;并且因为是四数相加,每个数的范围是(2^31)-1=2147483647,本题四个数相加需要考虑是否会有溢出情况,所以再相加的时候强转为double,防止相加之后溢出最大值从而变成了负数。

​ 本题一个恶心的测试案例就是位置测试是否溢出:

[1000000000,1000000000,1000000000,1000000000]//四个数都在范围内,但是相加之后溢出,结果是-1852516353,显然是小于target
-294967296
class Solution {             //双指针法,和三数之和类似
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>>result; 
        sort(nums.begin(),nums.end());
        for(int i=0;i<nums.size();i++){
            if(nums[i]>target && nums[i]>0)break;     //如果当a是大于0的(为了排除负数越加越小),且大于target
            if(i>0&&nums[i] ==nums[i-1])continue;     //a去重
            for(int j = i+1;j<nums.size();j++){
                if(nums[i] + nums[j]>target && nums[j]>0)break;    //当b是大于0的(为了排除负数越加越小)。且a+b大于target
                if(j>i+1&&nums[j] ==nums[j-1])continue;            //b去重
                int left = j+1;
                int right = nums.size()-1;                         //定义左右指针
                while(right>left){
                    if((double)nums[i]+nums[j]+nums[left]+nums[right]>target)right--;         //四个数相加大于target
                    else if((double)nums[i]+nums[j]+nums[left]+nums[right]<target)left++;//这里用double强转类型,是因为四个int相加会溢出int的范围
                    else{
                        result.emplace_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});  
                        while(right>left &&nums[left]==nums[left+1])left++;                 //和三数之和类似只要让left去重即可,right自适应
                        left++;
                    }
                }
            }
        }
        return result;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值