《代码随想录》学习笔记:哈希表合集

前言

  一刷代码随想录,今天的内容是 哈希表。所有题目均源自LeetCode。
  欢迎大家在评论区留言发表自己的看法,如文章有不妥之处,请批评指正。



一、哈希表简介

  哈希表利用空间换时间,将索引查找时间降到 O(1),常见的哈希表结构有三种:数组、set(集合)、map(映射)。

  有关哈希表具体的基础理论知识,请看卡哥的讲解 哈希表理论基础


二、练习题目

  1. 242. 有效的字母异位词
  2. 383. 赎金信
  3. 349. 两个数组的交集
  4. 202. 快乐数
  5. 1. 两数之和
  6. 454. 四数相加 II
  7. 15. 三数之和
  8. 18. 四数之和

三、源码剖析

1、242. 有效的字母异位词|★☆☆☆☆

第一版:哈希表,利用数组表示哈希表

时间复杂度:O(n),空间复杂度:O(1)

class Solution {
public:
    bool isAnagram(string s, string t) {
        // 哈希表,利用数组表示哈希表
        // 如果是字母异位词,则s和t的长度肯定是相等的,不等的直接返回false
        
        int lenS = s.size(), lenT = t.size();
        if (lenS != lenT) return false;
        // 26个英文字母,数组长度>=26即可,一定需要初始化
        int hash[26] = {0};                 
        // 使用memset初始化也可以
        // memset(hash, 0, sizeof(hash));   
        for (int i = 0; i < lenS; ++i) {
        	// 将s的元素放入,'a'的放入位置为下标0处, 减'a'是为了求出其他字符的放入位置
            ++hash[s[i] - 'a'];             
        }
        for (int i = 0; i < lenT; ++i) {
            --hash[t[i] - 'a'];
            // t中字母在s中没有出现,s中某个字母比t中多的情况也包含在此
            // 因为s和t的长度一致,s中某字母比t中多n个,则t中就有n个其他字母
            // 这n个其他字母最终会造成 hash[t[i] - 'a'] < 0 的结果。
            if (hash[t[i] - 'a'] < 0) return false;    
        }
        return true;
    }
};

2、383. 赎金信|★☆☆☆☆

第一版:数组哈希表

时间复杂度:O(n),空间复杂度:O(1)

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
       // 哈希表
       // 首先, magazine.size() >= ransomNote.size()
       // 然后,第一遍遍历将 magazine 映射到哈希表中统计每个字符出现的次数
       // 最后,第二遍遍历时将 ransomNote 的每个字符在 hash 表中减去次数
       // 如果造成了 hash[i]<0的情况,则false

       int lenR = ransomNote.size(), lenM = magazine.size();
       if (lenR > lenM) return false;
       int hash[26] = {0};
       for (int i = 0; i < lenM; ++i) {
           ++hash[magazine[i] - 'a'];
       }
       for (int i = 0; i < lenR; ++i) {
           --hash[ransomNote[i] - 'a'];
           if (hash[ransomNote[i] - 'a'] < 0) return false;
       }
       return true;
    }
};

3、349. 两个数组的交集|★☆☆☆☆

第一版:哈希表

时间复杂度:O(n),空间复杂度:O(1)

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        // 哈希表
        // 数组的交集,就是两个数组中将相同元素提取出来组成一个新的集合
        // 输出结果中的每个元素一定是 唯一 的,即只要元素相同即可,不考虑个数

        int len1 = nums1.size(), len2 = nums2.size();
        // 数组长度>=元素的种类,多一点可以避免有时候算错了造成越界
        int hash[1010] = {0};                   
        vector<int> ret;
        for (int i = 0; i < len1; ++i) {      
            hash[nums1[i]] = 1;
        }
        for (int i = 0; i < len2; ++i) {       
            if (hash[nums2[i]]-- > 0) ret.push_back(nums2[i]);
        }
        return ret;
    }
};

4、202. 快乐数|★★☆☆☆

第一版:不知道哈希表怎么使用,看了一下题解提示

class Solution {
public:
    bool isHappy(int n) {
        // 看到示例,首先想到的是模拟,如果闷头去模拟,可能会陷入无限循环
        // 无限循环,就是平方和出现了重复的情况,只要有一次重复出现,则后面均会重复,陷入循环
        // 那么就可以考虑使用hash表来记录是否无限循环了
        unordered_set<int> set;
        while (n != 1) {
        	// 遇到重复
            if (set.find(n) != set.end()) return false;     
            set.insert(n);
            int temp = n;
            n = 0;
            while (temp > 0) {
                n += pow(temp % 10, 2);
                temp /= 10;
            }
        }
        return true;
    }
};

5、1. 两数之和|★☆☆☆☆

第一版:map哈希表

时间复杂度:O(n),空间复杂度:O(n)

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        // 哈希表
        // map的key存nums[i],value存下标
        std::unordered_map<int, int> map;                         
        vector<int> ret;
        for (int i = 0; i < nums.size(); ++i) {
        	// 当前hash表中有target - nums[i]的数,即找到了两个整数相加等于target
            if (map.find(target - nums[i]) != map.end()) {   
            	// 存入另一个数的下标
                ret.push_back(map[target - nums[i]]);  
                // 存入当前数的下标      
                ret.push_back(i);                            
                return ret;
            }
            map[nums[i]] = i;
        }
        return ret;
    }
};

第二版:优化第一版代码,看了题解

时间复杂度:O(n),空间复杂度:O(n)

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        // 优化第一版代码 
        unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); ++i) {
            auto iter = map.find(target - nums[i]);
            if (iter != map.end()) {
                return {iter->second, i};
            }
            map[nums[i]] = i;
        }
        return {};
    }
};

6、454. 四数相加 II|★★☆☆☆

第一版:结果错误。

原因:没想清楚 a+b 和 -(c+d) 的个数关系,在这里是hash表中出现一次 -(c+d) 后就算一次,而且将hash表中的个数都-1了,是错误的做法。其实画画图就能知道,一个 -(c+d) 可以将上面的全部 a+b 都组成元组,因此出现一次 -(c+d) 就需要将hash表中 a+b 的个数累加起来。

第二版:看了题解,解决了第一版的问题

时间复杂度:O(n^2),空间复杂度:O(n)

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        // 类比两数之和,使用哈希表
        // 两个for循环,将a+b算出来,并且存入哈希表,key存a+b的值,value存a+b的值出现的次数
        // 然后再两个for循环,计算c+d,每计算一次就看-(c+d)的值是否在hash表中,如果在,则ret += map[-(c+d)] 
        //(其实画画图就能知道,一个 -(c+d) 可以将上面的全部 a+b 都组成元组,因此出现一次 -(c+d) 就需要将hash表中 a+b 的个数累加起来)
        int ret = 0;
        unordered_map<int, int> map;
        for (int a : nums1) {
            for (int b : nums2) {
                ++map[a + b];
            }
        }
        for (int c : nums3) {
            for (int d : nums4) {
                int sum = 0 - (c+d);
                if (map[sum] > 0) {
                    ret += map[sum];
                }
            }
        }
        return ret;
    }
};

7、15. 三数之和|★★☆☆☆

第一版:哈希表,主要在于怎么去重,没写出来,一直报错

第二版:双指针,去重也是难点,b、c去重部分还是看了题解

时间复杂度:O(n^2),空间复杂度:O(1)

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 双指针加一个固定指针
        // 先排序
        // 再固定指针i,双指针j(left)和k(right),j=i+1,k=nums.size() - 1
        // 每一次遍历,首先固定i不动,然后j和k向中间移动,每次都计算 nums[i]+nums[j]+nums[k]的值
        // 如果大于0,则k向左移动;如果小于0,则j向右移动;
        // 如果等于0,则将nums[i]、nums[j]、nums[k]存入ret,j和k都移动
        // 同时,在j,k移动时,如果移动后指向的元素与先前元素相等,则继续移动j,k
        // 最后,如果nums[i]>0,则直接break;否则遍历到末尾结束
        vector<vector<int>> ret;
        int n = nums.size();
        sort(nums.begin(), nums.end());
        for (int i = 0; i < nums.size(); ++i) {
            if (nums[i] > 0) {
                break;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int left = i + 1, right = n - 1;
            int a = nums[i];
            // 不能等,因为 j != k
            while (left < right) {      
                int b = nums[left], c = nums[right];
                if (a + b + c == 0) {
                    ret.push_back({a, b, c});
                    --right;
                    ++left;
                    while (left < right && nums[right] == nums[right + 1]) {
                        --right;
                    }
                    while (left < right && nums[left] == nums[left - 1]) {
                        ++left;
                    }
                }else if (a + b + c > 0) {  // 向左移动k
                    --right;
                }else {                     // 向右移动j
                    ++left;
                }
            }
        }
        return ret;
    }
};

8、18. 四数之和|★★★☆☆

第一版:看了题解,然后自己写的时候去重部分还是有问题

时间复杂度:O(n^3),空间复杂度:O(1)

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        // 双指针,实际是4指针,但是每次固定两个指针
        // 方法与三数之和一样,只是在剪枝的时候要注意
        // 五数之和、六数之和等都是一样的用双指针
        vector<vector<int>> ret;
        sort(nums.begin(), nums.end());
        int n = nums.size();
        for (int a = 0; a < n; ++a) {
            // 1级剪枝
            // 与三数之和有点区别,因为这里target不确定,如果为负数则直接nums[a] > target就会漏掉结果,如[-4,-3,-2,1,0],target = -9
            // 因为nums[a]是四个数中最小的,当nums[a] >= 0时,nums[a] > target后面就一定不会有满足条件的了,
            if (nums[a] > target && nums[a] >= 0) {  
                break;
            }
            // nums[a] 去重
            if (a > 0 && nums[a] == nums[a - 1]) {  // a>0是去掉a==0的边界情况
                continue;
            }

            for (int b = a + 1; b < n; ++b) {
                // 2级剪枝
                if (nums[a] + nums[b] > target && nums[a] + nums[b] >= 0) {
                    break;
                }
                // nums[b] 去重
                if (b > a + 1 && nums[b] == nums[b - 1]) {
                    continue;
                }
                int left = b + 1, right = n - 1;
                while (left < right) {
                    // 分析数据大小,最大可能为4个 10^9 相加,用int就溢出了
                    if ((long) nums[a] + nums[b] + nums[left] + nums[right] > target) {
                        --right;
                    }else if ((long) nums[a] + nums[b] + nums[left] + nums[right] < target) {
                        ++left;
                    }else {
                        ret.push_back({nums[a], nums[b], nums[left], nums[right]});
                        ++left;
                        --right;
                        // nums[left] 去重
                        while (left < right && nums[left] == nums[left - 1]) ++left;
                        // nums[right] 去重
                        while (left < right && nums[right] == nums[right + 1]) --right;
                    }
                }
            }
        }
        return ret;
    }
};

总结

   ( 1 ) (1) (1) 哈希表常在 数组 中使用,字符串也是一种数组。
   ( 2 ) (2) (2) 一道题能使用哈希表的话,首先分析一下数据量,能使用数组 arr[length] 做哈希表就尽量使用数组做哈希表,要比容器 map、set 更简单有效。
   ( 3 ) (3) (3) C++常用的哈希表容器:unordered_map/multimap、unordered_set/multimap,这四个的底层就是用的 hash table。
   ( 4 ) (4) (4) 三数之和、四数之和,如果还有五数之和、六数之和……,他们的思路都是一样的,只是需要注意 剪枝、去重、相加后是否会溢出 三点。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值