算法学习随笔 4_哈希表有关算法题

 本章记录一些有关哈希表的一些较为经典或者自己第一次做印象比较深刻的算法以及题型,包含自己作为初学者第一次碰到题目时想到的思路以及网上其他更优秀的思路,本章持续更新中......

目录

No 49. 字母异位词分组(中等)

No ​​​​​438.找到字符串中所有字母异位词(中等)

No 349. 两个数组的交集 (简单)

No 350. 两个数组的交集 II(简单)

No 454. 四数相加 II(中等)

No 15.三数之和(中等)


No 49. 字母异位词分组(中等)

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/group-anagrams/

题目描述:

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。

示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:
输入: strs = [""]
输出: [[""]]

示例 3:
输入: strs = ["a"]
输出: [["a"]]

思路:这个题目是哈希表的典型题目。所谓字母异位词就是相同数量和类型的字母组合成的不同单词。所以我们可以将每一个字符串进行一个排序作为 map 的 key 值,这样会导致所有的字母异位词有相同的key,把这一类字符串放在一个字符串数组中即可。结合题干,结果列表是无序的,那么采用unordered_map是一个高效的选择。创建的map的key的类型为stringvalue的类型为vector<string>。具体如下:

首先用一个增强型for循环遍历字符串数组,将每一个字符串排序;其次将这个排序的字符串作为 map 的 key 值,并将该字符串未排序的状态写入到value中。最后将map的value写入到结果字符串数组中即可。

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> result;
        //map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的
        unordered_map<string, vector<string>> map;
        for (string str : strs) {
            //拿到每一个字符串
            string key = str;
            //sort函数:第一个参数first:是要排序的数组的起始地址。
            //第二个参数last:是结束的地址(最后一个数据的后一个数据的地址)
            //第三个参数comp是排序的方法:可以是从升序也可是降序。如果第三个参数不写,则默认的排序方法是从小到大排序。
            sort(key.begin(), key.end());
            //把这个key和这个str建立映射关系
            map[key].push_back(str);
        }
        //auto是根据后面的数据自动确定数据类型
        for (auto it = map.begin(); it != map.end(); it++) {
            //it->first 表示的是这个元素的key的值;
            //it->second 表示的是这个元素的value的值。
            result.push_back(it->second);
        }
    return result;
    }
};

No ​​​​​438.找到字符串中所有字母异位词(中等)

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/

题目描述:

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

思路:这个题目我最初想到的是子串匹配,在匹配的过程中利用数组记录。具体如下:首先将字符串 p 用一个数组进行遍历,数组的下标为 字母在0-25上的映射,数值为出现的次数。其次开始遍历字符串 s ,用同样的方式记录字符串 s 的字母,只不过是以字符串 p 的长度为一轮遍历的长度的,即每次都从一个字符开始,往后走 p.length()个位置;然后比对此时两个记录数组的值是否相同。若相同,则找到了一个异位词子串,记录起始位置,并开始下一轮。否则直接开始下一轮。这个方法的时间复杂度很高,因为每一轮都需要进行比对两个记录数组是否相等的操作。

优化方法:滑动窗口法。滑动窗口也是一个经典的技巧了,在这个题目中也同样有很好的效果。首先将字符串 p 的字母出现次数用一个数组 resP 记录下来,下标为字母在0-25上的映射,数值为出现的次数。然后在遍历 s 字符串时,使用两个指针,快指针循环向后遍历,将每一个遍历到的字符在 resP 数组中执行 -1操作,表示该字符进入了窗口中,若果这个字符在 resP 中的值小于0,说明该字符不是我们所需要的字符,此时就将慢指针所指的字母在 resP 中的值执行+1操作,并将慢指针向后移动一个位置,表示将该字母移出窗口。当窗口的大小等于字符串 p 的大小时,这就是一个符合要求的子串,此时的慢指针就是子串的起始位置。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        
        //初始思路,时间消耗巨大

        // if(s.length()<p.length()){
        //     return vector<int>();
        // }
        // //先把目标字符串进行记录,字符作为key,次数作为value
        // int resP[26]={0};
        // vector<int> res;
        // for(char c:p){
        //     resP[c-'a']++;
        // }
        // //开始匹配,一个一个字符往后走
        // for(int i=0;i<s.length();i++){
        //     //每次都要重置resS,因为每次匹配p.length()后就要重新匹配了,往后走一个字符重新匹配
        //     int resS[26]={0};
        //     //每次都是从i开始,往后走p.length()个位置
        //     for(int j=i;j<i+p.length()&&j<s.length();j++){
        //         resS[s[j]-'a']++;
        //     }
        //     //判断resS和resP是不是相等,相等就把这个i的位置保存
        //     int equal=1;
        //     for(int k=0;k<26;k++){
        //         if(resS[k]!=resP[k]){
        //             equal=0;
        //             break;
        //         }
        //     }
        //     if(equal==1){
        //         res.emplace_back(i);
        //     }
        // }
        // return res;

        //滑动窗口法
        int lenS = s.size(), lenP = p.size();
        if(lenS < lenP){
            return {};
        } 
        //用于记录字符串p中各个字母的出现次数,字母为key
        vector<int> resP(26);
        for(auto ch : p){
            resP[ch - 'a']++;
        }
        //用于保存结果
        vector<int> res;
        for(int left = 0, right = 0; right < lenS; right++) {
            //右边界一直像后移动,同时右边界所指的元素在resP中-1
            resP[s[right] - 'a']--;
            //找到了匹配的,开始让左边界右移,移动之前在resP中对左边界所指元素+1
            while(resP[s[right] - 'a'] < 0) {
                //只要是小于0的,就不是p中的字符
                resP[s[left] - 'a']++;
                left++;
            }

            if(right - left + 1 == lenP) {
                res.push_back(left);
            }
        }
        return res;
    }
};

No 349. 两个数组的交集 (简单)

来源:力扣(LeetCode)
链接:​https://leetcode-cn.com/problems/intersection-of-two-arrays/

题目描述:

给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。

示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

思路:这一题主要是为了记录一下 set 的使用。这里使用unordered_set,因为结果不要求考虑顺序,且唯一。在set中可以理解为key就是value,所以在set中不可以有重复值。因此我们可以先将第一个数组写入到set中,写入的同时就把重复值去除掉了,再后续遍历的时候就不需要再考虑去重的问题了。

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        //unordered_set读写效率最高,结果集合的特点是无序,不重复,符合unordered_set的特点
        unordered_set<int> res_set;
        //先把nums1放到集合中,注意在unordered_set中重复的只放一次
        unordered_set<int> nums1_set(nums1.begin(),nums1.end());
        //遍历nums2,看看是否有在nums1的集合中存在的数字
        for(int num:nums2){
            //.find()方法是在集合或数组中找传入的参数,如果没有就会返回最后一个元素的下一个元素,也就是空
            if(nums1_set.find(num)!=nums1_set.end()){
                //insert()可以将数据添加到结果数据集合中
                res_set.insert(num);
            }
        }
        return vector<int>(res_set.begin(),res_set.end());
    }
};

No 350. 两个数组的交集 II(简单)

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/

题目描述:

给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。

示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]

示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]

思路本题看起来和349差不多,但是这一题却是需要使用map来求解,因为题目中要求返回出现的次数,且不能重复,不考虑顺序,所以我们用unordered_map求解更好。map 的 key 是数值,value是数值出现的次数。关键点在于如何处理这句话:如果出现次数不一致,则考虑取较小值。首先我们将长度短的数组放到前面,并用unordered_map 来记录第一个数组。然后开始遍历第二个数组,每当第二个数组中的数字在map中有记录时,视为匹配成功,将该数值写入到结果中,并将 map 中该值的 value 值 -1,如果 value为 0 则擦除该数据。

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        //value可以重复,考虑使用map系列

        //小的在前
        if(nums1.size()>nums2.size()){
            return intersect(nums2,nums1);
        }
        //用unordered_map保存nums1,key为数值,value为出现的次数
        unordered_map<int,int> nums1_map;
        for(int num:nums1){
            nums1_map[num]++;
        }
        vector<int> res;
        //遍历nums2的数字
        for(int num:nums2){
            //如果m中num的次数不为0,也就是说nums2中的这个数在nums1中出现了
            if(nums1_map.count(num)!=0){
                //把这个数放到结果中,并在m中将这个key对应的次数-1
                res.push_back(num);                
                nums1_map[num]--;
                //这个key对应的value(次数)为0,就删除掉这个数字,保证了可以去两个中小的那个次数
                //这个思路和自己想的一样,但是自己的方法中需要更新集合,而集合是不可更改的
                if(nums1_map[num]==0){
                    nums1_map.erase(num);
                }
            }
        }
        return res;
    }
};

No 454. 四数相加 II(中等)

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/4sum-ii/

题目描述:

给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

        0 <= i, j, k, l < n
        nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

思路:猛地一看这道题涉及四个数组,好像很复杂,但其实已经简化不少,结果中也只需要返回符合条件的组数,并没有要求返回下标。思路是将前两个数组相加,这里相加的意思是一个数组中每一个数都要和另一个数组中的数相加,并将这个数值作为key记录在 unordered_map 中,把这个和出现的次数作为value 记录在 unordered_map 中。然后利用双层for循环遍历数组3和数组4,如果数组3和数组4中两个元素的和等于负的数组1和数组2的和,也就是map中的key值,那么这是一个符合条件的情况,但是在map中这个key值 (也就是数组1和数组2的和) 可能出现多次,这个时候就要把map中当前key对应的value添加到结果中。

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> sumAB_map;
        //把nums1和nums2中的每一个数字相加,得到一个map,key为相加的和,value为和出现的次数
        for(int a:nums1){
            for(int b:nums2){
                sumAB_map[a+b]++;
            }
        }
        int count=0;
        //遍历nums3和nums4,并由a+b+c+d=0来反推在map中找key为0-(c+d)的元素,找到了就将value加到count上
        for(int c:nums3){
            for(int d:nums4){
                if(sumAB_map.find(0-(c+d))!=sumAB_map.end()){
                    count+=sumAB_map[0-(c+d)];
                }
            }
        }
        return count;
    }
};

No 15.三数之和(中等)

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/3sum/

题目描述:

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

示例 2:
输入:nums = []
输出:[]

示例 3:
输入:nums = [0]
输出:[]

思路:这道题有一点难度,关键在于去重操作。如果我们使用哈希表在做,去重将是一个非常复杂的部分,在面试的情境下可能很难想完整。其实这道题也可以利用双指针来做,整体思路是先排序,然后在两个指针的外面套一层for循环,然后每一轮循环中,左指针从 i + 1 开始,右指针从末尾开始,计算此时处于位置 i,左指针,右指针这三个数的和,当左右指针重合时该轮结束,i + 1

当和等于0的时候,把这三个坐标写入到结果中,然后让两个指针分别向后、向前移动,再次计算这三个位置的和;

当和大于0的时候,因为已经排过序了,所以要想把和变小,只能让处于末尾的右指针向前移动,计算此时的和;

当和小于0的时候,同样因为排过序了,要想把和变大,只能让左指针向后移动,再次计算这三个位置的和。

关键点在于去重,每当我们移动了左指针,右指针和这一轮结束时移动 i 以后,都需要进行去重操作。去重的时候不能看当前位置下一个移动到的位置的数值是否一样,而是应该先使用该数值进行操作,操作完了以后,移动到下一个位置的时候,再看和上一个位置是否一样。否则就会导致漏掉某些结果。

// 错误去重方法,将会漏掉-1,-1,2 这种情况
if (nums[i] == nums[i + 1]) {
    continue;
}
//正确去除重复项,以i为例。实际上每次进行一次i,左指针,右指针的操作时都要进行一次去重操作
if(i>0&&nums[i]==nums[i-1]){
    continue;
}

上面的代码就是对循环变量 i 的错误和正确去重方式,对于左右指针也是一样,只不过这里所说的下一个位置对于左右指针来说移动方向是不同的,左指针的下一个位置是向后移动,右指针的下一个位置是向前移动。这种方法其实就是将原本需要三重循环O(n³) 的时间复杂度降低到了O(n²)。

//18. 四数之和 https://leetcode-cn.com/problems/4sum/
for(int j=i+1;j<max_len;j++){
    //对j正确去重
    if(j>i+1&&nums[j]==nums[j-1]){
        continue;
    }
}

还有一个题目是四个数字相加,利用双指针就可以把O(n^4)降低为O(n³)。四个数字相加的题目和这个类似,只不过是在两个指针的外面套两层for循环即可,但是在对于第二层的循环变量 j 去重时需要注意一点,j总是从j=i+1开始的,所以去重时j也要从j=i+1开始,而不能从0开始,如上代码所示。

三数之和完整代码如下:

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> res;
        //处理特殊情况,先排序
        sort(nums.begin(),nums.end());
        int max_len=nums.size();

        //双指针法
        for(int i=0;i<max_len;i++){
            if(nums[i]>0){
                return res;
            }
            //去除重复项,每次进行一次i,左指针,右指针的操作时都要进行一次去重操作
            if(i>0&&nums[i]==nums[i-1]){
                continue;
            }
            // 错误去重方法,将会漏掉-1,-1,2 这种情况
            // if (nums[i] == nums[i + 1]) {
            //     continue;
            // }
            
            int windows_left=i+1;
            int windows_right=max_len-1;
            while(windows_left<windows_right){
                int sum=nums[i]+nums[windows_left]+nums[windows_right];
                if(sum==0){
                    //去重怎么加减,关键看执行加减操作之前的数字是不是答案
                    res.push_back(vector<int>{nums[i],nums[windows_left],nums[windows_right]});
                    //找到答案时左右指针都收缩
                    windows_left++;
                    windows_right--;
                    //左指针去重,执行++操作的数字不是答案,所以要看执行++操作之前的数字的上一个数字是否重复
                    while(windows_left<windows_right&&nums[windows_left]==nums[windows_left-1]){
                        windows_left++;
                    }
                    //右指针去重,执行--操作的数字不是答案,所以要看执行--操作之前的数字的下一个数字是否重复
                    while(windows_left<windows_right&&nums[windows_right]==nums[windows_right+1]){
                        windows_right--;
                    }
                }
                //此时右指针应该从末尾向前移动,使得sum的值减小
                else if(sum>0){
                    windows_right--;
                    //右指针去重,执行--操作的数字不是答案,所以要看执行--操作之前的数字的下一个数字是否重复
                    while(windows_left<windows_right&&nums[windows_right]==nums[windows_right+1]){
                        windows_right--;
                    }
                }
                //此时左指针应该从前向后移动,使得sum的值增大
                else{
                    windows_left++;
                    //左指针去重,执行++操作的数字不是答案,所以要看执行++操作之前的数字的上一个数字是否重复
                    while(windows_left<windows_right&&nums[windows_left]==nums[windows_left-1]){
                        windows_left++;
                    }
                }
            }
        }
        return res;
    }
};

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值