第三章 - 哈希表 part01
@author LemonCat
@Time 2023/7/31
- 哈希表理论基础
- 242.有效的字母异位词
- 349.两个数组的交集
- 202.快乐数
- 1.两数之和
理论基础
建议:大家要了解哈希表的内部实现原理,哈希函数,哈希碰撞
什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。 这句话很重要,大家在做哈希表题目都要思考这句话。
常见的三种哈希结构
- 数组
- 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
-
/**
* 方法一 - 哈希表法
*/
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 的应用,其实和上一题差不多,就是套在快乐数一个壳子
方法 - 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 解决哈希问题的第一题。
建议大家先看视频讲解,然后尝试自己写代码,在看文章讲解,加深印象。
文章讲解/视频讲解:代码随想录 (programmercarl.com)
思路:
-
访问到一个数 nums[i],与之和为 target 的数只能是 target-nums[i]
-
将每次便利的数存入 Map 中
-
注意:K-V 分别对应的值!!!
- K:数值
- V:索引
-
因为我们最终是要找对应数值的索引 -> arr[1] = map.get(target - nums[i])
-
-
然后找 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;
}
}
- 明天加油!!!