哈希表理论基础
哈希表一些概念
哈希表又叫散列表--hash table
哈希表的作用:根据关键码的值直接进行访问的数据结构
举个🌰:数组就是一张哈希表
哈希表中的关键码就是数组的索引下标key,然后通过这个下标直接去访问数组中的元素值value。看下边的图更加易于理解
哈希表可以解决的问题:一般哈希表用来判断一个元素是否出现在集合里
举个🌰:查询一个人的名字是否在这个学校里?
枚举的话时间复杂度是O(n),但是用哈希表的话用O(1)就可以做到。
首先先初始化把这所学校的人名都放到哈希表里,在查询的时候直接通过索引就可以知道这所学校里的学生是否在里面了
将学生姓名映射到哈希表里面需要通过哈希函数(hash function) 哈希函数
考虑个问题:哈希表的长度有限,如果hashcode得到的数值大于hashtable.此时为了保证映射出来的索引都落在哈希表上,会对数值进行一个取模的操作,这样就保证了学生姓名一定可以映射到哈希表上。
此时问题又来了,刚才说哈希表其实就是个数组,如果学生数量大于数组哈希表大小,此时就算计算的再均匀也避免会有几位学生的名字同时映射到哈希表的同一个索引下标位置,那这时候就涉及到哈希碰撞了
哈希碰撞
如图所示:小王和小明都映射到了索引下标为1的位置,这种现象叫做:哈希碰撞
那么解决哈希碰撞有两种方法:拉链法和线性探测法
拉链法
因为小王和小明发生冲突,我们把发生冲突的元素放到链表里就可以通过索引找到小明和小王
数据规模是datasize,哈希表大小是tablesize。
拉链法要选择适当的哈希表大小,这样就不会因为数组空置而浪费大量内存,也不会因为哈希表太长而在查找上浪费时间。
线性探测法
使用线性探测法,一定要保证tablesize>datasize,要依靠哈希表中的空位来解决碰撞问题
举个🌰:例如冲突的位置放了小王,那么就向下一个来找小明的信息,所以要求tablesize一定要大雨datasize。不然就没有空位置来存放冲突数据了。
常见三种哈希结构
当我们想使用哈希法来解决问题的时候,一般会选择如下三种数据结构
- 数组
- set(集合)
- map(映射)
对应文章讲解: programmercarl.com/%E5%93%88%E…
总结
当遇到要快速判断一个元素是否出现在集合里的时候,就要使用哈希法
哈希法是一种牺牲空间来换时间,因为要使用额外的数组,set/map来存放数据,才能实现快速查找。
哈希表相应题目练习
1.两数之和❗️用map(key value)解决
题目:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
对应题目链接:leetcode.cn/problems/tw…
两数之和自我理解与困难分析1
解法1:暴力枚举
复杂度分析:时间复杂度O(n*n) 空间复杂度为O(1)
算法
初步想法:双层for循环,暴力枚举出所有两数组合的索引,判断索引所对应的两个数是否符合和为target
最终敲定:想法正确哦!!!
1.两数之和 暴力解法 代码与解析
class Solution {
public int[] twoSum(int[] nums, int target) {
int[]res=new int[2];
int n=nums.length;
for(int i=0;i<n-1;i++){
for(int j=i+1;j<n;j++){
if(nums[i]+nums[j]==target){
res[0]=i;
res[1]=j;
return res;
}
}
}
return res;
}
}
看图更加易于理解:
1.两数之和自我理解与困难分析2
解法2:哈希表
哈希表复杂度解析:时间复杂度O(n) 空间复杂度O(n)
算法
初步想法:在处理第一个方法的时候我们会发现,会存在重复扫描的情况,所以我们需要引入个哈希表 来存储遍历过的元素(遍历的同时我们还需要看当前遍历到的元素与哈希表中的元素是否有满足情况的)。
最终敲定:想法正确哦!!!
1.两数之和 哈希表解法 代码与分析
class Solution {
public int[] twoSum(int[] nums, int target) {
int n=nums.length;
Map<Integer,Integer> map=new HashMap<Integer,Integer>();
for(int i=0;i<n;i++){
if(map.get(target-nums[i])!=null){
return new int[]{map.get(target-nums[i]),i};
}else{
map.put(nums[i],i);
}
}
return new int[0];
}
看图更加易于理解:
对应文章讲解:
programmercarl.com/0001.%E4%B8…
对应carl视频讲解:www.bilibili.com/video/BV1aT…
我觉得好的视频讲解:www.bilibili.com/video/BV1eg…
总结:哈希法适用于查询一个元素是否出现过 ,这个元素在这个集合里是否出现过。 map存储键值对的 这道题中的key是元素 value是元素索引 。
242.有效字母异位词❗️用数组解决❗️❗️
题目:给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。 对应题目链接:leetcode.cn/problems/va…
输入: s = "anagram", t = "nagaram"
输出: true
242.有效字母的异位词自我理解与困难分析1
解法1:排序
复杂度分析:时间复杂度O(nlogn) 空间复杂度O(logn)
算法
最终敲定:调内置函数排一下第一个,排一下第二个,看第一个和第二个是不是相等
242.有效字母异位词 排序解法 代码与分析
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);
}
}
242.有效字母的异位词自我理解与困难分析2
解法2:哈希表
复杂度分析:时间复杂度O(n) 空间复杂度O(n)
算法
最终敲定:创建个数组来统计第一个字符串中每个字母出现的频率++,遍历第二个字符串每个字母出现的频率--,最后哈希表里面所有数组的元素为0,说明这两个字符串有效。
242.有效字母异位词 哈希表 代码与解析
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++){
hash[t.charAt(i)-'a']--;
}
for(int i=0;i<hash.length;i++){
if(hash[i]!=0){
return false;
}
}
return true;
}
}
看图更好理解:
对应视频链接:www.bilibili.com/video/BV1YG…
对应文章链接: programmercarl.com/0242.%E6%9C…
总结:哈希法常见三种数据结构
数组:适用于哈希值较小,范围可控场景
set:适用于数值很大的场景
map:key value
349.两个数组的交集❗️用set解决❗️❗️
题目:给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2]
对应题目链接:leetcode.cn/problems/in…
题目指出输出结果中每个元素一定是唯一的,那我们就要考虑去重的操作,使用set集合
349.两个数组的交集自我理解与困难分析1
解法1:引入set
复杂度分析:时间复杂度O(m+n) 空间复杂度O(m+n)
算法:
把nums1的所有元素转为存哈希表进去,遍历nums2,看nums2的元素在哈希表里面是否出现过,如果出现过就把出现过的元素放到一个新数组里。
349.两个数组的交集 set 代码与解析
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1=new HashSet<Integer>();//初始化个set存numss1
Set<Integer> reset=new HashSet<Integer>();//存放最终的
for(int i=0;i<nums1.length;i++){
set1.add(nums1[i]);
}
for(int i=0;i<nums2.length;i++){
if(set1.contains(nums2[i])){
reset.add(nums2[i]);
}
}
return reset.stream().mapToInt(x->x).toArray();
}
}
349.两个数组的交集自我理解与困难分析2
解法2:数组
复杂度分析:时间复杂度O(m+n) 空间复杂度O(m+n)
349.两个数组的交集 数组 代码与解析
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
//数组
int[]set1=new int[1005];
Set<Integer> reset=new HashSet<Integer>();
for(int i=0;i<nums1.length;i++){
set1[nums1[i]]=1;
}
for(int i=0;i<nums2.length;i++){
if(set1[nums2[i]]==1){
reset.add(nums2[i]);
}
}
return reset.stream().mapToInt(x->x).toArray();
}
}
何时应该使用set与数组
使用数组来做哈希的题目,是因为题目都限制了数值的大小。
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
此时就要使用另一种结构体了,set 那有同学可能问了,遇到哈希问题我直接都用set不就得了,用什么数组啊。
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。
后记
本题后面 力扣改了 题目描述 和 后台测试数据,增添了 数值范围:
- 1 <= nums1.length, nums2.length <= 1000
- 0 <= nums1[i], nums2[i] <= 1000
所以就可以 使用数组来做哈希表了, 因为数组都是 1000以内的。
对应视频理解:www.bilibili.com/video/BV1ba…
对应文章理解:programmercarl.com/0349.两个数组的交…
202.快乐数
题目:编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为: 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。 如果这个过程 结果为 1,那么这个数就是快乐数。 如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
对应题目链接:leetcode.cn/problems/ha…
输入: n = 19
输出: true
解释: 12 + 92 = 82
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
202.快乐数自我理解与困难分析
解法1:引用set
复杂度分析:时间复杂度O(logn) 空间复杂度O(logn)
这道题目看上去貌似一道数学问题,其实并不是! 题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,成为一个环 这对解题很重要!
正如:关于哈希表,你该了解这些! (opens new window)中所说,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
算法
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。当一个数不是快乐数时,会一直无限循环,即在求和的过程中sum会重复,使用set存放求和过程中出现过的sum,当sum重复时,或sum==1,退出循环得到结果。
202.快乐数的代码与分析
class Solution {
public boolean isHappy(int n) {
// 创建一个 HashSet 地数据结构,进行没有顺序,不重复元素的存储
Set<Integer> set = new HashSet<>();
// 向着集合中添加元素,判断是不是形成了闭环,也就是不可能成为快乐数
while (n != 1 && !set.contains(n)) {
set.add(n);
n = getNextNum(n);
}
return n == 1; // 跳出循环时,n=1,返回ture,否则返回false
}
/**
根据当前的数字,获取到下面的数字是多少,下面的数字是根据当前的数字分别取到每一位(也就是个十百千万位等等,经过了平方和的计算)
*/
public int getNextNum(int n) {
int res = 0;
while (n > 0) {
int temp = 0;
temp = n % 10;
n = n / 10;
res += temp * temp;
}
return res;
}
}