day06-哈希表|242.有效的字母异位词、349. 两个数组的交集、202. 快乐数、1. 两数之和

参考:代码随想录

1. 哈希表理论基础

1.1 什么是 哈希表?

哈希表(英文名字为Hash table),哈希表是根据关键码的值而直接进行访问的数据结构。

1.2 哈希表能解决什么问题呢?

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

例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数

1.3 常见的三种哈希结构

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

  • 数组
  • set (集合)
  • map (映射)

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

1.3.1 set

在这里插入图片描述
unordered_set底层实现为哈希表,set 和 multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的;
如果需要集合是有序的,那么就用set;
如果要求不仅有序还要有重复数据的话,那么就用multiset。

1.3.2 map

在这里插入图片描述
unordered_map 底层实现为哈希表,map 和 multimap 的底层实现是红黑树,因此它们的key也是有序的。
map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的。

1.4 总结

总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!

2. 字符串相关题目

2.1 有效的字母异位词

链接:242. 有效的字母异位词

题目描述:

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

方法:

用数组存储出现过的字符,就是数组来作为哈希表,把字符映射到数组也就是哈希表的索引下标上

  • 遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 。
  • 在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
  • 最后record数组如果有的元素不为零0,return false。否则return true。

时空复杂度:

时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)

代码:

class Solution {
public:
    bool isAnagram(string s, string t) {
        if (s.size() != t.size()) {
            return false;
        }
        // 可以用数组存储出现过的字符,就是用数组来作为哈希表
        int record[26] = {0};
        for (int i = 0; i < s.size(); i++) {
            record[s[i] - 'a']++;
        }
        for (int i = 0; i < t.size(); i++) {
            record[t[i] - 'a']--;
        }
        for (int i = 0; i < 26; i++) {
            if (record[i] != 0) {
                return false;
            }
        }
        return true;
    }
};

2.2 赎金信

链接: 383. 赎金信

题目描述:

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)

方法:

本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成。

方法 同上,用字符数组作为哈希表,采用空间换取时间的哈希策略

  • 用一个长度为26的数组来记录magazine里字母出现的次数。
  • 然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。

代码:

 class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        // 可以用字符数组存储出现过的字符,就是字符数组作为哈希表
        int record[26] = {0};
        for (int i = 0; i < magazine.size(); i++) {
            record[magazine[i] - 'a']++;
        }
        for (int i = 0; i < ransomNote.size(); i++) {
            record[ransomNote[i] - 'a']--;
        }
        for (int i = 0; i < 26; i++) {
            if (record[i] < 0) {
                return false;
            }
        }
        return true;
    }
};

2.3 字母异位词分组

链接: 49.字母异位词分组

题目描述:

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

示例 1:

输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]

方法:
要将 字母异位词 组合在一起,
1. 判断两个字母是异位词:可以将字符串排序,如果两个字符串相等,就说明是异位词。
2. 将 字母异位词 组合在一起:使用map键值对存储每一组字母异位词。键为:排序好的字符串,值为:一组字母异位词列表。

PS: 由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。

对string的排序可以用algorithm中的sort:

string key = str;
sort(key.begin(), key.end());

代码:

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> ans; // 存放结果的数组
        unordered_map<string, vector<string>> mp;
        // 遍历字符串数组
        for (string str:strs) {
            string key = str;
            sort(key.begin(), key.end());
            // 如果map中没有就插入数据,如果有就返回对应的value
            mp[key].emplace_back(str);
        }
        // 遍历map,返回结果
        for (auto it = mp.begin(); it != mp.end(); it++) {
            ans.emplace_back(it->second);
        }
        return ans;
    }
};

2.4 找到字符串中所有字母异位词

链接: 438.找到字符串中所有字母异位词

题目描述:

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

示例 1:

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

方法一:双循环遍历
一个一个字符串试。
每次比较相同长度的主串与子串的频次:用数组来存储频次,将数组中元素一个一个比较。
时间复杂度O(mn)。

代码:

class Solution {
public:
   vector<int> findAnagrams(string s, string p) {
       // 如果s长度大于p长度,那么一定不存在
       if (p.size() > s.size()) {
           return vector<int>();
       }
       // 用数组表示哈希表,来存储p的字母以及出现的频次
       int pCount[26] = {0};
       for (int i = 0; i < p.size(); i++) {
           pCount[p[i] - 'a']++;
       }
       vector<int> res; // 存储结果
       // 遍历字符串s,每次取字符串p个长度,判断是否是它的异位词
       for (int i = 0; i < s.size() - p.size() + 1; i++) {
           // 记录新截取的字串的哈希表
           int sCount[26] = {0};
           for (int j = i; j < i + p.size(); j++) {
               sCount[(s[j] - 'a')]++;
           }
           bool flag = true;
           for (int j = 0; j < 26; j++) {
               if (sCount[j] != pCount[j]) {
                   flag = false;
                   break;
               }
           }
           if (flag) {
               res.emplace_back(i);
           }
       }
       return res;
   }
};

方法二:改进版 滑动窗口

  1. 在对主串和子串的比较中,随着滑动窗口的后移,每次只改变一个字符,所以不用每个子串都用一个数组来重新记录,用滑动窗口,修改去掉的字母的频次以及新加入的频次就行了。(这样就不用双层for循环,可以提高效率)
  2. 比较两个字符串的频次,可以用vector代替数组来保存,这样可以直接进行比较,不用数组一个一个比较。

代码:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        // 如果s长度大于p长度,那么一定不存在
        if (p.size() > s.size()) {
            return vector<int>();
        }
        // 用数组表示哈希表,来存储p的字母以及出现的频次
        // vector便于比较
        vector<int> sCount(26);
        vector<int> pCount(26);
        for (int i = 0; i < p.size(); i++) {
            sCount[s[i] - 'a']++; // 记录初始时滑动窗口内的字母频次
            pCount[p[i] - 'a']++;// 记录字符串p的字母频次
        }
        vector<int> res; // 存储结果
        // 判断初始时滑动窗口内的情况
        if (sCount == pCount) {
            res.emplace_back(0);
        }
        // 滑动窗口开始向后移动
        int left = 1;
        while (left <= s.size() - p.size()) {
            sCount[s[left - 1] - 'a']--; // 左边的第一个字母频次减一
            sCount[s[left + p.size() - 1] - 'a']++; // 右边新进来的字母频次加一
            // 判断是否相等
            if (sCount == pCount) {
                res.emplace_back(left);
            }
            left++;
        }
        return res;
    }
};

3. 数组相关题目

3.1 两个数组的交集

链接: 349.两个数组的交集

题目描述:

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

示例 1:

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

方法:

哈希表保存元素。

unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set

遍历两个数组,本来是想一个数组中的元素存放在集合中,然后再遍历另一个数组,如果数组中的数在集合中,那么就将这个数加入结果数组中,但是这样不能保证数组中结果唯一不重复,
所以就先用集合保存结果,再转为数组返回。

代码:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> set1, res;
        for (auto num :nums1) {
            set1.insert(num);
        }
        for (auto num:nums2) {
            if (set1.count(num)) {
                res.insert(num);
            }
        }
        vector<int> ans;
        // 把集合中的数存放到数组中
        for (auto num:res) {
            ans.emplace_back(num);
        }
        return ans;
    }
};

3.2 两个数组的交集 II

链接: 350.两个数组的交集 II

题目描述:

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

示例 1:

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

方法一:

哈希数组,用空间换时间。

代码:

class Solution {
public:
   vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
       vector<int> ans;
       int record[1005] = {0};
       for (int num: nums1) {
           record[num]++;
       }
       for (int num: nums2) {
           if (record[num]) {
               ans.emplace_back(num);
               record[num]--;
           }
       }
       return ans;
   }
};

方法二:

哈希表,用map键值对保存出现的数字以及个数。

代码:

class Solution {
public:
   vector<int> intersect(vector<int> &nums1, vector<int> &nums2) {
       // 为了减少空间,长度小的数字保存到map中
       if (nums1.size() > nums2.size()) {
           return intersect(nums2, nums1);
       }
       vector<int> ans;
       unordered_map<int, int> mp; // 保存出现的数字以及个数
       for (int num: nums1) {
           mp[num]++;
       }
       for (int num: nums2) {
           if (mp.count(num)) {
               ans.emplace_back(num);
               mp[num]--;
               if (mp[num] == 0) {
                   mp.erase(num);
               }
           }
       }
       return ans;
   }
};

4.数字相关题目

4.1快乐数

链接: 202. 快乐数

题目描述:

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

示例 1:

输入:n = 19
输出:true
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

方法:
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现。

使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。

代码:

class Solution {
public:
   // 判断下一个数是什么
   int getNextNum(int n) {
       int sum = 0;
       while (n) {
           int num = n % 10;
           sum += num * num;
           n = n / 10;
       }
       return sum;
   }
   
   bool isHappy(int n) {
       // 如果是快乐数,那么最后就会变成1,如果不是快乐数,最后会出现循环
       unordered_set<int> set; // 用集合来判断是否出现循环
       set.insert(n);
       while (true) {
           n = getNextNum(n);
           if (n == 1) {
               return true;
           }
           if (set.count(n)) {
               return false;
           }
           set.insert(n);
       }
   }
};

4.2 两数之和

链接: 1.两数之和

题目描述:

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

方法一:暴力枚举,双层循环

时间复杂度O(n^2)

当我们使用遍历整个数组的方式寻找 target - x 时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x

代码:

class Solution {
public:
   vector<int> twoSum(vector<int>& nums, int target) {
       int n = nums.size();
       for (int i = 0; i < n; ++i) {
           for (int j = i + 1; j < n; ++j) {
               if (nums[i] + nums[j] == target) {
                   return {i, j};
               }
           }
       }
       return {};
   }
};

方法二:哈希表

我本来的想法:(不可行)

  1. 首先把数组中的数都放进哈希表中,我刚开始想用set,但是set不知道下标是多少,所以只能用map
  2. 然后再遍历数组,看和为target的两个数是否都在集合中,如果数组中有两个一样的数,那么在map中找下标的时候,不好找,没有能直接调用的方法,所以这种方法不行。

改进版:

  1. 使用map存储数组中的元素,但是 (key,value)是:(元素,下标)
  • 为什么(key,value)不是:(下标,元素)?
    因为我们后面需要在map中找的是元素而不是下标,所以用key来存元素。
  1. 每遍历数组中的一个元素就插入一个元素到map,而不是一开始就先把全部的元素都插入到map中。
    这样做的目的是为了避免把相同的两个数字都插入到map中,后面查找元素的时候找不到第二个元素。

先在map中找target - num,如果没有就把当前数字num插入到map中,如果map中有,就返回元素对应下标。

代码:

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中寻找是否有匹配的元素
           auto it = map.find(target - nums[i]);
           if (it != map.end()) {
               return {it->second, i};
           }
           // 如果没找到匹配对,就把访问过的元素和下标加入到map中
           map.insert(make_pair(nums[i], i));
       }
       return {};
   }
};

4.3 四数相加II

链接:454.四数相加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

方法:哈希法
一采用分为两组,HashMap 存一组,另一组和 HashMap 进行比对。

这样的话情况就可以分为三种:

  • HashMap 存一个数组,如 A。然后计算三个数组之和,如 BCD。时间复杂度为:O(n)+O(n^3),得到 O(n^3).
  • HashMap 存三个数组之和,如 ABC。然后计算一个数组,如 D。时间复杂度为:O(n^3)+O(n),得到 O(n^3).
  • HashMap存两个数组之和,如AB。然后计算两个数组之和,如 CD。时间复杂度为:O(n2)+O(n2),得到 O(n^2).

根据第二点我们可以得出要存两个数组算两个数组。
我们以存 AB 两数组之和为例。首先求出 A 和 B 任意两数之和 sumAB,以 sumAB 为 key,sumAB 出现的次数为 value,存入 hashmap 中。
然后计算 C 和 D 中任意两数之和的相反数 sumCD,在 hashmap 中查找是否存在 key 为 sumCD。

算法时间复杂度为 O(n^2)

代码:

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3,
                     vector<int>& nums4) {
        // 先用哈希表存储两个数组的和,再计算另外两个数组的和,
        // 这样就转化为 两数之和,可以用哈希表来做
        unordered_map<int, int> map; // (key,value)是(元素和,出现次数)
        for (int num1 : nums1) {
            for (int num2 : nums2) {
                map[num1 + num2]++;
            }
        }

        int count = 0; // 统计总次数
        for (int num1 : nums3) {
            for (int num2 : nums4) {
                if (map.find(-(num1 + num2)) != map.end()) {
                    count += map[-(num1 + num2)];
                }
            }
        }
        return count;
    }
};
  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值