代码随想录day6
哈希表基础
哈希表概念:
首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。
哈希表是根据关键码的值而直接进行访问的数据结构。
其实直白来讲其实数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数
哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来哈希碰撞登场
一般哈希碰撞有两种解决方法, 拉链法和线性探测法
拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
242.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:输入: s = "anagram", t = "nagaram"
输出: true
示例 2:输入: s = "rat", t = "car"
输出: false
说明:你可以假设字符串只包含小写字母。
思路:
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。
定义一个数组叫做record用来上记录字符串s里字符出现的次数。
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
在遍历字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
// for range
func isAnagram(s string, t string) bool {
//定义一个记录26个字母的数组
var array [26]int
//遍历数组s,将数组s的内容作为数组array的下标记录进去。s[i]-'a'的范围在0-25的啊
for i:=0;i<len(s);i++{
array[s[i] - 'a'] += 1
}
//遍历数组t,将数组t的内容作为数组array的下标记录进去。t[i]-'a'的范围在0-25的啊
//只不过上面那个是记录增加,这个是删掉记录
for i:=0;i<len(t);i++{
array[t[i] - 'a'] -= 1
}
//最终比较array存放的s内容是不是清空了,是就代表两者都有同样多的不同字母。
return array == [26]int{}
}
// 普通for循环
func isAnagram(s string, t string) bool {
var array [26]int
for i:=0;i<len(s);i++{
array[s[i] - 'a']++
}
for i:=0;i<len(t);i++{
array[t[i] - 'a']--
}
return array == [26]int{}
}
349. 两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。说明:输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序
示例 1: 输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2: 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
提示: 1 <= nums1.length, nums2.length <= 1000 0 <= nums1[i], nums2[i] <= 1000
思路:
这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。那么用数组来做哈希表也是不错的选择。但是要注意,使用数组来做哈希的题目,是因为题目都限制了数值的大小。而且如果值比较大、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
1、遍历nums1,把nums1中的值作为key或者下标存放进map或者array。
2、遍历nums2,查看map[nums2_value]或者array[nums2_value]是否存在,存在就说明是重合的,取出来重合的,然后将值删除。
// 数组写法
func intersection(nums1 []int, nums2 []int) []int {
var array [1024]int
for _, v := range nums1 {
array[v] += 1 // 将nums1中的值作为array的下标。
}
res := []int{}
for _, v := range nums2 {
if array[v] != 0 { //如果array[v] != 0,也就是说数组存有nums2的元素作为下标的
res = append(res, v ) //代表他俩重合了啊,把值放进res
array[v] -= array[v] //取完之后记得删除,这里不是--,而是删对应的值。防止重复。
}
}
return res
}
// map写法
func intersection(nums1 []int, nums2 []int) []int {
a_map := make(map[int]int,0)
for _, v1 := range nums1 {
a_map[v1]++
}
res := []int{}
for _, v2 := range nums2 {
count, ok := a_map[v2]
if ok && count > 0 {
res = append(res, v2)
a_map[v2] -= a_map[v2]
}
}
return res
}
202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例: 输入:19 输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
分析:
这道题目看上去貌似一道数学问题,其实并不是!题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
func isHappy(n int) bool {
a_map := make(map[int]int)
for n != 1 {
if a_map[n] == 1 {
break
}
a_map[n] = 1
n = getHappySum(n)
}
return n == 1
}
func getHappySum(x int) int {
sum := 0
for x != 0 {
sum += (x%10)*(x%10)
x = x/10
}
return sum
}
1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3: 输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案
思路:
1、暴力循环
枚举所有可能组合。复杂度O(N²)
func twoSum(nums []int, target int) []int {
for i:=0;i<len(nums);i++{
for j:=i+1;j<len(nums);j++{
if nums[i] + nums[j] == target{
return []int{i,j}
}
}
}
return []int{}
}
2、哈希表
首先我在强调一下 什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题呢,我就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
那么我们就应该想到使用哈希法了。
因为我们不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素比配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
要想清楚map的key和value是要存说明东西,一般来说key用来存放某些元素的值。要找什么存放什么,对吗?
//普通循环
func twoSum(nums []int, target int) []int {
a_map := make(map[int]int, 0)
for i:=0;i<len(nums);i++{
value, ok := a_map[target - nums[i]] // 判断一个map[key]是否存在
if ok {
return []int{i, value} // 如果存在则返回当前的下标和另一个的下标,因为他俩下标所对应的值等于target
}
// 第一次肯定不存在,后续如果不存在就要把数组的值和下标存下来。
if !ok{
a_map[nums[i]] = i
}
}
return nil
}
//for range 循环
func twoSum(nums []int, target int) []int {
a_map := make(map[int]int, 0)
for i, v := range nums {
value, ok := a_map[target - v]
if ok {
return []int{i, value}
}
if !ok {
a_map[v] = i
}
}
return nil
}
今日小结:
1、学习了哈希表基础,了解了哈希表的映射关系。一般哈希表都是用来快速判断一个元素是否出现集合里。
2、学习了 242.有效的字母异位词 ,根据快速判断一个元素是否出现集合里 我们知道用哈希表是比较合适的,那么选择数组还是map呢?我们说当值的范围比较小、比较连续的时候使用数组,当值特别大、比较分散的时候用map。所以第一题我们可以使用数组来解决。遍历第一个数组,将值存放进行数组的下标,再遍历第二个数组,将第二个数组作为下标放进我们建立的数组里面,只不过对应的操作是--,第一个是++。最后比较我们建的数组是否为空就可以了。还要注意nums[I] - ‘a’是什么意思。
3、学习了 349. 两个数组的交集 根据快速判断一个元素是否出现集合里 我们知道用哈希表是比较合适的。我们可以先把第一个数组的元素的值作为key存放进map/数组,然后在遍历第二个数组的时候查询第二个数组的值有没有作为key在map/数组中出现,出现了我们就说这个值是重复的,然后就计数,别忘记计完数把该key所对应的value清空。
4、学习了 202. 快乐数,首先我们学习了快乐数的定义,当一个数拆分为单个单个位数的数字的时候,将每位数平方后相加,看看是否等于1,是等于1就是快乐数,不是1就要继续进行判断。在这题我们知道除了判断是不是1之外,我们还要判断拆分计算完之后相加的值是不是已经判断过了,这样才能找到另一个退出循环的条件啊。所以这个拆分累加的Sum很重要,要判断它是不是已经出现过一次,根据快速判断一个元素是否出现集合里 我们知道用哈希表是比较合适的。还有就是判断快乐数的函数的构建,我们是通过拆分每个数,就是通过x%10来得到每个数(从右边开始),然后对其平方后累加给sum,然后别忘记x/10让其进行下去往右边移动继续拆分计算。比如125,拆分的时候就是x%10 就是拆分出5了,然后进行平方然后累加,然后就是要x/10变成12,然后再进行x%10拆分出2,然后进行平方后累加,以此类推计算完所有。我们就得到了125的第一次快乐数计算的结果。然后根据这个结果我们判断是不是1,有没有出现过。
5、学习了 1. 两数之和。梦破碎的地方。我的第一道算法题。当时想了好久好久还是懵懵懂懂,在之后的很长一段时间我还是懵懵懂懂。但是今天我终于略微懂一点了。这个我不好总结,下次一定,拉回前面看看详细的吧。
6、感谢观看,内容许多来自 代码随想录。