文章目录
每日任务
一,哈希表理论基础
一,leetcode 242 有效的字母异位词
二,leetcode 349 两个数组的交集
三,leetcode 202 快乐数
四,leetcode 001 两数之和
一、哈希表理论基础
哈希表,英文名字为Hash table,也可叫做散列表。其官方描述为:哈希表是一种根据关键码的值而直接进行访问的数据结构。
其实,数组就是一张哈希表,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
哈希表最常用的作用就是:快速判断一个元素是否出现集合里!
举一个例子,现在要查询一个名字是否在这所学校里,要枚举的话时间复杂度是O(n)(将学校里的学生依次遍历,直到找到目标名),但如果使用哈希表的话, 只需要O(1)就可以做到。我们需要做的就是,初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
(1) 哈希函数
上述提到的,将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
具体怎么做呢?通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了,如下图:
① 如果hashCode得到的数值大于哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样我们就保证了学生姓名一定可以映射到哈希表上了。
② 哈希表我们刚刚说过,就是一个数组。如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表同一个索引下标的位置。 ,这就是所谓的哈希碰撞
(2)哈希碰撞
对应有两种解决手段:拉链法和线性探测法
① 拉链法
② 线性探测法
常用的Hash表结构:
数组
set (集合)
map (映射)
二、leetcode 242 有效的字母异位词
1、原题链接
2、解题思路及代码展示
什么是有效的字母异位词? 比如“a a b c”和“b a a c”就是有效的字母异位词,都是2个a,1个b和1个c。
暴力解法,就是利用两个for循环,看字符串s中每一个字母是否在字符串t中出现过。
这里使用哈希表的解题思路,使用数组作为哈希表结构,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
(1)定一个数组叫做record,数组大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值(小写的字母从a到z就是26个)。
(2)需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
(3)在遍历字符串s时,record数组中 ( s[i] - ‘a’ )所在的元素
再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
(4)统计完s中字符出现的次数之后,再遍历t中的字符,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
最后发现,如果我们的record数组中全是0的话,说明s和t中字符的个数,类型一致,知识顺序不同。
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/904ceeb316c64b68b46d01ba72b71b39.png
具体代码实现:
class Solution {
public boolean isAnagram(String s, String t) {
// 先定义一个数组,作为简单的哈希表结构,数组的大小设置为26,用来表示每一个字符,a代表下标围为0,z则代表下标为25
int[] record = new int[26];
// 先遍历字符串s
for(int i = 0 ; i < s.length; i++){
// 对于s中的每一个字符s[i], 与 ‘a’作差,实际上是该字符的ASCII与a的ASCII值作差,不用记具体的每个字符对应的ASCII值
// ‘a’ - ‘a’就是对应下标0,‘b’-‘a’就是对应下标1,以此类推...
record[s[i] - 'a'] ++ ;
}
// 再遍历字符串t
for(int j = 0 ; j < t.length; j++){
// 对于遍历字符串t的时候,如果遇到了,就将对应下标位置上的值减一
record[t[j] - 'a'] --;
}
// 再遍历一遍record数组,看有没有0
for(int k = 0 ;k < record.length;k++){
if(record[k] !=0){
return false;
}
}
return true;
}
}
有一个小细节,传入的s和t都是字符串,而我们的record是一个int类型的数组:
(1)字符串的 length() 方法:当你有一个字符串变量,比如 s 或 t,你可以通过调用 s.length() 或 t.length() 来获取字符串的长度。这是因为 String 类提供了一个名为 length 的方法,它返回字符串的长度。
(2)数组的 length 属性:对于数组,比如 int[] record,你不需要调用方法来获取其长度。数组有一个内置的属性 length,它是一个 int 类型的值,直接表示数组的长度。
二、leetcode 349 (两数组的交集)
1、原题链接
2、解题思路及代码展
本题就是采用哈希表的思路,因为哈希表就是用来解决“一个元素是不是在某个集合中出现过”
(1)先遍历nums1,将其中出现过的数值放入到哈希表中。
(2)再遍历nums2,看nums2中每一个元素是否在哈希表中出现过,如果出现过则放回到result中。
然后,还需确定我们使用的哈希表的具体结构,使用数组的话,如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
C++语言中,set ,关于set,C++ 给提供了如下三种可用的数据结构:std::set,std::multiset和std::unordered_set,std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
PS:使用数组来做哈希的题目,前提是题目都限制了数值的大小。
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
// 使用HashSet
// 如果传入的nums1和nums2为空,直接返回
if(nums1.length == 0 || nums1 == null || nums2.length == 0 || nums2 == null){
return new int[0];
}
// 定义一个HashSet,存遍历nums1的内部元素的结果
Set<Integer> set1 = new HashSet<>();
// 定义一个HashSet,存放最终取得交集的结果
Set<Integer> resultSet = new HashSet<>();
// 遍历nums1
for(int i = 0;i<nums1.length;i++){
set1.add(nums1[i]);
}
// for(int i : nums1){
// set1.add(i);
// }
// 遍历数组2,判断哈希表set1中是否存在该元素
for(int j = 0 ; j < nums2.length ; j++ ){
if(set1.contains(nums2[j])){
resultSet.add(nums2[j]);
}
}
// for(int j : nums2 ){
// if(set1.contains( j ){
// resultSet.add(j);
// }
// }
// 完成两次遍历之后,最终的结果是存在resultSet中,但是其是HashSet格式的,所以需要将其装欢为数组
// 方式一,直接将HashSet转格式为数组
return resultSet.stream().mapToInt(x -> x).toArray();
// 方式二,再准备一个数组,将resultSet中的元素依次取出
// int[] result = new int[resultSet.size()];
// int k = 0;
// for(int i :resultSet ){
// result[j++] = i;
// }
// return result;
}
}
三、leetcode 202 快乐数
1、原题链接
2、解题思路及代码展示
如果一个数字不是快乐数,说明出现了无限循环,就是在求和的过程中,某一个sum值会重复出现,而哈希表就是用来快速判断一个元素是否出现在集合里。
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
判断sum是否重复出现就可以使用unordered_set。
还有一个难点,就是求和的过程中,需要对取某一个数各个位上的单数操作比较熟悉,也是本题关键之一。
直接上代码:
class Solution {
public boolean isHappy(int n) {
//利用一个HashSet,无序且无重复元素
Set<Integer> record = new HashSet<>();
// 只有当我们计算的平方和不为1且我们的record不包含这个值(没出现过),才进行循环体出道作
while(n != 1 && !record.contains(n)){
record.add(n);
n = getNextNumber(n);
}
return n == 1;
}
// 这个函数主要是用来 : 每次得到一个数得平方和之后,把这个新得平方和作为输入参数,返回一个新得计算结果
private int getNextNumber(int n){
int res = 0;
while(n > 0){
int temp = n % 10; // 将数字最右边一位的数字取出来
res += temp * temp; // 计算平方
n = n / 10; // 将最右边一位数字去掉
}
return res;
}
}
四、leetcode 001 (两数之和)
1、原题链接
2、解题思路及代码展示
前文说过,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题的解题思路,需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,换一种说法就是是否出现在这个集合?
(1)本题我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key-value键值对的结构来存放,key来存元素,value来存下标,那么使用map正合适。
而前文一直使用的数组和set来做本题的就会有局限:
① 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
② set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断是否存在而且还要记录下标位置,set也不可用。
此时就要选择另一种数据结构:map ,map是一种key-value的存储结构,可以用key保存数值,用value再保存数值所在的下标。
(2)在明确了使用map这一数据结构之火,还要确定两点:
① map用来做什么?map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)
② map中key和value分别表示什么?某一元素具体的值就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标,即: {key:数据元素,value:数组元素对应的下标}
所以也就是,在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
class Solution {
public int[] twoSum(int[] nums, int target) {
// 定义一个结果数组,用来存找到的两个元素对应的下标,因为本题是求两数之和所以对应的就是数组容量初始化为
int[] result = new int[2];
// 空值判断
if(nums == null || nums.length == 0){
return result;
}
// 使用Map这一数据结构来存储已经遍历过的元素
// {key:数据元素,value:数组元素对应的下标}
Map<Integer,Integer> map = new HashMap<>();
// 开始遍历数组
for(int i = 0 ; i < nums.length ; i++ ){
// nums[i]代表了当前值,然后要在map集合中找的能匹配的值是target-nums[i]
int tempTarget = target - nums[i];
// 在map中查询,看有没有key(数据元素)是和tempTarget匹配的
if(map.containsKey(tempTarget)){
// 如果存在的话,我们就是一个对应的结果,注意我们这里存的是对应下表(也就是map中的value)
result[1] = i;
result[0] = map.get(tempTarget); //map.get() 传入参数为key,得到key对应的value
break; //找到一组结果就可以返回结果了,有就行,break直接跳出当前循环
}
map.put(nums[i],i);
}
return result;
}
}
提示:这里对文章进行总结: