:::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中寻找是否有匹配的key
if (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
// 如果查询不到,就返回默认值 defaultValue
map.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++; // 匹配成功 次数+1
useArr[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 = 0
left 和 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 时不能剪枝 后面可能有 -1
if (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;
}
}
- 恰饭~