哈希表
1. 理论
一般哈希表都是用来快速判断一个元素是否出现集合里
1.1 哈希结构
- 数组
- set (集合)
- map(映射)
1.2 数组
1.3 set集合
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(log n) | O(log n) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set
底层实现为哈希表std::set
和std::multiset
的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
1.4 Map集合
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(log n) | O(log n) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
- std::unordered_map 底层实现为哈希表
std::map
和std::multimap
的底层实现是红黑树 同理,std::map
和std::multimap
的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)
1.5 总结
- 当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
- 但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
- 如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
- 在哈希值比较小,范围可控情况下用数组;数组很大用set;k对应有value的话用map
2. 242有效的字母异位词 E (数组哈希解)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
2.1 哈希表解法
思路
- t 是 s 的异位词等价于两个字符串中字符出现的种类和次数均相等
- 定义一个数组叫做record用来上记录字符串s里字符出现的次数。
- 需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
- 再遍历字符串s的时候,只需要将
s[i] - ‘a’
所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。 - 如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
- 那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
- 最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
解题代码
class Solution {
//两种方法 排序 哈希表
public boolean isAnagram(String s, String t) {
int[] record=new int[26];
for (int i = 0; i < s.length(); i++) {
record[s.charAt(i)-'a']++; 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
}
for (int i = 0; i < t.length(); i++) {
record[t.charAt(i)-'a']--;
}
for (int count : record) {
if (count!=0) { //record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
return true;
}
}
- 时间复杂度为O(n)
- 空间复杂度为O(1),空间上因为定义是的一个常量大小的辅助数组
总结
学会用字符下标来映射为数组下标这是本题要学会的
2.2 排序解法
思路
- t 是 s 的异位词等价于两个字符串排序后相等
- 因此我们可以对字符串 s 和 t 分别排序,看排序后的字符串是否相等即可判断
- 此外,如果 s 和 t 的长度不同,t 必然不是 s 的异位词
解题代码
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
char[] str1 = s.toCharArray();//字符串数组
char[] str2 = t.toCharArray();
//排序
Arrays.sort(str1);//底层调用快排
Arrays.sort(str2);
return Arrays.equals(str1, str2);
}
}
3. 349两个数组的交集 E (Set解)
给定两个数组
nums1
和nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
《代码随想录》建议:
本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和测试用例,添加了
0 <= nums1[i]
,nums2[i] <= 1000
条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。
3.1 Set哈希表解法
思路分析
- 遍历两数组,遍历nums1数组存入到set哈希表结构中
- 遍历数组num2的过程中查询哈希表中是否存在该元素,若存在,存入一个新的set集合中。用set集合是为了结果中去重
- 后将结果集转换为数组
解题代码
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1==null||nums1.length==0||nums2==null||nums2.length==0) {
return new int[0];
}
//设置两集合,一个用于将数组存入哈希表结果中,另一个作为结果集
Set<Integer> set1=new HashSet<>();
Set<Integer> resSet=new HashSet<>();//结果集 set集合不可重复
//将nums1遍历存入哈希表结构中
for (int i : nums1) {
set1.add(i);
}
//遍历nums2,判断哈希表中是否有相同的值,并存入
for (int i : nums2) {
if (set1.contains(i)) {
resSet.add(i);
}
}
//方法1:将结果集合转为数组
return resSet.stream().mapToInt(x -> x).toArray();
//将结果集转换为数组
int[] arr=new int[resSet.size()];
int j=0;
for (Integer integer : resSet) {
arr[j++]=integer;
}
return arr;
}
}
复杂度分析
-
时间复杂度是 (m+n),其中 m 和 n 分别是数组
nums1
和nums2
的长度。在第一个循环中,我们需要遍历数组
nums1
的所有元素,将其放入set1
中,时间复杂度为 O(m)。在第二个循环中,我们需要遍历数组
nums2
的所有元素,对于每个元素,需要在set1
中进行查找,这个操作的时间复杂度是 O(1),因为使用了哈希表进行存储,所以这个循环的时间复杂度为 O(n)。最后,将
resSet
转换为数组,需要遍历resSet
中的所有元素,时间复杂度为O(k),其中 k 是最终结果集合中元素的个数。因此,时间复杂度为 O(m+n+k),实际上也就是O(m+n)。
-
空间复杂度O(min(m,n))
空间复杂度主要是由两个哈希表所占用的空间决定的。
首先是
set1
哈希表,其最坏的情况下需要存储所有的nums1
中的元素,所以其空间复杂度是O(m)。然后是
resSet
哈希表,最坏的情况下需要存储所有nums1
和nums2
中的交集元素,所以其空间复杂度是 O(min(m,n))。因此,代码的空间复杂度是 O(min(m,n))。
解释一段代码
resSet.stream().mapToInt(x -> x).toArray();
- 将 ResultSet 转换成一个 Stream 对象。
- 调用 mapToInt() 方法将 Stream 中的每个元素映射为 int 类型。
- 最后,toArray() 方法将 Stream 转换为一个 int 数组。
-
通过调用 resSet 对象的 stream() 方法,将其转换成一个流对象 Stream。在这个流对象上我们调用 mapToInt() 方法,将其中的每个元素都转换为 int 类型,再用 toArray() 方法将该流对象转换为一个 int 类型的数组。
-
mapToInt()
方法接受一个函数式接口ToIntFunction<T>
作为参数,该接口定义了一个只接受一个参数并返回一个 int 值的方法。在这里,我们使用Lambda表达式x -> x
实现了ToIntFunction<T>
的接口方法,将传入的参数直接返回。 -
x -> x
是一个 Lambda 表达式,它实现了一个只有单个参数的函数式接口。这个 Lambda 表达式表示的函数mapToInt()的参数是x
,函数体是x
。换句话说,这个函数会把接收到的参数原封不动地返回。这种写法称为“恒等函数”(identity function)。在这个例子中,我们可以使用这个 Lambda 表达式实现一个
ToIntFunction<T>
接口的方法,该方法会将参数本身转换成一个int
值。这是因为,这个 Lambda 表达式的参数和返回值的类型都是Integer
,而该类型已经实现了将自己转换成一个int
值的方法,即intValue()
。需要注意的是,
x -> x
这种写法其实相当于函数定义int f(int x){return x;}
。不同的是,Lambda 表达式是一种匿名函数,它没有名称,也没有方法声明。
3.2 数组解法
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0];
}
Set<Integer> res=new HashSet<>();
int[] hash=new int[1005];
// nums1中出现的字母在hash数组中做记录
for (int i : nums1) {
hash[i]=1;//标记为1
}
for (int i : nums2) {
if (hash[i]==1) {
res.add(i);
}
}
//集合转数组
int[] arr=new int[res.size()];
int j=0;
for (int re : res) {
arr[j++]=re;
}
return arr;
}
}
-
时间复杂度:O(m+n)
-
空间复杂度:
空间复杂度主要是由
hash
数组和Set
集合所占用的空间决定的。hash
数组长度最大为 1005,所以空间复杂度为 O(1)。Set
集合保存了交集元素,所占用的空间取决于nums1
和nums2
的交集元素数量,最坏的情况下为 min(m,n),所以空间复杂度为 O(min(m,n))。
3.3 总结及其注意
- 集合求交集可以用set集合来去重
4. 202 快乐数 E(Set解)
编写一个算法来判断一个数
n
是不是快乐数.
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果
n
是 快乐数 就返回true
;不是,则返回false
。
4.1 思考分析
根据探索,猜测会有以下三种可能:
-
最终会得到 1
-
最终会进入循环
-
值会越来越大,最后接近无穷大——>这种永远不会发生,证明:链接
所以共有2种可能
-
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
-
当遇到要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
-
另一个难点就是求和的过程,对取数值各个位上的单数操作。
算法分析,分为两部分:
- 给一数字n,下一个数是什么?
- 做数位分离,求平方和
- 按照一系列数字判断是否进入循环
- 使用哈希集合完成。每次生成链中的下一个数字时,我们都会检查它是否已经在哈希集合中。
- 如果它不在哈希集合中,我们应该添加它。
- 如果它在哈希集合中,这意味着我们处于一个循环中,因此应该返回 false。
- 使用哈希集合完成。每次生成链中的下一个数字时,我们都会检查它是否已经在哈希集合中。
4.2 解题代码
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while (n != 1 && !record.contains(n)) {
record.add(n);
n = getNextNumber(n);
}
return n == 1;
}
private int getNextNumber(int n) {
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
-
时间复杂度:O(logn),其中n是输入的数字。
- 每次计算下一个数字时,都需要将当前数字的每个位数平方并求和,而下一个数字大小最多为log10(n)。因此,循环运行的最大次数是log10(n)。
-
空间复杂度:O(logn)
- 需要记录一组HashSet,最坏情况下会存储所有数字,即log10(n)个数字。每个数字最多占用O(logn)的空间,因此总空间复杂度为O(logn)。
5. 1 两数之和 E(Map 解)
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出 和为目标值target
的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
5.1 思路分析
1. 为什么想到用哈希法?
将遍历过的元素放到一个集合中,每次遍历新的位置的时候,就判断和目前遍历元素匹配的元素是否出现在这个集合中,如果出现就是遍历过
2. 用什么样的哈希表?
在本题中,我们不仅要知道元素是否被遍历过,还要知道元素所对应的下标,所以使用map
- map用来存放遍历过的元素
- map的key用来存放元素, value用来存放下标
- 因为要查的是元素是否出现过,所以应该把元素作为key,最后返回对应的value值,即下标。map的作用就是能在最快的时间内查找元素是否出现过
3. 为什么不选用数组或者set呢?
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而这道题目,不仅要判断temp是否存在而且还要记录temp的下标位置,因为要返回对应的下标。所以set 也不能用
📍📍📍
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
5.2 解题代码
class Solution {
public int[] twoSum(int[] nums, int target) {
//map集合
HashMap<Integer, Integer> map = new HashMap<>();
//遍历数组
for (int i = 0; i < nums.length; i++) {
int temp = target - nums[i];
if (map.containsKey(temp)) {//在map中寻找是否有nums[i]匹配的key key存元素 value存下标
return new int[]{map.get(temp), i};//返回角标
}
//若不包含,则把数组添加入到map集合
map.put(nums[i], i);
}
return new int[0];
}
}
-
时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,可以O(1) 地寻找 target - x。
-
空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
5.3 总结及其注意
- map集合中key可以存的是数组元素,这样可以更快查找元素是否出现过。value存下标
- map集合中get()是取,put是存