文章目录
1.哈希表理论基础
1.1 哈希表
一般哈希表都是用来快速判断一个元素是否出现集合里。
-
哈希表是根据关键码的值而直接进行访问的数据结构。
-
直白来讲其实数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
-
例如要查询一个名字是否在这所学校里。要枚举的话时间复杂度是O(n),但如果使用哈希表的话,只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
1.2 哈希函数
-
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
-
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
-
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样 就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。如果学生的数量大于哈希表的大小怎么办(比如有20个学生,哈希表大小为15,则有5个同学的名字会同时映射到哈希表同一个索引下标的位置),此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来哈希碰撞登场
1.3 哈希碰撞
如下图所示,小李和小王都映射到了索引下标 1 的位置,这种现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法,拉链法和线性探测法。
#拉链法
1.3.1 拉链法
-
小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
-
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
1.3.2 线性探测法
-
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。(数据规模是dataSize,哈希表的大小为tableSize)
-
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
1.4 常见的三种哈希结构(找java对应的资料)
1.4.1 数组
1.4.2 set 集合
1.4.3 map 映射
1.5 总结
- 要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
- 但哈希法牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
- 如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
232.有效的字母异位词
-
题目描述:给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
-
思路:用数组来解这道题。
- 定义一个长度26的空数组,代表存‘a’到’z’26个字母。
- 遍历字符串s,出现了某个字母,就在对应的字母位置处另·令数组值++;
- 遍历字符串t,出现了某个字母,就在对应的字母位置处令数组值–;
- 最后遍历数组中的26个值,如果有非零值,说明s和t不是字母异位词。
-
代码注意:
- 26个字母的ASCII码是连续的,某个字母的ASCII码-字母‘a’的码值,就可以判断它是第几个字母。
- 求长度的方法:
数组:lenth是数组的一个属性,没有括号,用于求数组的长度。
字符串:length()是字符串自带的方法,用于求String的长度。
列表:size()是列表自带的方法,用于求列表长度。 - charAt()方法用于返回指定索引处的字符。
- charAt举例
-
时间复杂度:O(n)
-
空间复杂度:O(1),定义的是一个常量大小的辅助数组,所以空间复杂度为O(1);1
class Solution {
public boolean isAnagram(String s, String t) {
int[] hash = new int[26];
for(int i = 0; i < s.length(); i++) {
hash[s.charAt(i) - 'a']++;
}
for(int i = 0; i < t.length(); i++) { //字符串用的是length()方法
hash[t.charAt(i) - 'a']--;
}
for(int i = 0; i < hash.length; i++) { //注意,数组用的是length属性
if(hash[i] != 0) {
return false;
}
}
return true;
}
}
349.两个数组的交集
-
题目描述:给定两个数组,编写一个函数来计算它们的交集。注意:输出结果中的每个元素一定是唯一的,也就是结果需要去重。
-
思路:见代码注释。
-
用set解决【以后补充吧 我今天必须玩到王国之泪】
-
用数组解决
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash = new int[1000];
//创建一个HashSet来存放结果,直接往里add就ok。
Set<Integer> res = new HashSet<>();
//遍历nums1,把nums1中的数存到hash数组里。
for(int i = 0; i < nums1.length; i++) {
//出现过一次就赋值1,后续就算有重复的元素也是1。
hash[nums1[i]] = 1;
}
//遍历nums2
for(int i = 0; i < nums2.length; i++) {
//如果已经等于1了的话,意味着muns2中出现了和nums1相交的元素。
if(hash[nums2[i]] == 1) {
res.add(nums2[i]); //把相交的元素放到HashSet中。
}
}
//返回结果
return res.stream().mapToInt(x -> x).toArray();
}
}
202.快乐数
- 202.快乐数 | 题目链接
- 题目描述:编写一个算法来判断一个数 n 是不是快乐数。如果 n 是 快乐数 就返回 true ;不是则返回 false 。
- 「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。 - 思路:
1. 题目中说了 无限循环,也就是说,不是快乐数的n,会进入无限循环,那么就判断一个元素是否出现在集合里即可。这类题首先需要想到哈希表!
class Solution {
public boolean isHappy(int n) {
//创建一个Set存放每次的新的n;
Set<Integer> record = new HashSet<>();
//只要n不等于1,并且record中没有曾经出现过的数字,就一直循环
while(n != 1 && !record.contains(n)) {
record.add(n); //把这次的n加到record里
n = getNextN(n); //得到新的n
}
return n == 1;
}
//求数n的各个位置上的平方和
public int getNextN(int n) {
int res = 0;
//循环,一直对10取余,直到最后n已经成个位数了,/10以后为0,结束循环。
while(n != 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
1.两数之和
- 1.两数之和 | 题目链接
- 题目描述:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。 - 思路:
- 创建Map存放遍历过的元素。
- 为什么创建Map?因为答案需要返回下标,所以要存两个数据,Map中的key-value满足条件,把元素存到key中,下标存到value中,因为我们要查找元素是否在Map中出现过,快速查找,对应的是关键码的值(也就是这里的key)
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2]; //存放结果的数组,两个符合要求的元素的下标
if(nums == null || nums.length == 0) {//如果传的是null,或数组为空,返回res即可
return res;
}
Map<Integer, Integer> map = new HashMap<>();//创建一个HashMap来存放遍历过的元素的值和下标
for(int i = 0; i < nums.length; i++) {
int temp = target - nums[i];
if(map.containsKey(temp)) { //如果map中存过temp这个值为key的话
res[0] = i; //其中一个元素的下标就是当前遍历的i
res[1] = map.get(temp); //另一个元素的下表是key为temp的元素所对应的value;
break;//如果找到了一组,就跳出循环
}
map.put(nums[i] , i); //如果没找到,就把当前元素的值存为Key,下表存为Value,存到HashMap中
}
return res;
}
}