【笔记】哈希表

本系列总计六篇文章,是 基于STL实现的笔试题常考七大基本数据结构 该文章在《代码随想录》和《labuladong的算法笔记》题目中的具体实践,每篇的布局是这样的:开头是该数据结构的总结,然后是在不同场景的应用,以及不同的算法技巧。本文是系列第三篇,介绍了哈希表的相关题目,重点是要掌握STL的set和map,注意N数之和问题,用双指针最方便。

下面文章是在《代码随想录》和《labuladong的算法笔记》题目中的具体实践:
【笔记】数组
【笔记】链表
【笔记】哈希表
【笔记】字符串
【笔记】栈与队列
【笔记】二叉树

0、总结

  • 哈希表用来快速判断一个元素是否出现在集合里

  • 一般常用数组、unordered_setunordered_map 这三种数据结构实现哈希表,后两者的底层实现是哈希表,无序、数值(key)不可重复、数值(key)不可修改、查询和增删效率都是 O(1)

  • set,multiset,map,multimap底层是红黑树,当要求key有序、重复时,可以选用

  • 两数、三数、四数之和问题,用 双指针法 最方便

1、set 作为哈希表

349. 两个数组的交集 - 力扣(LeetCode)

思路:输出交集,要求去重、不考虑输出结果的顺序,因此选用unordered_set

注意:set和vector互相转化可以用构造函数直接实现,不需要遍历;set增加元素用insert;用auto更方便

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        vector<int> result;
        // 要去重,所以用unordered_set
        unordered_set<int> result_set, nums1_set;
        // unordered_set<int> nums_set(nums1.begin(), nums1.end());
        for (int i : nums1)
            nums1_set.insert(i);
        for (int i : nums2) {
            if (nums1_set.find(i) != nums1_set.end()) {
                result_set.insert(i);
            }
        }
        // return vector<int>(result_set.begin(), result_set.end());
        for (auto it = result_set.begin(); it != result_set.end(); it++)
            result.push_back(*it);
        return result;
    }
};

202. 快乐数 - 力扣(LeetCode)

思路:用set判断集合中的某个元素是否出现过,若出现过,代表有循环,此数并非快乐数,应及时return

注意:按位取数的写法应牢记,n不为0的情况下,不断地先%10,再/10,这样可以低位到高位取数

class Solution {
public:
    bool isHappy(int n) {
        unordered_set<int> set;
        while(1) {
            int sum = getSum(n);
            if (sum == 1) return true;
            if (set.find(sum) != set.end()) {
                return false;
            } else {
                set.insert(sum);
            }
            n = sum;
        }
    }
    int getSum(int n) {
        int sum = 0;
        while (n) {
            sum += pow(n % 10, 2);
            n /= 10;
        }
        return sum;
    }
};

2、map 作为哈希表

350. 两个数组的交集 II - 力扣(LeetCode)

思路:与349的区别是,不去重,若公共元素是a,返回结果中a出现的次数,应与a在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值),要用unordered_map

进阶:

  • 如果给定的数组已经排好序呢?你将如何优化你的算法?用双指针只需 O(n) 的时间复杂度

  • 如果 nums1 的大小比 nums2 小,哪种方法更优?较小的数组用hash存储,然后在另一个数组中寻找

  • 如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

    还是较小的数组用hash存储,然后在另一个数组中,每次读取出一部分数据进行寻找

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        vector<int> result;
        // 题目不要求去重
        unordered_map<int, int> map;
        for (int i : nums1)
            map[i]++;
        for (int i : nums2) {
            // 若是双方的重复元素,输出
            if (map[i] > 0) {
                result.push_back(i);
                map[i]--;
            }
        }
        return result;
    }
};

242. 有效的字母异位词 - 力扣(LeetCode)

思路:t 是否是 s 的字母异位词,若 st 中每个字符出现的次数都相同,则称 st 互为字母异位词。本题需要统计字符及其对应出现次数,用map,由于不要求key有序,最终选用unordered_map

或者是:比较字符串排序后的结果是否相等

class Solution {
public:
    bool isAnagram(string s, string t) {
        unordered_map<char, int> map;
        for (char c : s)
            map[c]++;
        for (char c : t)
            map[c]--;
        // for (auto it = map.begin(); it != map.end(); it++) {
        for (unordered_map<char, int>::iterator it = map.begin(); it != map.end(); it++) {
            // 只要有次数不是0的,说明不是异位词
            if (it->second != 0)
                return false;
        }
        return true;
    }
};

383. 赎金信 - 力扣(LeetCode)

思路:判断 ransomNote 能不能由 magazine 里面的字符构成,与上题相似,magazine 中的每个字符只能在 ransomNote 中使用一次,也就是说magazine可以有剩余。当magazine中元素个数不够或是不含ransomNote中的字符时,返回false。

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        unordered_map<char, int> map;
        for (char c : magazine)
            map[c]++;
        for (char c : ransomNote)
            map[c]--;
        for (auto it = map.begin(); it != map.end(); it++) {
            if (it->second < 0)
                return false;
        }
        return true;
    }
};

49. 字母异位词分组 - 力扣(LeetCode)

思路:本题要将相同的异位词总结在一起,异位词排序后的结果是相同的,所有想到将 字符串排序后的结果作为key,其原本的值作为value,然后分组输出,可以按任意顺序返回结果列表,选用unordered_map

注意:map的value不为int时候的插入语法,用push_back

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> result;
        unordered_map<string, vector<string>> map;
        for (string str : strs) {
            // 保存原来的值
            string s = str;
            // 当前str排序
            sort(str.begin(), str.end());
            // 排序后的值作为key,插入原来的值
            map[str].push_back(s);
        }
        for (auto it = map.begin(); it != map.end(); it++)
            result.push_back(it->second);
        return result;
    }
};

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

思路:要找的异位词是子串,联想到用滑动窗口+双指针,且本题中的窗口大小固定,始终是p.size()。利用unordered_map,记录need需要的字符及其数量和window窗口中符合要求的字符及其数量

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> result;
        int valid = 0;
        int left = 0, right = 0;
        unordered_map<char, int> window, need;
        for (char c : p)
            need[c]++;
        while (right < s.size()) {
            char c = s[right];
            right++;
            if (need.count(c)) {
                window[c]++;
                if (window[c] == need[c])
                    valid++;
            }
            while (right - left >= p.size()) {
                if (valid == need.size())
                    result.push_back(left);
                char d = s[left];
                left++;
                if (need.count(d)) {
                    if (window[d] == need[d])
                        valid--;
                    window[d]--;
                }
            }
        }
        return result;
    }
};

454. 四数相加 II - 力扣(LeetCode)

思路:利用map,遍历前两个数组,把两两之和 以及 和对应出现的次数写入map,然后遍历后两个数组,看0-(c+d)是否在map中,若有,则将其对应出现的次数累加

注意:本题只能计算出有多少个元组满足条件,与n数之和要求的返回下标的题目,完全不同

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int, int> map;
        for (int a : nums1)
            for (int b : nums2)
                map[a + b]++;
        int count = 0;
        for (int c : nums3)
            for (int d : nums4) {
                auto it = map.find(0 - c - d);
                if (it != map.end()) {
                    count += it->second;
                }
            }
        return count;
    }
};

3、n Sum 问题 - 排序+双指针

1. 两数之和 - 力扣(LeetCode)

思路:不用set用map是因为要返回下标

陷阱:排序+双指针法不能使用!因为1.两数之和要求返回的是索引下标,而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。但是如果要求返回的是数值的话,就可以使用双指针法了

注意:map中insert一对值,必须是以pair的形式插入

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 it = map.find(target - nums[i]);
            if (it != map.end())
                return {i, it->second};
            else
                map.insert(pair<int, int>(nums[i], i));
        }
        return {};
    }
};

167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)

思路:数组已经有序,数值大小和下标大小正相关关系,可以用双指针法

注意:换算index和数组下标

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int left = 0, right = numbers.size() - 1;
        while (left < right) {
            if (numbers[left] + numbers[right] > target) right--;
            else if (numbers[left] + numbers[right] < target) left++;
            else {
                // 左右指针记录数组下标,需要转换成index
                return {left + 1, right + 1};
            }
        }
        return {};
    }
};

15. 三数之和 - 力扣(LeetCode)

思路:map不推荐,因为包含了复杂的去重逻辑。先排序!两数之和,直接左右双指针即可;三数之和,i和左右双指针,一层for循环控制i的遍历;四数之和,i,j以及左右双指针,两层for循环分别控制i、j的遍历…

注意:由于本题求和为0,因此nums[i] > 0可以直接return;a的去重写法;b、c的去重,放在前面if-else分支也可以。保证了三元组之间没有重复,但三元组之内可以重复

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) return result;
            // a去重
            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++;
                // 找到三数之和为0
                else {
                    result.push_back({nums[i], nums[left], nums[right]});
                    // b、c去重,放在前面if分支也可以
                    while (left < right && nums[right] == nums[right - 1]) right--;
                    while (left < right && nums[left] == nums[left + 1]) left++;
                    right--;
                    left++;
                }
            }
        }
        return result;
    }
};

18. 四数之和 - 力扣(LeetCode)

思路:先排序!两层for循环分别控制i、j的遍历,每层开始时候都需要剪枝

注意:

1、本题的和为target,不是0,剪枝的逻辑有区别

// 一级剪枝

if (nums[i] > target && nums[i] >= 0) break;

// 二级剪枝

if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;

2、对a、b的去重写法没有区别;

3、双指针写法和收缩写法没有区别

4、防止溢出,不要写成四数之和形式

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++) {
            // 四数之和,是i,j还有左右双指针
            // 对i正数要剪枝
            if (nums[i] > target && nums[i] >= 0) break; // 这里使用break,统一通过最后的return返回
            // 对nums[i]去重
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            for (int j = i + 1; j < nums.size(); j++) {
                // 对j进行2级剪枝
                if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;
                // 对nums[j]去重
                if (j > i + 1 && nums[j] == nums[j - 1]) continue;
                int left = j + 1;
                int right = nums.size() - 1;
                while (left < right) {
                    // 小心溢出
                    if (nums[i] + nums[j] > target - nums[left] - nums[right]) right--;
                    else if (nums[i] + nums[j] < target - nums[left] - nums[right]) left++;
                    else {
                        result.push_back({nums[i], nums[j], nums[left], nums[right]});
                        // 对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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值