哈希表理论基础
建议:大家要了解哈希表的内部实现原理,哈希函数,哈希碰撞,以及常见哈希表的区别,数组,set 和map。什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。 这句话很重要,大家在做哈希表题目都要思考这句话。
文章讲解:代码随想录
哈希表
哈希表是根据关键码的值而直接进行访问的数据结构。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里,要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数
哈希函数如下图所示,通过hashCode把学生名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表同一个索引下标的位置。
哈希碰撞
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了。
数据规模是dataSize, 哈希表的大小为tableSize。
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放冲突的数据了。如图所示:
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组:哈希的值比较小,范围也比较小,范围可控
- set (集合):哈希的值很大
- map (映射):k-v结构
哈希表基础知识总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
242.有效的字母异位词
建议: 这道题目,大家可以感受到数组用来做哈希表给我们带来的遍历之处。
题目链接/文章讲解/视频讲解: 代码随想录
重点:
1. 因为总共只有26个字母,所以用数组来作哈希表
2. hashfunction为与 'a' 的ASCII码的差值
public static boolean isAnagram(String s, String t) {
// 如果长度都不相等,直接返回false
if (s.length() != t.length()) {
return false;
}
// 建一个能索引26个字母的hash数组
int[] hash = new int[26];
// 对s字符串的每个字符进行个数统计在hash数组
char[] sArr = s.toCharArray();
for (char c : sArr) {
hash[c - 'a']++;
}
// 对t字符串的每个字符在hash数组减减
char[] tArr = t.toCharArray();
for (char c : tArr) {
hash[c - 'a']--;
}
// 如果是字母异位词,此时的hash数组元素应该全为0
for (int i : hash) {
if (i != 0) {
return false;
}
}
return true;
}
349. 两个数组的交集
建议:本题就开始考虑什么时候用set什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。题目链接/文章讲解/视频讲解:代码随想录
重点:
1. 因为我们需要在nums2中查找nums1中的元素,所以考虑哈希
2. 利用Set来当哈希表结构
思路:
1. 把第一个数组Hash进Set里,Set会自动去重
2. 把第二个数组和这个Set比较,如果有相同的,则添加进resultSet里,同样会自动去重
3. 把resultSet给转化成数组并返回
public static int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) {
return new int[0];
}
if (nums2 == null || nums2.length == 0) {
return new int[0];
}
Set<Integer> nums1Set = new HashSet<>();
Set<Integer> resultSet = new HashSet<>();
// 把第一个数组hash进nums1Set里,nums1Set会自动去重
for (int i : nums1) {
nums1Set.add(i);
}
// 把第二个数组和nums1Set比较,如果有相同的,
// 则添加进resultSet里,同样会自动去重
for (int i : nums2) {
if (nums1Set.contains(i)) {
resultSet.add(i);
}
}
// 把resultSet给转化成数组并返回
int[] resultArr = new int[resultSet.size()];
int i = 0;
for (Integer integer : resultSet) {
resultArr[i++] = integer;
}
return resultArr;
}
202. 快乐数
建议:这道题目也是set的应用,其实和上一题差不多,就是套在快乐数一个壳子
题目链接/文章讲解:代码随想录
重点
1. 因为我们要一个很大的集合来保存我们计算过的数,并且能快速查找是否计算过,所以考虑使用哈希表。
2. 题目中说了会无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
3. 我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
4. 所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
思路:
就是建一个hashSet保存已经判断过的数,如果在计算过程中在hashSet里发现了已经判断过的数,说明这不是个快乐数,会不断的重复一样的计算。
public static boolean isHappy(int n) {
// hashSet保存已经计算过的数
Set<Integer> hashSet = new HashSet<>();
// 发现等于1就是快乐数,发现hashSet里已经存在同样的数说明不是快乐数
while (n != 1 && !hashSet.contains(n)) {
hashSet.add(n);
n = getNextNumber(n);
}
return n == 1;
}
private static int getNextNumber(int n) {
int next = 0;
// 不断的向后消一位,来计算每个位置上数字的平方和
while (n > 0) {
int unit = n % 10;
next += unit * unit;
n = n / 10;
}
return next;
}
1. 两数之和
建议:本题虽然是力扣第一题,但是还是挺难的,也是代码随想录中数组,Set之后,使用Map解决哈希问题的第一题。
建议大家先看视频讲解,然后尝试自己写代码,在看文章讲解,加深印象。
题目链接/文章讲解/视频讲解: 代码随想录重点:1. 因为要把计算过的数保存下来,并且index也要保存,查找也要快速,所以考虑使用Map哈希2. 用Map来作哈希表结构思路:1. 以value - index的键值对来保存2. 用target与当前元素的差值在Map中查找key
public static int[] twoSum(int[] nums, int target) {
int[] result = new int[2];
if (nums == null || nums.length == 0) {
return result;
}
// 建hashMap来保存已经遍历过的 value-index
Map<Integer, Integer> hashMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 看当前元素差target多少
int diff = target - nums[i];
// 如果这个差在hashMap中找到,证明有两个数和为target
if (hashMap.containsKey(diff)) {
result[0] = hashMap.get(diff);
result[1] = i;
return result;
}
// 如果这个差在hashMap没找到,以value-index加入到hashMap中
hashMap.put(nums[i], i);
}
return result;
}