:::tips
作者:LemonCat🐱
时间:2023-8-1
:::
第三章 - 哈希表 part01
:::info
- 哈希表理论基础
- 242.有效的字母异位词
- 349.两个数组的交集
- 202.快乐数
-
理论基础
建议:大家要了解哈希表的内部实现原理,哈希函数,哈希碰撞
- 什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
- 这句话很重要,大家在做哈希表题目都要思考这句话。
文章讲解:代码随想录 (programmercarl.com)
常见的三种哈希结构
数组
- set - 集合
- map - 映射
242.有效的字母异位词
建议: 这道题目,大家可以感受到 数组 用来做哈希表 给我们带来的遍历之处。 题目链接:242. 有效的字母异位词 - 力扣(LeetCode) 文章讲解/视频讲解:代码随想录 (programmercarl.com)
方法一 - 哈希表法 - 数组
这个题可以取巧 - 因为字符的范围是小写字母,所以可以用数组当哈希表,
minuscule[ch - 'a']表示字符ch的出现次数,这样做更加省时间省内存
- 暴力解法:两层 for 循环,同时记录字符是否重复出现
- 哈希表法:
- 定义一个数组 minuscule 用来上记录字符串 s 里字符出现的次数
- 把字符映射到数组也就是哈希表的索引下标上
- 映射
minuscule[s.charAt(i) - 'a'] - 因为字符 a 到字符 z 的 ASCII 是 26 个连续的数值,所以字符 a 映射为下标 0,相应的字符 z 映射为下标 25
- 映射
- 遍历 字符串 s 将出现元素 +1 -> 这样就将字符串 s 中字符出现的次数,统计出来了。
- 遍历 字符串 t 将出现元素 -1
- 最后检查 minuscule 数组
- 如果有元素不为零 0,说明字符串 s 和 t 一定是谁多了字符或者谁少了字符 -> 不符合异位词
return false - 若全为零 则是异位词
return true
- 如果有元素不为零 0,说明字符串 s 和 t 一定是谁多了字符或者谁少了字符 -> 不符合异位词
/*** 方法一 - 哈希表法*/class Solution {public boolean isAnagram(String s, String t) {// 小写字母哈希表int[] minuscule = new int[26];// 遍历 s 字符串 将对的字符出现的个数 记录数组中(+)for (int i = 0; i < s.length(); i++) {minuscule[s.charAt(i) - 'a']++; // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了}// 遍历 t 字符串 将对的字符出现的个数 记录数组中(-)for (int j = 0; j < t.length(); j++) {minuscule[t.charAt(j) - 'a']--;}for (int k : minuscule) {if (k != 0) {return false;// minuscule 数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。}}return true; // minuscule 数组所有元素都为零0,说明字符串s和t是字母异位词}}
- 时间复杂度: O(n)
- 空间复杂度: O(1)
349. 两个数组的交集
建议:本题就开始考虑 什么时候用 set 什么时候用数组,本题其实是使用 set 的好题 但是后来力扣改了题目描述和 测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用 set。 题目链接:349. 两个数组的交集 - 力扣(LeetCode) 文章讲解/视频讲解:代码随想录 (programmercarl.com)
思路
- 输出结果中的每个元素一定是唯一的 -> 结果是去重的, 同时可以不考虑输出结果的顺序 -> 用 set 集合会更好一点
- 题目描述中数值范围比较小 -> 用数组解决更好一点
- 双 set
- 优点:自动帮助我们去重;在数据量较大时 更节省空间
- 缺点:直接使用 set 不仅占用空间比数组大,而且速度要比数组慢
- 双数组
- 优点:由于题中数值范围较小 -> 用数组速度会更快
- 缺点:面对数据量较大 或 数据跨度较大(稀疏矩阵)-> 占用空间较大
方法一 - 双 set
- 用一个哈希集合存放一个数组中的元素,会自动去重。
- 遍历另一个数组,当访问到的元素出现在哈希集合中时 -> 将其放到结果集中 resSet
- 将 resSet 集合 -> arr 数组
/*** 方法一 - 两个 set + 一个数组*/class Solution {public int[] intersection(int[] nums1, int[] nums2) {// 若两个数组传入的有一个是 null -> 提前返回if (nums1 == null || nums2 == null) {return null;}Set<Integer> hashSet = new HashSet<>(); // Hash 表Set<Integer> resSet = new HashSet<>(); // 结果集// 将nums中的值 存储到 hashSet 中for (int n1 : nums1) {hashSet.add(n1);}// 将nums2中的值 与 hashSet 中的值作比较 若存在则放到结果集 resSet 中(可以帮我们自动去重)for (int n2 : nums2) {if (hashSet.contains(n2)) {resSet.add(n2);}}// 将 set 转换成数组int[] arr = new int[resSet.size()];int j = 0;for (Object o : resSet) {arr[j++] = (int) o;}return arr;}}
改进
- 将 set 转换成数组
- 原来的写法
int[] arr = new int[resSet.size()];int j = 0;for (Object o : resSet) {arr[j++] = (int) o;}
- 更优雅的写法
- 具体详情以后有时间在研究
int[] arr = resSet.stream().mapToInt(x -> x).toArray();
方法二 - 双数组
/*** 方法二 - 双数组*/class Solution {public int[] intersection(int[] nums1, int[] nums2) {// 若两个数组传入的有一个是 null -> 提前返回if (nums1 == null || nums2 == null) {return null;}int[] hashArr = new int[1001]; // Hash 表int[] resArr; // 结束数组// 遍历 num1 将其放到对应的hash表中for (int i : nums1) {hashArr[i]++;}// 遍历 num2 若发现便利的对象在hash表中已经存在了 证明是交集 -> 存入结果集中Set<Integer> resSet = new HashSet<>();for (int j : nums2) {if (hashArr[j] != 0) {resSet.add(j);}}resArr = new int[resSet.size()];// 将结果集 -> 转换成 数组int j = 0;for (Integer res : resSet) {resArr[j++] = (int) res;}return resArr;}}
202. 快乐数
建议:这道题目也是 set 的应用,其实和上一题差不多,就是套在快乐数一个壳子 题目链接:202. 快乐数 - 力扣(LeetCode) 文章讲解:代码随想录 (programmercarl.com)
方法 - set 集合
- 有限大小的整数都可以在有限次数的替换运算中压缩到有限位数,所以如果不是快乐数一定会出现循环
- 那么对一个数不断做迭代替换过程中如果出现了不在 1 处的循环则一定不是快乐数
- 所以用
Set存放过程中所有的状态- 每次替换就在这个
Set中查询是否到达过,如果到达过则返回false
- 每次替换就在这个
/*** 方法 - set 集合*/class Solution {public boolean isHappy(int n) {Set<Integer> hashSet = new HashSet<>(); // 记录每次平方和结果集while (true) {int res = 0; // 每次平方和int[] arrN = getNum(n); // 传入数 返回数每一位数组成的数组// 计算每个数平方后的和for (int i : arrN) {res += i * i;}// 如果结果是 1 就是快乐数if (res == 1) {return true;}// 结果集中存在 结束 -> 发生重复 -> 不是快乐数if (hashSet.contains(res)) {return false;}hashSet.add(res);n = res;}}/*** 传入数 返回数每一位数组成的数组** @param n* @return*/int[] getNum(int n) {int length = ((Integer) n).toString().length(); // 获取位数的长度int[] arrN = new int[length]; // 返回的数组for (int i = 0; i < length; i++) {arrN[i] = n % 10;n /= 10;}return arrN;}}
1. 两数之和
建议:本题虽然是 力扣第一题,但是还是挺难的,也是 代码随想录中 数组,set 之后,使用 map 解决哈希问题的第一题。建议大家先看视频讲解,然后尝试自己写代码,在看文章讲解,加深印象。 题目链接 1. 两数之和 - 力扣(LeetCode) 文章讲解/视频讲解:代码随想录 (programmercarl.com)
思路
- 访问到一个数
nums[i],与之和为target的数只能是target-nums[i] - 将每次便利的数存入 Map 中
- 注意:K-V 分别对应的值!!!
- K:数值
- V:索引
- 因为我们最终是要找对应数值的索引 ->
arr[1] = map.get(target - nums[i])
- 注意:K-V 分别对应的值!!!
- 然后找
target-nums[i]是否在 Map 中出现过 -> 将当前索引 和 将找到的 数值对应的 V 存入数组 - 返回数组~
方法一 - Map 方法
class Solution {public int[] twoSum(int[] nums, int target) {HashMap<Integer, Integer> map = new HashMap<>();int[] arr = new int[2];if (nums == null || nums.length == 0) {return arr;}// K - V// K:数值// V:索引for (int i = 0; i < nums.length; i++) {// 遍历当前元素,并在map中寻找是否有匹配的keyif (map.containsKey(target - nums[i])) {arr[0] = i;arr[1] = map.get(target - nums[i]);return arr;}// 如果没找到匹配对,就把访问过的元素和下标加入到map中map.put(nums[i], i);}return arr;}}
第三章 - 哈希表 part02
:::info
- 454.四数相加II
- 383.赎金信
- 15.三数之和
- 18.四数之和
- 总结
:::
454.四数相加II
建议:本题是 使用map 巧妙解决的问题,好好体会一下 哈希法 如何提高程序执行效率,降低时间复杂度,当然使用哈希法 会提高空间复杂度,但一般来说我们都是舍空间 换时间, 工业开发也是这样。 题目链接:454. 四数相加 II - 力扣(LeetCode) 文章讲解/视频讲解:代码随想录
思路
如果 暴力解 嵌套四层循环做这个题时间复杂度太高,需要 O(n4)
而我们采用下面的方法只需要 O(n2)
- 首先定义一个 HashMap
- key -> 两数之和
- value -> 两数之和出现的次数
- 定义
count,用来统计a+b+c+d = 0出现的次数。 - 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
- 遍历大C和大D数组,找到如果
0-(c+d)在map中出现过的话- 用 count 把 map 中 key 对应的value也就是出现次数统计出来。
- 最后返回统计值 count 就可以了
为什不能把第一个单独放在一起,后三个放一起?
-
方法 - Map
/*** 方法 - Map*/class Solution {public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {Map<Integer, Integer> map = new HashMap<>();int count = 0; // a+b+c+d=0 的个数// 统计 num1 与 num2 两个数组中的元素之和是相同的出现的次数for (int i : nums1) {for (int j : nums2) {// getOrDefault方法,查询参数是Key - getOrDefault(Object key, V defaultValue)// 如果查的到,返回的是对应的 value// 如果查询不到,就返回默认值 defaultValuemap.put(i + j, map.getOrDefault(i + j, 0) + 1);}}// 统计剩余的两个数组中元素的的和 (i + j) 为num3和num4数组组成元素的和// 在map中找是否存在 Key 为 0 - (i + j) 的情况,同时记录次数for (int i : nums3) {for (int j : nums4) {count += map.getOrDefault(-(i + j), 0);}}return count;}}
383.赎金信
建议:本题 和 242.有效的字母异位词 是一个思路 ,算是拓展题 题目链接:力扣 383. 赎金信 文章讲解:代码随想录
方法一 - 暴力法
- 两层for循环,不断去寻找
- 找到一个就记录一次 最后匹配找到的个数是否与 ransomNote 字符串长度相等 若相当 -> true
- 注意1:找到后要退出循环 - 否则会继续对比下去 就会变成 1 对 多的关系
- 注意2:需要一个magazine大小的数组 用于存 magazine 已经匹配成功的字符 ```java /**
方法一 - 暴力解 */ class Solution { public boolean canConstruct(String ransomNote, String magazine) {
int[] useArr = new int[magazine.length()]; // 定义一个书畅读为 magazine 长度的数组int count = 0; // 记录成功匹配的次数for (int i = 0; i < ransomNote.length(); i++) {for (int j = 0; j < magazine.length(); j++) {// 如果当前 ransomNote 字符 与 magazine 字符相等 并且 不是我们之前在magazine匹配过的字符if (ransomNote.charAt(i) == magazine.charAt(j) && useArr[j] == 0) {count++; // 匹配成功 次数+1useArr[j] = magazine.charAt(j); // 并将成功匹配的字符存在对应索引的位置break; // 一定要退出循环!!! 否则会继续对比下去 就会变成 1 对 多的关系}}}return count == ransomNote.length();
方法二 - 哈希法
- 这道题目和 242.有效的字母异位词(opens new window) 很像
- 都是字符种类固定 且不大 -> 26个小写字母
- 同样是做匹配
- 因为题目所只有小写字母,那可以采用空间换取时间的哈希策略,
- 用一个长度为26的数组
int[] record = new int[26] - 记录 magazine 里字母出现的次数
record[ransomNote.charAt(i) - 'a']++
- 用一个长度为26的数组
- 然后再用ransomNote去验证这个数组是否包含了 ransomNote所需要的所有字母
record[magazine.charAt(i) - 'a']--```java /**
方法二 - 哈希法 */ class Solution { public boolean canConstruct(String ransomNote, String magazine) {
// 如果前>后直接返回if (ransomNote.length() > magazine.length()) {return false;}// 定义一个哈希映射数组int[] record = new int[26];// 将 randomNote 中的字符 映射到数组中 ++for (int i = 0; i < ransomNote.length(); i++) {record[ransomNote.charAt(i) - 'a']++;}// 将 magazine 中的字符 映射到数组中 --for (int i = 0; i < magazine.length(); i++) {record[magazine.charAt(i) - 'a']--;}// 如果大于零说明 ransomNote 里出现的字符 magazine没有for (int a : record) {if (a > 0) {return false;}}return true;
} } ```
- 时间复杂度: O(n)
- 空间复杂度: O(1)
注意:
- 为什么不直接用map?
- 其实在本题的情况下,使用map的空间消耗要比数组大一些的,
- 因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!
- 数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
15.三数之和
建议:本题虽然和 两数之和 很像,也能用哈希法,但用哈希法会很麻烦,双指针法才是正解,可以先看视频理解一下 双指针法的思路,文章中讲解的,没问题 哈希法很麻烦。 题目链接:力扣 15. 三数之和 文章讲解/视频讲解:代码随想录
方法一 - 排序 + 双指针
- 难点:结果需要去重

- 对数组进行排序 -> 方便后面双指针进行操作
- for 循环遍历
- i从下标0的地方开始
- 定义指针 left 在 i+1 的位置上
- 定义指针 right 在数组结尾的位置上 ->
nums.length - 1
a = nums[i],b = nums[left],c = nums[right]找出a + b + c = 0left 和 right 指针的移动 -> 注意数组是排序过后的
i > 0即去除了第一个元素情况- 当
i = 0时 若进行 i - 1 就直接 NPE - 并且
i = 0由于是第一个不可能有所谓重复出现
- 当
使用
nums[i] == nums[i - 1]而不是nums[i] == nums[i + 1]原理同 a 去重
- 注意放置位置 -> 去重逻辑应该放在找到一个三元组之后
- 否则第一次出现的可能也被干掉了~
while (right > left && nums[right] == nums[right - 1]) right--;while (right > left && nums[left] == nums[left + 1]) left++;
- 否则第一次出现的可能也被干掉了~
Code
/*** 排序 + 双指针*/class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> lists = new ArrayList<>(); // 用于返回的二维集合// 先对数组内容进行排序Arrays.sort(nums);int left;int right;int sum;// 找出a + b + c = 0// a = nums[i], b = nums[left], c = nums[right]for (int i = 0; i < nums.length - 2; i++) {if (nums[i] > 0) { // 若排序后第一个元素都大于0 就直接返回return lists;}left = i + 1;right = nums.length - 1;// 对a去重// 1. i > 0 即除了第一个元素情况// 2. 对比当前元素和前一个元素 相等则说明 a 重复// 3. 不能用当前元素比较后一个元素 -> 题目中要求的是组合不能有重复 而不是组合内不不能有重复if (i > 0 && nums[i] == nums[i - 1]) {continue;}while (left < right) {sum = nums[i] + nums[left] + nums[right];if (sum < 0) {left++;} else if (sum > 0) {right--;} else {lists.add(Arrays.asList(nums[i], nums[left], nums[right]));// 去重逻辑应该放在找到一个三元组之后 分别对b 和 对c去重while (right > left && nums[right] == nums[right - 1]) right--;while (right > left && nums[left] == nums[left + 1]) left++;// 指针移动right--;left++;}}}return lists;}}
方法二 - 哈希法(不推荐)
- 以后有时间补充 -> 去重的过程不好处理 👻
18.四数之和
建议: 要比较一下,本题和 454.四数相加II 的区别,为什么 454.四数相加II 会简单很多,这个想明白了,对本题理解就深刻了。 本题 思路整体和 三数之和一样的,都是双指针,但写的时候 有很多小细节,需要注意,建议先看视频。 题目链接:力扣 18. 四数之和 文章讲解/视频讲解:代码随想录
方法 - 双层循环 + 双指针
- 整体思路与三数之和保持一致
- 四数之和 = 三数之和 + for循环
- 剪枝:剪枝操作不能直接套之前的 三数之和
- 分为 一级剪枝 和 二级剪枝
- 剪枝一定要注意 只有
target > 0才能剪枝 负数减不了 - 二级剪枝时 不能直接像一级剪枝一样直接返回 -> 应该 break
只是当前组合不成立了,还有每遍历到到的组合!!!
- 剪枝一定要注意 只有
- 剪枝过程更细腻 需要注意所比较的数值是谁
- 分为 一级剪枝 和 二级剪枝
- 去重:整体思路与三数之和相同
- 区别在于去重逻辑多一个数
class Solution {public List<List<Integer>> fourSum(int[] nums, int target) {List<List<Integer>> lists = new ArrayList<>(); // 用于返回的二维集合// 先对数组内容进行排序Arrays.sort(nums);int left;int right;long sum; // int 在后面求和会溢出// 找出a + b + c + d = target// a = nums[j] b = nums[i], c = nums[left], d = nums[right]for (int j = 0; j < nums.length - 3; j++) {// 一级剪枝 - 注意一定是大于 0 才能剪枝 否则若 target = -4 nums[j] = -3 时不能剪枝 后面可能有 -1if (target > 0 && nums[j] > target) {return lists;}// 对 a 去重if (j > 0 && nums[j] == nums[j - 1]) {continue;}for (int i = j + 1; i < nums.length - 2; i++) {// 二级剪枝 - 同一级剪枝if (target > 0 && nums[j] + nums[i] > target) {break; // 这个地方一定是 break 不能是 return 因为后面还有没遍历的呢 你给返回就GG了}// 对 b 去重if (i > j + 1 && nums[i] == nums[i - 1]) {continue;}left = i + 1;right = nums.length - 1;while (left < right) {sum = (long) nums[j] + nums[i] + nums[left] + nums[right];if (sum < target) {left++;} else if (sum > target) {right--;} else {lists.add(Arrays.asList(nums[j], nums[i], nums[left], nums[right]));// 去重逻辑应该放在找到一个三元组之后 分别 对c 和 对d 去重while (right > left && nums[right] == nums[right - 1]) right--;while (right > left && nums[left] == nums[left + 1]) left++;// 指针移动right--;left++;}}}}return lists;}}
- 恰饭~
