哈希表
哈希表是根据关键码的值而直接进行访问的数据结构。通俗讲,就是根据索引直接访问结构中的数据。一般来哈希表用来判断一个元素是否出现在集合里。将数据映射到哈希表上是由哈希函数完成
哈希函数
哈希函数如上图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把数据映射为哈希表上的索引数字了。如果hashcode得到的数值大于哈希表大小,对其做一个取模操作,保证得到的数值不大于tableSize。
哈希碰撞
上图中,两个数据同时被哈希函数映射至一个索引,这一现象称为哈希碰撞。
原因:哈希函数的输入空间远大于其输出空间,因此存在多个不同的输入映射到相同的哈希值的可能。
一般有如下几种解决办法:
1、开放地址法
(1)线性探测
索引已经存在,则在原有索引基础上后移一位,直至不发生哈希冲突
(2)再平方探测
按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。
要注意平方不能超过容量的值
Size=16的时候,找备选的单元只能取i=1,2,3,也就是距离冲突单元1,4,9个单位的位置了。
(3)伪随机探测
按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。
2、链式地址法(HashMap的哈希冲突解决方法)
对于相同的值,使用链表进行连接。使用数组存储每一个链表。
就是hashmap的底层原理 :数组+链表 就是没有红黑树
优点:
(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点:
(1) 指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。
3、再哈希法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
上述哈希碰撞相关内容来源于博主@DXXME
242. 有效的字母异位词
此题比较简单,直观的感受就是计算每个字符串中字母出现的个数,然后判断两个字符串每个字母出现次数是否相同。完全符合哈希表的使用规律
func isAnagram(s string, t string) bool {
var s_Length map[string]int = make(map[string]int)
var t_Length map[string]int = make(map[string]int)
// len获取的是字符的字节长度,两种方法计算字符长度:utf8.RuneCountInString()
// len([]rune(s))
// s[i]取出的是字符对应的utf8编码
if len([]rune(s)) == len([]rune(t)) {
fmt.Printf("%d\n", len(s))
for i := 0; i < len([]rune(s)); i++ {
s_Length[string([]rune(s)[i])]++
t_Length[string(t[i])]++
}
for n := 0; n < len([]rune(s)); n++ {
if s_Length[string(s[n])] != t_Length[string(s[n])] {
return false
}
}
return true
}
return false
}
此处对Go语言操作字符串进行一个小总结。
1、计算字符串的长度
go中string底层是通过byte数组实现的,使用len(str)方法实际是在计算字节长度。对于len()函数,获取的是字符的字节长度,而不是字符长度。Go中有两个方法能计算字符长度:utf8.RuneCountInString()和[ ]rune()方法。前者直接计算字符长度,后者是将字符转换成字符数组,再配合len()函数使用。
2、获取字符串中的字符
直接使用索引方式访问返回的是字符的utf8编码
3、Go的字符类型
byte(uint8):代表ASCII码的一个字符
rune(int32):代表一个UTF-8字符,当处理中文等其他符合字符时,需要使用该类型
由于此处直接使用两个map类型,需要占用大量的空间,对于此题目的优化方式,直接创建一个空间大小为26的数组,记录每个字母出现的次数。下列代码来源代码随想录
func isAnagram(s string, t string) bool {
s_num := [26]int{}
for _, n := range s {
fmt.Println(n)
s_num[n-rune('a')]++
}
for _, n := range t {
s_num[n-rune('a')]--
}
return s_num == [26]int{} //判断其是否减为0
}
349. 两个数组的交集
此题也比较简单,只需要一个数组中的出现过的数存入map中,再判断另外一个数组中的元素在map中是否存在,存在就表示是两个数组的交集。但是这题有个小坑:如果遇到重复的元素,都会存入result数组中。解决方法:1、每次匹配成功后,需要删除map中该元素。2、使用set,但是Go原生数据结构中,没有set数据结构,只有第三方包("github.com/deckarep/golang-set")有实现,后续可以研究一下这个包。
func intersection(nums1 []int, nums2 []int) []int {
var map1 map[int]int = make(map[int]int)
// 动态数组
var result []int
for _, n := range nums1 {
map1[n]++
}
for _, n := range nums2 {
if _, ok := map1[n]; ok {
result = append(result, n)
delete(map1, n)
}
}
return result
}
202.快乐数
emm,快乐数有点不快乐。没做过这题真的很难想到如何利用map来AC。最开始一直想着硬算,一路算下去,直到算到1结束。但是这样没有办法判断fasle的情况如何停止。后来发现,如果算到中间出现了之前出现过的值,后面继续算下去也没有意义。这下就可以用到map的特性了,把每次计算的结果存入hash,当出现重复时便返回false即可
func isHappy(n int) bool {
map1 := make(map[int]int)
for {
n = getSum(n)
if n == 1 {
return true
}
if map1[n] > 0 {
return false
} else {
map1[n]++
}
}
}
1. 两数之和
这个题目还挺经典的,是使用map解决hash问题的典型案例。这个题目不是那么直接可以用map解决的问题。val存元素索引,key保存nums[i]即可。然后再判断target-nums[i]是否在map中,存在则返回两者的索引
func twoSum(nums []int, target int) []int {
var result []int
map1 := make(map[int]int)
for i := 0; i < len(nums); i++ {
if map1[target-nums[i]] > 0 {
result = append(result, i)
result = append(result, map1[target-nums[i]]-1)
} else {
map1[nums[i]] = i + 1
}
}
return result
}