一、哈希表理论基础
哈希表其实就是数组,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了
哈希碰撞, 如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般,解决哈希碰撞有两种解决方法,拉链法和线性探测法
拉链法,就是在同一位置上再拉长链,解决冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
线性探测法,使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
常见的三种哈希结构,当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
这里数组就没啥可说的了,我们来看一下set。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
set
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
map
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 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
二、242有效的字母异位词
这道题要先明白字符串的每个字符本来就是26个字符,从a-z。所以直接创建一个大小为26的int数组recond来存这个字符,我们可以将第一个字符串的每一个字符做一个扫描,之后每一个字符在recond中统计次数,这里要使用一个相对数值来表示,并且这里也用到一个字符串的第i个位置的字符提取出来的函数s.charAt(i)
在第一个for循环中先统计第一个字符串的每个字符的数目,recond[]++
在第二个for循环中再次统计第二个字符串的每个字符的数目,在recond[]--
之后再循环统计recond中的各个字符是不是次数为0,都为0就返回true,有一个不为0就返回false
这个循环操作是for(int count:recond),是把recond数组中的每个值循环赋值给count,下边对每一个count做操作
代码如下:
class Solution {
public boolean isAnagram(String s, String t) {
int[] recond = new int[26];
for(int i = 0;i<s.length();i++)
{
recond[s.charAt(i) - 'a']++;
}
for(int i = 0;i<t.length();i++)
{
recond[t.charAt(i) - 'a']--;
}
for(int count:recond)
{
if(count != 0)
{
return false;
}
}
return true;
}
}
三、349两个数组的交集
这道题方法大致有两种,第一种是设置hashset数组,不过最后是要转换一下整数数组,这道题我也是主要是用的这种方法,第二种是hash数组,就是保存了每个数的次数,一个数对应一个次数,接下来主要讲一下hashset的思路
首先要判断一下nums这两个数组是否为空,长度是否为零,如果为0或者空的话,那么就返回一个长度为0的整型数组
之后就是建立两个hashset数组,要注意建立的格式Set<Integer> set1 = new HashSet<>()
然后就是两个for循环,第一个for循环是将nums1的值放入hashset数组中,第二个for循环是判断在第一个hashset数组中有没有包含自己的值,也就是求交集,有的话放入自己的hashset数组中
之后就是想办法将第二个 hashset数组(交集)还原为整数数组输出来
第一种办法就是常规办法,先建立一个整数数组,以set2.size()为大小的,然后将set2的值放入,即for(int i:set2){ arr[j++] = i;},之后return arr;
第二种方法是直接用流,map转化来处理,就是set2.stream().mapToInt(x -> x).toArray(); 这里mapToInt(x -> x)的含义是使用 mapToInt
将流中的每个元素 x
转换为一个整数,并返回一个特定类型的流,即 IntStream
代码如下:
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> set2 = new HashSet<>();
for(int i:nums1)
{
set1.add(i);
}
for(int i:nums2)
{
if(set1.contains(i))
{
set2.add(i);
}
}
int[] arr = new int[set2.size()];
int j = 0;
for(int i:set2)
{
arr[j++] = i;
}
return arr;
}
}
四、202快乐数
首先我再强调一下 什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
这道题主要是考察了哈希set的效率性问题,使用哈希数组而不使用普通数组,一来这个空间大小不需要指定,二来哈希集合是一种更高效的数据结构,可以在常数时间内执行插入、查找和删除操作,因此通常更适合用于这种问题。它允许你以较低的复杂度进行查找和存储操作,从而更容易实现检测循环的逻辑。
所以首先创建一个哈希数组set,然后循环判断n,以及他的中间结果,是否在数组中,因为每一次都会计算出新的n来,再进行下一轮的循环,所以循环条件就是n != 1 && !recond.contains(n)
之后进入循环,如果不在recond中,那么就将他添加到哈希数组中,如果他在数组中,那说明已经陷入了循环,所以在外层出来的时候要判断是否n == 1
然后下一步调用自定义函数getnextnum,这个函数主要是获取的下一轮n,在函数之外声明新的函数,新的函数主要是求一下n的各个位置数字的平方和,正常的第一步与10取余操作,然后统计个位数的平方,用res来存,用一个res+=的操作来求十位以及百位的和,之后要取整取出来个位之前的位数数字,之后再进入循环取出个位,相加,直到n是等于0的时候,结束循环,返回最终的结果res给主函数 判断是否是1,是1的话就是直接返回true,因为返回的是布尔型的,所以是return n==1;
代码如下:
class Solution {
public boolean isHappy(int n) {
Set<Integer> recond = new HashSet<>();
while(n != 1 && !recond.contains(n))
{
recond.add(n);
n = getnextnum(n);
}
return n == 1;
}
public int getnextnum(int n)
{
int res = 0;
while(n>0)
{
int temp = n % 10;
res += temp*temp;
n = n / 10;
}
return res;
}
}
五、1两数之和
这道题卡住的点有四个,分别是map.containsKey(temp),map.get(temp),map.put(nums[i],i),break
本道题主要想到为什么用哈希map,因为①首先想到要查询一个元素是否重复出现过,是否在一个集合中出现过,②不能用set是因为set只能存储一个数值,存储不了下标,这道题要返回的是值和下标,③map键值对存储的就是kv键值对,可以保存值和下标,所以选用hashmap
之后要明确先创建一个整型数组res,数组大小为int[2],保存要返回的值和下标,这都是要从map中赋值出来的
然后正常的判断一下nums是否为空,或者长度是否为0,是的话就返回空数组res
之后进入正常的for循环判断数组nums的每一个值,每一个值都要做一下target-的操作,然后匹配是否在map中有没有包含,如果有包含就是正常的取值操作map.get(nums[i])赋值给res[0],下标赋值给res[1],没有的话就是正常的存值到map数组中,存值是要用map.put(nums[i],i)操作,如果能匹配到中间还得break跳出for循环,for循环之外返回res
代码如下:
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[0] = map.get(temp);
res[1] = i;
break;
}
map.put(nums[i],i);
}
return res;
}
}