代码随想录学习记录——哈希表篇

常见的三种哈希表结构

常见三种哈希表结构为数组、集合(set)、映射(map)。

集合底层实现是否有序数值是否可重复能否更改数值查找效率增删效率
set红黑树有序O(logn)O(logn)
multiset红黑树有序O(logn)O(logn)
unordered_set哈希表无序O(1)O(1)
映射底层实现是否有序数值是否可重复能否更改数值查找效率增删效率
map红黑树key有序key不可重复key不可修改O(logn)O(logn)
multimap红黑树key有序key可重复key不可修改O(logn)O(logn)
unordered_map哈希表key无序key不可重复key不可修改O(1)O(1)

因此在选择集合时,优先选择unordered_set ,因为它效率是最佳的。因此要求集合有序就选择set,如果还要求可重复就选择multiset

因此当遇到情况为需要快速判断一个元素是否在集合里的时候就考虑哈希法,但它需要额外的set或者map来存储数据,因此是牺牲了空间换取了时间

245、有效的字母异位词

由于s和t中只包含小写字母,那么思路可以是用一个长度为26的数组来记录,具体为:

  • 先遍历s的每一个字符,然后在数组的对应位置上增加计数
  • 再遍历t的每一个字符,然后在数组的对应位置上减少计数

如果满足要求,那么最后数组必然每一个位置都是0,否则就出错。

class Solution {
public:
    bool isAnagram(string s, string t) {   
        int result[26] = {0};
        for(int i = 0; i < s.size();i++){
            result[int(s[i]-'a')]++;
        }
        for(int i = 0; i < t.size();i++){
            result[int(t[i] - 'a')]--;
        }
        for(int i = 0; i < 26;i++){
            if(result[i]!= 0){
                return false;
            }
        }
        return true;
    }
};
383、赎金信

这一题可以利用上一题的方法,只需要创建两个数组分开统计,然后再遍历第一个数组,如果元素不为0且元素大于第二个数组的对应元素,就说明第二个数组无法供应,那么就返回错误。

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int num_strR[26] = {0};
        int num_strM[26] = {0};
        for(int i = 0;i<ransomNote.size();i++){
            num_strR[int(ransomNote[i]-'a')]++;
        }
        for(int i = 0;i<magazine.size();i++){
            num_strM[int(magazine[i] - 'a')]++;
        }
        
        for( int i = 0 ; i < 26; i++){
            if(num_strR[i] != 0){
                if(num_strR[i] > num_strM[i]){
                    return false;
                }
            }
        }
        return true;
    }
};
49、字母异位词分组

该题比较难的一点是具有多个字符串,那么最简单的思路当然是直接两两进行比较,但这样的时间复杂度太高了。而我们是目标是找到那些属于字母异位词的然后将其纳入对应的list中,再合成一个大list返回,因此思路便是有没有可能找到属于字母异位词的特点,再通过映射的方式进行存储

字母异位词是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。也就是说如果两个单词互为字母异位词,那么它们含有的字母是相同的,只不过是顺序不一样,那么可以对每一个单词进行排序,排完序如果是相同的那么就一定属于字母异位词。所以可以用排完序的新单词来作为映射的键,而值就是对应的字母异位词的列表。因此可以写出如下代码:

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> result;  // 用来存放结果
        unordered_map<string,vector<string>> hash1;  // 用来作为映射
        for(vector<string>::iterator it = strs.begin();it != strs.end();it++){
            string temStr = *it;  // 不对原来的排序,因为后面结果还需要存放为原来的
            sort(temStr.begin(),temStr.end());  // 排序
            hash1[temStr].push_back(*it); // 按照对应键而插入
        }
        for(unordered_map<string,vector<string>>::iterator it = hash1.begin(); it != hash1.end(); it++){
            result.push_back(it->second);
        }
        return result;
    }
};
438、找到字符串中所有的字符异位词

这一题我原来也想用上一题的思路,也就是滑动窗口然后加上排序比较,写出来以下的代码:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        string temStr = p;
        sort(temStr.begin(),temStr.end());
        int lenP = p.length();
        vector<int> result;
        int slowPoint = 0;
        int fastPoint = lenP-1;
        int lenS = s.length();
        while(fastPoint < lenS){
            string temCop = s.substr(slowPoint,lenP);
            sort(temCop.begin(),temCop.end());
            if ( temCop == temStr){
                result.push_back(slowPoint);
            }
            fastPoint++;
            slowPoint++;
        }
        return result;
    }
};

但这会导致超时,因为排序和字符串切片的时间可能造成了影响,因此要避开这种排序的思想,那么就回到上次利用数组来记录字母出现次数的想法,因此可以写出以下代码;

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int>result;
        vector<int>Arrs(26);  //26个默认值为0
        vector<int>Arrp(26);
        if(s.length() < p.length()){
            return vector<int>();  // 返回的空集也需要是满足int的vector类型
        }
        for(int i = 0; i< p.length(); i++){
            Arrs[s[i]-'a']++;
            Arrp[p[i]-'a']++;
        }   
        if(Arrs == Arrp){
            result.push_back(0);
        }
        int slowP = 0;
        int fastP = p.length();
        while(fastP < s.length()){
            Arrs[s[slowP] - 'a']--;
            Arrs[s[fastP] - 'a']++;
            slowP++;
            fastP++;
            if(Arrs == Arrp){
                result.push_back(slowP);
            }
        }
        return result;
    }
};

这里有需要注意的地方,就是如果s的长度比p的长度短,那么一定不满足要返回一个空集,但是返回的空集也要满足vector的形式,因此返回的vector()就是一个空集的形式 。另一个需要注意的点就是先滑动窗口在进行判断

349、两个数组的交集

这一题一开始我的思路比较清晰,首要便是要去除重复元素,然后再用一个映射,对其中一个数组的每一个元素进行映射,那么该数组中存在的元素就映射不为0,否则为0。那么再循环第二个数组,遍历其中每一个元素,如果该元素在映射中的值不为0,即为交集中的元素,否则就不是。因此代码如下:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        vector<int>result;
        unordered_set<int> setNums1(nums1.begin(),nums1.end());
        unordered_set<int> setNums2(nums2.begin(),nums2.end());
        unordered_map<int,int> mapComp;
        for(unordered_set<int>::iterator it = setNums1.begin();it != setNums1.end(); it++){
            mapComp[*it]++;
        }
        for(unordered_set<int>::iterator it = setNums2.begin();it != setNums2.end(); it++){
            if (mapComp[*it] != 0){
                result.push_back(*it);
            }
        }
        return result;
    }
};

代码随想录中的思路更为简便,就是先为一个数组建立集合,然后在遍历第二个数组的元素,在第一个数组的集合中进行查找,这样只用到了一个set的空间,时间复杂度也比较低。并且由于没有对第二个数组去重,因此其用来存储结果的容器需要为集合类型才能去重。

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int>result;
        unordered_set<int> setNums1(nums1.begin(),nums1.end());
        for(int num:nums2){
            if(setNums1.find(num) != setNums1.end()){
                result.insert(num);
            }
        }
        return vector<int>(result.begin(),result.end());
    }
};

这里补充一下for条件中冒号的用法

vector<int>result;
for(auto a: result)...

这里如果不清楚类型就可以用 a u t o auto auto,自动判读类型,这样就会自动遍历容器中的每一个元素,不用再写迭代器。另外一个需要注意的点是这种方法对容器内部数据的修改是无效的,因为这样做的是值拷贝,如果要修改则需要传递方式为引用,即:

vector<int>result;
for(auto& a: result)...

另外,如果数组中的元素的范围并且不太大,那么用普通的数组来记录也是可以的,如下:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result; // 存放结果,之所以用set是为了给结果集去重
        int myArr[1005] = {0}; // 默认数值为0
        for (int num : nums1) { // nums1中出现的字母在hash数组中做记录
            myArr[num] = 1;
        }
        for (int num : nums2) { // nums2中出现话,result记录
            if (myArr[num] == 1) {
                result.insert(num);
            }
        }
        return vector<int>(result.begin(), result.end());
    }
};
350、两个数组的交集2

这一题也是一样,因为前面的题目因此我特地观察了其元素的范围,也是小于1000,那么我们也可以用数组来解决这道题。另外一个要注意的点就是这里是需要计及重复的,因此我们需要记录每一个元素出现的次数,然后比对的时候如果都出现了就按照次数小者来插入。

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        int countNums1[1005] = {0};
        int countNums2[1005] = {0};
        for(int num:nums1){
            countNums1[num]++;
        }
        for(int num:nums2){
            countNums2[num]++;
        }
        vector<int>result;
        int temNum = 0;
        for(int i = 0; i < 1005; i++){
            if(countNums1[i] * countNums2[i] != 0){
                temNum = min(countNums1[i],countNums2[i]);
                for(int j = 0; j < temNum; j++){
                    result.push_back(i);
                }
            }
        }
        return result;
    }
};
202、快乐数

这道题需要从题目中提取出关键信息,即如果最终不为1,那么一定是无限循环的,那么就需要一个容器来记录出现过的所有求和结果,并且每次计算完毕就检查一遍,如果出现过,那么就已经循环,即不可能为1了。也就是说该容器是不可以出现重复数据的,那么我们选择集合,因此代码如下:

class Solution {
public:
    int getSum(int n){
        int sum = 0;
        while(n != 0){
            sum += (n % 10) * ( n % 10 );
            n = n / 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        unordered_set<int> mySet;
        while(1){
            int sum = getSum(n);
            if(sum == 1){
                return true;
            }
            if(mySet.find(sum) != mySet.end()){
                return false;
            }else{
                mySet.insert(sum);
            }
            n = sum;
        }
    }
};
1、两数之和

这一题最简单的思路当然是直接两层遍历来解决问题。但还有一种想法,就是把已经遍历过的数据存放起来,然后遍历之后的数据时,直接在存放之前数据的容器中寻找是否有能够匹配的,如果有则说明找到了答案,如果没有就也把当前数据存放进去。而由于返回的答案是索引,因此不仅仅要存放元素的值,更要存放元素的下标,而且比较的时候是拿值来比较的,因此元素的值作为键,下标作为value。因此代码如下:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> ValueIndex;
        vector<int>result;
        int temp;
        for(int i = 0; i < nums.size(); i++){
            temp = target - nums[i];  //差值,如果存在则匹配成功
            if(ValueIndex.find(temp) != ValueIndex.end()){
                //说明找到了
                result.push_back(i);
                result.push_back(ValueIndex[temp]);
                return result;
            }
            ValueIndex.insert(pair<int,int>(nums[i],i));//需要用pair函数转换后才能够插入
        }
        return result;
    }
};
454、四数相加2

这道题一开始可能比较难以理解,我最开始的思路是先遍历两个数组,然后计算两两元素之和,将其放入映射中,由于我们是需要统计出现次数不需要统计索引位置,因此键值对为 两数之和–出现次数。然后再遍历另外两个数组,计算两两数组之和,并在映射中寻找是否存在能让四数相加为0的,有则加上出现的次数。

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int>numCount;
        int result = 0;
        for(int i:nums1){
            for(int j: nums2){
                numCount[i+j]++;
            }
        }
        for(int i:nums3){
            for(int j:nums4){
                if(numCount[-1*(i+j)] != 0){  // 不为0即代表有出现过
                    result += numCount[-1*(i+j)];
                }
            }
        }
        return result;
    }
};

补充:标有注释那一行也可以替换为以下:

if(numCount.find(-1*(i+j)) != numCount.end())

因为迭代器end()是容器的最后一个元素的下一个位置,查找是从头开始,如果找到就返回对应元素的迭代器,否则就找到最后一个元素的下一个位置的迭代器作为终止条件并返回

15、三数之和

这一道题比较难,第一种使用哈希表的写法也是在代码随想录的指导下磕磕绊绊写完的。

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){
                //排序后最小的元素都大于0,那么不可能构成三个相加为0
                break;
            }
            if( i > 0 && nums[i] == nums[i - 1]){
                //去重
                continue;
            }
            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;
                }
                int c = 0 - (nums[i] + nums[j]);
                if(set.find(c) != set.end()){
                    result.push_back({nums[i],nums[j],c});
                    set.erase(c);
                }else{
                    set.insert(nums[j]);
                }
            }
        }
        return result;
    }
};

第二种的双指针法其实比哈希表更容易理解,主要思想就是固定一个指针,然后另外两个指针根据当前总和的大小去做移动,具体为:

  • 先对数组进行排序,这有利于我们通过大小关系来判断
  • 第一个指针i是作为遍历数组每一个元素的,因此最外层循环i遍历数组的每一个元素
  • 第二个指针 l e f t = i + 1 left=i+1 left=i+1 指向指针i的下一个元素,第三个指针 r i g h t = n u m s . s i z e ( ) − 1 right=nums.size()-1 right=nums.size()1指向最后一个元素
  • 对当前三个指针对应的数值进行相加,如果总和小于0,说明 l e f t left left需要右移来增大如果总和大于0,说明 r i g h t right right需要左移来减小
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(),nums.end());
        int i = 0;
        for(; i < nums.size() - 2; i++){
            if(nums[i] > 0){  //三个元素中最小的都大于0,那么肯定不行,后面也不用循环了后面全是大于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){
                    right--;
                }else if(nums[i] + nums[left] + nums[right] < 0){
                    left++;
                }
                else{
                    result.push_back(vector<int>{nums[i],nums[left],nums[right]});
                    while(right > left && nums[right] == nums[right-1]){
                        right--;
                    }
                    while(right > left && nums[left] == nums[left + 1]){
                        left++;
                    }
                    right--;
                    left++;
                }
            }
        }
        return result;
    }
};

另外一个问题就是去重的问题:

主要是对指针i的去重,这里去重的方法有两种,分别为;

if(i > 0 && nums[i] == nums[i-1]){
	continue;
}
//以及
if(nums[i] == nums[i+1]){
    continue;
}

那为什么不用第二种呢,因为重点是不可以有重复的三元组,但是三元组内的元素是可以重复的,如果数据为 − 1 , − 1 , 2 {-1,-1,2} 1,1,2第二种方法就直接跳过了第一个-1,就无法选出来的。第二种方法是已经确定了前面一个的情况,如果移动后还与前面一个相同那就需要去重了

其次是对指针 l e f t 、 r i g h t left 、right leftright的去重,仍然要掌握住重点不可以有重复的三元组,但是三元组内的元素是可以重复的,因此最好是找到了满足条件的三元组之后,再来对 l e f t 、 r i g h t left 、right leftright进行移动,如果这时候移动了发现相同,那么由于前面已经将这个满足条件的三元组加入了,现在发现相同就需要去重了

18、四数之和

这一题可以用和三数之和完全一样的思路,就是从固定一个变成固定两个而已,也就是两层循环。其次就是一些细节的处理:

  • 不可以判断nums[i] > target 就退出,因为这里目标不再是0,如果target取-10,而数据{-4,-4,-1,-1},那么就算第一个数就比target大,但由于后面都是负的,还是有可能达到target的
  • 第一层循环和第二层循环都要采用一样的去重方法以及判断是否应该break的方法
  • 计算四个数字之和时如果不加上long,将会在某个题目上面溢出

其他地方就都和三数之和一样,具体代码如下:

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>>result;
        sort(nums.begin(),nums.end());
        int left;
        int right;
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] > target && nums[i] >= 0 && target >= 0){  //新的判断退出的条件
                break;
            }
            if( i > 0 && nums[i] == nums[i-1]){ // 去重
                continue;
            }
            for(int j = i + 1; j < nums.size(); j ++){
                if(nums[i] + nums[j] > target && nums[i] + nums[j]>=0 && target >= 0){
                    break;
                }
                if( j > i+1 && nums[j] == nums[j-1]){
                    continue;
                }
                left = j + 1;
                right = nums.size() - 1;
                while(left < right){
                    if( (long)nums[i] + nums[j] + nums[left] + nums[right] < target){
                        left++;
                    }else if((long)nums[i] + nums[j] + nums[left] + nums[right] > target ){
                        right--;
                    }else{
                        result.push_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});
                        while(left < right && nums[right] == nums[right-1]){
                            right--;
                        }
                        while(left < right && nums[left] == nums[left + 1]){
                            left++;
                        }
                        right--;
                        left++;
                    }
                }
            }
        }
        return result;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值