今日任务
● 哈希表理论基础
● 242.有效的字母异位词
● 349. 两个数组的交集
● 202. 快乐数
● 1. 两数之和
详细布置
哈希表理论基础
了解哈希表的内部实现原理,哈希函数,哈希碰撞,以及常见哈希表的区别,数组,set 和map。
什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。 这句话很重要,在做哈希表题目都要思考这句话。
文章讲解:https://programmercarl.com/%E5%93%88%E5%B8%8C%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 |
底层实现 |
是否有序 |
数值是否可以重复 |
能否更改数值 |
查询效率 |
增删效率 |
std::set |
红黑树 |
有序 |
否 |
否 |
O(log n) |
O(log n) |
std::multiset |
红黑树 |
有序 |
是 |
否 |
O(logn) |
O(logn) |
std::unordered_set |
哈希表 |
无序 |
否 |
否 |
O(1) |
O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 |
底层实现 |
是否有序 |
数值是否可以重复 |
能否更改数值 |
查询效率 |
增删效率 |
std::map |
红黑树 |
key有序 |
key不可重复 |
key不可修改 |
O(logn) |
O(logn) |
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也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?
实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
总结
总结一下,。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
242.有效的字母异位词
建议: 这道题目,大家可以感受到 数组 用来做哈希表 给我们带来的遍历之处。
题目链接/文章讲解/视频讲解: https://programmercarl.com/0242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.html
思路:不会哈希
暴力解:
将s、t存储在数组中,对数组s、t分别sort,看s、t是否相等
class Solution { public boolean isAnagram(String s, String t) { if (s.length() != t.length()){ return false; } char[] arrS = s.toCharArray(); char[] arrT = t.toCharArray(); Arrays.sort(arrS); Arrays.sort(arrT); return Arrays.equals(arrS,arrT); } }
哈希:
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
如果对哈希表的理论基础关于数组,set,map不了解的话可以看这篇:关于哈希表,你该了解这些!(opens new window)
需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。
为了方便举例,判断一下字符串s= "aee", t = "eae"。
操作动画如下:
定义一个数组叫做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']++; } for (int i = 0; i < t.length(); i++) { record[t.charAt(i) - 'a']--; } for (int count: record) { if (count != 0) { return false; } } return true; } }
charAt(i) 函数 是获取字符串中i+1位置的字符
String str = "abc"; //从字符串中取字符 char ch = str.charAt(0); //第一个字符 char ch2 = str.charAt(1);// 第二个字符 //ch是a,ch2是b;
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
总结:用哈希时,如果要查询的范围不是特别大,试试用数组解决
349. 两个数组的交集
建议:本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和 测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0349.%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E7%9A%84%E4%BA%A4%E9%9B%86.html
思路:
1)暴力解:创建结果数组,遍历数组1,查询是否在数组2中出现,如果是则存入结果数组;最后将结果数组排序去重输出
2)哈希:
import java.util.HashSet; import java.util.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<>(); for (int i: nums1) { set1.add(i); } for (int i: nums2) { if (set1.contains(i)){ resSet.add(i); } } //将结果几何转为数组 return resSet.stream().mapToInt(x -> x).toArray(); } }
202. 快乐数
建议:这道题目也是set的应用,其实和上一题差不多,就是 套在快乐数一个壳子
题目链接/文章讲解:https://programmercarl.com/0202.%E5%BF%AB%E4%B9%90%E6%95%B0.html
思路 :不会写
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
正如:关于哈希表,你该了解这些!(opens new window)中所说,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
class Solution { public boolean isHappy(int n) { Set<Integer> record = new HashSet<>(); while (n != 1 && !record.contains(n)){ record.add(n); n = getNextNum(n); } return n == 1; } private int getNextNum (int n){ int res = 0; while (n > 0){ int temp = n % 10; res += temp * temp; n = n / 10; } return res; } }
1. 两数之和
建议:本题虽然是 力扣第一题,但是还是挺难的,也是 代码随想录中 数组,set之后,使用map解决哈希问题的第一题。
建议大家先看视频讲解,然后尝试自己写代码,在看文章讲解,加深印象。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0001.%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C.html
思路:无
什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题呢,需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
再来看一下使用数组和set来做哈希法的局限。
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。
class Solution { public int[] twoSum(int[] nums, int target) { int[] res = new int[2]; if (nums == null || nums.length == 0){ return res; } Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int temp = target - nums[i]; if (map.containsKey(temp)){ res[1] = i; res[0] = map.get(temp); } map.put(nums[i], i ); } return res; } }
总结
本题有四个重点:
- 为什么会想到用哈希表
- 哈希表为什么用map
- 本题map是用来存什么的
- map中的key和value用来存什么的