跟着Carl学算法--哈希表

有效的字母异位词

力扣链接:题目链接

题目:给你两个字符串,如果这两个字符串的每个字符出现的次数都一样,返回true

思路:当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • 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不可修改 0(1) 0(1)

当我们要使用集合来解决哈希问题的时候,

  1. 优先使用unordered_set,因为它的查询和增删效率是最优的,、

  2. 如果需要集合是有序的,那么就用set,

  3. 如果要求不仅有序还要有重复数据的话,那么就用multiset。

Map同理

回到此题,使用字母连续,可以使用数组解决,让每一个字母映射一个数组一个地址a映射数组索引为0的地址(a-97),以此类推,需要一个容量26的数组,:

cpp 复制代码
class Solution {
public:
    bool isAnagram(string s, string t) {
        int hash[26]={0};        
        for(char i:s)
        hash[i-97]+=1;
        for(char i:t)
        hash[i-97]-=1;
        for(int i:hash){
            if(i!=0){
                return false;
            }
        }
        return true;
    }
};

两个数组的交集

力扣链接:题目链接

题目:求两个数组的交集

思路:因为数字大小随机,就不能使用数组了(映射不方便),(后来测试数据改为了1000以内了,可以创建1000的数组,空间换时间了)。

因为两个数组中都有可能有重复元素,因此需要两个set或者map(map,其实没必要,有些浪费vaule空间了),

一个set1存储数组1的元素,然后遍历数组2的元素是否在集合set1中出现过,如果出现过就添加到最终集合,最后遍历一遍最终集合以数组形式输出。

cpp 复制代码
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> set1;
        unordered_set<int> result;
        vector<int> ans;
        for (int i : nums1) {
        set1.emplace(i);
   		}
        for (int i : nums2) {
            if (set1.contains(i)) {
                result.emplace(i);
            }
        }
        for(int i:result){
            ans.emplace_back(i);
        }
       
        return ans;
    }
};

快乐数

力扣链接:题目链接

题目:求解一个数是不是快乐数

『快乐数]定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为1,也可能是无限循环但始终变不到1.如果这个过程结果为1,那么这个数就是快乐数。
  • 如果 (n 是快乐数就返回true ;不是,则返回 false

思路:关键是确定循环条件是什么,如何让循环结束

如果是快乐数,那得到1就可以结束了,那如果不是快乐数,那循环结束的条件应该是什么?当然是接下来的循环是无效循环,再循环下去已经没有意义了。就像题目中给出的提示"无限循环",只要当前的数之前已经得到过了,那就没有可以结束循环了。

哈希表,将已经计算过的值存入哈希表,查询更快,效率为O(1)(无序哈希表),

cpp 复制代码
class Solution {
public:
    int getNext(int n) {
        int sum = 0;
        while (n) {
            int digit = n % 10;
            sum += digit * digit;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        unordered_set<int> set;
        // 当新添加的数与集合内的数字相同时结束循环
        while (n != 1&&!set.count(n)) {
            // 不为1的新数存放到集合
            set.insert(n);
            //求下一个数
            n = getNext(n);
            // 为1时结束循环
        }
        return n==1;
    }
};

双指针:如果对上一阶段链表的题目的环形链表II印象比较深,就可以联想到,数字就是结点,判断是否是循环就是判断是否有环

cpp 复制代码
class Solution {
public:
    int getNext(int n) {
        int sum = 0;
        while (n) {
            int digit = n % 10;
            sum += digit * digit;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        int slow = n;
        int fast = getNext(n); //快指针先走,避免刚开始就符合结束条件

        while (fast != 1 && slow != fast) {
            slow = getNext(slow);          // 慢指针走一步
            fast = getNext(getNext(fast)); // 快指针走两步
        }

        return fast == 1;
    }
};

虽然两者的时间复杂度相同,都为O(log n),但是哈希表插入时,内部会哈希值计算、可能的哈希冲突处理、可能的内存重新分配、存储中间值的内存消耗等等,一系列内在消耗,还是建议使用双指针。

循环、环形等就使用双指针

注意:

  • 封装函数后,系统会进行优化,优化为内联函数,不会多出过多的消耗的,能封装为函数就尽可能封装。

  • 能手搓就尽量不调用

  • 求平方和的标准写法,直接判断当前数是否为0而不是提前判断下一个。

cpp 复制代码
while (n) {
    int digit = n % 10;
    sum += digit * digit;
    n /= 10;
}

两数之和

力扣链接:题目链接

题目:给你一个数组和target,问数组里面的哪两个元素之和等于target,如果等于则返回下标

思路:

遍历数组,把元素值-索引存入map(因为题目最后要求返回索引)

接着第二次遍历数组查找map中是否存在target-元素值的key

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); i++) {
            map.emplace(nums[i], i);
        }
        for (int i = 0; i < nums.size(); i++) {
            if (map.count(target - nums[i]) && i != map[target - nums[i]]) //由于map是独立存在数组的副本,需要额外判断map中的元素和数组中匹配的元素是否是同一个(题目要求不能使用两次)
                return {i, map[target - nums[i]]};
        }
        return {};
    }
};

遍历数组,把元素值-索引移入map,并查找map中是否存在target-元素值的key(是以上方法的优化),少了额外的相同元素判断,以及提前终止会少几次map的存储操作。

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); i++) {
            if (map.count(target - nums[i])) {
                return { map[target - nums[i]], i };
            }
            map[nums[i]] = i;
        }
        return {};
    }
};

四数相加II

力扣链接:题目链接

题目:给你4个数组,每个数组n个元素,各取每个数组的1个元素,相加等0,一共几种方案

思路:没错,就是基于上一道两数之和的,将4数之和转化为2组2数之和本质上是找 A + B = -(C + D) 的情况。具体如何实现:

  1. 计算前两个集群(nums1, nums2)的所有可能的和,以及每个记录和出现的次数,因此使用map。

  2. 同理计算后两个集群(nums3, nums4)。

  3. 遍历map1,查找map1中的key是否在map2中存在与之互为相反数的key,若存在,value值相乘即为可能方案,最后将所有方案相加返回。

cpp 复制代码
class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        if(nums1.size()==0){
            return 0;
        }
        unordered_map<int,int>map1;
        unordered_map<int,int>map2;
        for(int i:nums1){
            for(int j:nums2){
                map1[i+j]+=1;
            }
        }
        for(int i:nums3){
            for(int j:nums4){
                map2[i+j]+=1;
            }
        }
        int sum=0;
        for(auto pair:map1){
            if(map2.contains(-pair.first)){
                sum+=pair.second*map2[-pair.first];
            }
        }
        return sum;
        
    }
};

优化:

可以将2、3步合并,直接在遍历3、4数组时就查找合适的结果。

省去3、4数组所有可能在map2的存储空间,以及map1的遍历(虽然是效率是O(1))。

cpp 复制代码
class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        if(nums1.size()==0){
            return 0;
        }
        unordered_map<int,int>map1;
        for(int i:nums1){
            for(int j:nums2){
                map1[i+j]+=1;
            }
        }
        int sum=0;
        for(int i:nums3){
            for(int j:nums4){
               if(map1.contains(-i-j)){
                sum+=map1[-i-j];
               }
            }
        }
        return sum;
    }
};

赎金信

力扣链接:题目链接

题目:给你字符串A和B,问你A能不能由B里面的字符组成,B的字符每个只能用一次

思路:是有效的字母异位词的简单版,甚至不需要最后遍历一次哈希表,只要在遍历A时判断字符是否被包含即可(上一个是判断是否相等,因此需要判断哈希桶中是否有多余值存在,需要从头遍历一遍哈希表)

cpp 复制代码
class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int hash[26] = {0};
        for (char i : magazine) {
            hash[i - 97] += 1;
        }
        for (char i : ransomNote) {
            hash[i - 97] -= 1;
            if (hash[i - 97] < 0) {
                return false;
            }
        }
        return true;
    }
};

虽然简单,第一个一次OC有思路的

三数之和

力扣链接:题目链接

题目:给你一个整数数组,判断数组中是否存在三个数和为0,返回所有的所有和为 0 且不重复的三元组。

思路:两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在数组里出现过,然后使用索引实现元素不复用。
但是要实现元组不重复很难,比如{1,1,-1,-1,0},需要对a、b、c都进行重复判断

这种时候使用双指针更好,

首先将数组排序,然后有一层for循环,遍历数组,即a,在开始前需要对a进行去重,如果a的值与前一次相同就需要跳过。

双指针分别分别从剩余数组的两端向中间移动,根据三数之和的情况移动左右指针,等于0后取值,类似于二分法

取值后需要对左右指针继续收缩,但是如果下一个b、c与之前相同,答案就会出现重复元组,因此需要在移动指针时跳过相同的元素(同时注意指针不能越界)。

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> result;
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] > 0) //最小值大于0已经不会有满足要求的解了,直接返回
                return result;
            if (i > 0 && nums[i - 1] == nums[i]) //a去重
                continue;
            int left = i + 1;
            int right = nums.size() - 1;
            while (left < right) {
                if (nums[i] + nums[left] + nums[right] > 0) {
                    right--;
                    continue;
                }
                if (nums[i] + nums[left] + nums[right] < 0) {
                    left++;
                    continue;
                }
                result.push_back({nums[i], nums[left], nums[right]});
                while (left<right&&nums[left] == nums[++left]); //b去重
                while (left<right&&nums[right] == nums[--right]); //c去重
            }
        }
        return result;
    }
};

四数之和

力扣链接:题目链接

题目:给你一个数组和一个target,从数组里面取4个元素进行相加,等于target的结果有多少个。四元组不能重复。

思路:是三数之和的进阶版,只要在三数之和最外层再加一层指针即可,要注意去重和剪枝细节即可。a、b、c、d分别代表第1、2、3、4个数的位置。

剪枝优化:在进入具体的循环前,先对后续四个数的取值范围进行预判,

因为升序数组,随着指针前移,取值范围会不断变大(右移),所以一旦

当前循环的最小值大于目标值:直接break,已经无解,结束循环;

当前循环的最大值小于目标值:直接continue,当前循环不会有解,进入下一次循环。

cpp 复制代码
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> results;
        int n = nums.size();

        // 如果数组元素不足4个,直接返回
        if (n < 4)
            return results;

        // 排序
        sort(nums.begin(), nums.end());

        // 第一层固定
        for (int a = 0; a < n - 3; a++) {
            // 去重
            if (a > 0 && nums[a] == nums[a - 1])
                continue;

            // 优化:如果最小可能和大于target,则后续无解
            if ((long long)nums[a] + nums[a + 1] + nums[a + 2] + nums[a + 3] >
                target)
                break;

            // 优化:如果最大可能和小于target,继续下一个
            if ((long long)nums[a] + nums[n - 3] + nums[n - 2] + nums[n - 1] <
                target)
                continue;

            // 第二层固定
            for (int b = a + 1; b < n - 2; b++) {
                // 去重
                if (b > a + 1 && nums[b] == nums[b - 1])
                    continue;

                // 优化:如果最小可能和大于target,则后续无解
                if ((long long)nums[a] + nums[b] + nums[b + 1] + nums[b + 2] >
                    target)
                    break;

                // 优化:如果最大可能和小于target,继续下一个
                if ((long long)nums[a] + nums[b] + nums[n - 2] + nums[n - 1] <
                    target)
                    continue;

                // 双指针
                int c = b + 1;
                int d = n - 1;

                while (c < d) {
                    long long current_sum =
                        (long long)nums[a] + nums[b] + nums[c] + nums[d];
                    if (current_sum < target) {
                        c++;
                    } else if (current_sum > target) {
                        d--;
                    }

                    else {
                        results.push_back({nums[a], nums[b], nums[c], nums[d]});

                        // 去重 (无论是否相等都移到下一位)
                        while (c < d && nums[c] == nums[++c])
                            ;
                        while (c < d && nums[d] == nums[--d])
                            ;
                    }
                }
            }
        }

        return results;
    }
};

本来还想使用两组双指针解决的,里外各一组,因为最后要遍历数组,结束条件一定是里左右指针相遇了,根据里指针的最后移动情况,比如最后是里左指针移动的,说明数太小了,就移动外左指针,进行下一次循环。但是这样就不能进行剪枝了,因为一旦在里循环同层之前continue,进入了死循环,因为外左右指针都没有移动,外左右指针的移动是在内循环中进行的(根据内左右指针最后的移动情况确定移动外左指针还是外右指针),外左右指针的移动还不能里循环的之外进行,那样就又不能动态确定外指针的移动了。

最后最好不要妄想修改完善此方法,不仅费时费脑考虑各种细节,而且剪枝可能还没有一个双指针剪的好

相关推荐
海绵宝宝的好伙伴3 小时前
【数据结构】哈希表的理论与实现
数据结构·哈希算法·散列表
Aqua Cheng.3 小时前
代码随想录第七天|哈希表part02--454.四数相加II、383. 赎金信、15. 三数之和、18. 四数之和
java·数据结构·算法·散列表
zym大哥大3 小时前
哈希表封装myunordered_map以及set
数据结构·散列表
Nebula_g3 小时前
Java哈希表入门详解(Hash)
java·开发语言·学习·算法·哈希算法·初学者
Kent_J_Truman3 小时前
【模拟散列表】
数据结构·算法·蓝桥杯·散列表·常识类
努力努力再努力wz3 小时前
【C++进阶系列】:万字详解unordered_set和unordered_map,带你手搓一个哈希表!(附模拟实现unordered_set和unordered_map的源码)
java·linux·开发语言·数据结构·数据库·c++·散列表
加油=^_^=3 小时前
【C++】哈希表
数据结构·c++·散列表
Lchiyu3 小时前
哈希表 | 454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和
算法
对纯音乐情有独钟的阿甘3 小时前
【C++庖丁解牛】哈希表/散列表的设计原理 | 哈希函数
c++·哈希算法·散列表