1. Map(映射)
Map,也有语言称为字典,还可以称为哈希表、Dic、映射,在go语言中称为Map。
特点如下:
- 长度与容量可变。
- 存储的元素是key-value对(键值对),value可变。
- key无序且不重复,由于map的key是无序存储的,千万不要从遍历结果来推测其内部顺序。
- 不可索引,因为map是非线性数据结构,需要通过key来访问。
- 零值不可用,也就是说,必须要用make或字面常量构造(初始化前)。
零值可用,这里说的是初始化后,但零值对应的结果,得看具体的数据类型,如int=0,bool=true。- 引用类型 ,也有header,其中包含指向底层hash表的指针。
- 哈希表。
- len返回key value的对数,cap不能使用,因为map在内部使用哈希表来存储键值对。
1.1 哈希算法
1.1.1 哈希算法的特点
- 给定一个x,一定会返回一个y。并且只要hash算法不变,x不变,y永远也不会变。
- 输入x可以是任意长度,输出y是固定长度(bit),但不同的x有可能得到相同的y。
· 假设固定长度为8bit,那y就会有256种状态。由于x可以是任意长度,那就说明x的值域会非常大,最后必须由hash算法处理并收敛到256的其中之1。
· 但这带来了一个问题,可能会出现不同的x由hash处理后输出了相同的y(哈希冲突),因为x的范围无限大,y的范围相对x来说是有限的,就一定会出现冲突,但有办法解决。- hash算法效率高,计算速度快。
- x随便一个微小的变化,都会引起y的巨大变化。
- 不能由y反推出x,hash算法不可逆
1.1.2 常见的哈希算法
- MD5:(Message Digest Algorithm 5)信息摘要算法5,输出是128位。运算速度较SHA-1快。
应用场景:
· 用户密码存储
· 上传、下载文件完整性校验
· 大的数据的快速比对,例如字段很大,增加一个字段存储该字段的hash值,对比内容开是否修改- SHA:SHA(Secure Hash Algorithm)安全散列算法,包含一个系列算法,分别是SHA-1(bit)、SHA-224(bit)、SHA-256(bit)、SHA-384(bit),和SHA-512(bit)。
应用场景:
· 数字签名防篡改。
1.1.2.1 MD5示例
package main
import (
"crypto/md5"
"fmt"
)
func main() {
h := md5.New()
h.Write([]byte("abc")) // 求abc的md5哈希值
// 由于md5默认处理完是一个超大整数,所以这里转成16进制
fmt.Printf("%x\n", h.Sum(nil))
// 重新开启一个新的mdb,需要使用Reset
h.Reset()
h.Write([]byte("abb"))
fmt.Printf("%x", h.Sum(nil))
}
==========调试结果==========
900150983cd24fb0d6963f7d28e17f72
ea01e5fd8e4d8832825acdd20eac5104
1.1.2.2 SHA256示例
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
h1 := sha256.New()
h1.Write([]byte("abc"))
s := fmt.Sprintf("%x", h1.Sum(nil))
fmt.Println(s, len(s))
}
==========调试结果==========
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 64
1.2 哈希表内存模型
1.2.1 原理简介
map采用哈希表实现。Go的map类型也是引用类型,有一个标头值hmap,指向一个底层的哈希表。
哈希表Hash Table:
- 当把一个key放入map中时,map会在系统重开辟一块连续的内存空间,每一小块的内存空间都会有一个唯一的编号且占用内存空间大小一致。这里可以把每小块内存空间称之为“桶”。
- 同时Go会使用哈希函数计算key的哈希码,这个哈希码会对应一个桶编号,随即把value存储到桶中(编址有序存储无序)。后续找这个key只需通过map的首地址+元素偏移量(桶编号)就能快速定位特定的key。
使用key寻找value的时间复杂度是O(1)。
1.2.2 哈希冲突
如果两个键的哈希码相同(key太多),它们可能会被放在同一个桶中(冲突),这里有两种办法解决:
- 开地址法(Open Addressing): 它有很多种算法,比如线性探测(Linear Probing),从发生冲突的位置开始,线性地探测下一个空闲位置。如果到达表尾,可能会从表头开始继续探测。
- 链地址法(Chaining): 哈希表的每个桶中都会维护一个链表,当发生哈希冲突的时候,会把这些key对应的值通过链表链接起来,以此存储到同一个桶中。
但注意,链表的长度过长,会导致时间复杂度从O(1)退化到O(n),不过go内置的负载因子机制完美的解决了这个问题。随着存储的数据越来越多,始终还是会出现大量的哈希冲突,这里有个专业术语称为“负载因子”。
当负载因子超过一定的阈值时,就会触发map的动态扩容,扩容通常涉及到创建一个更大的哈希表,并将旧表中的所有元素重新映射(rehash)到新表中。
1.2.3 总结
对于map来说,使用key查找元素是最佳方式,时间复杂度为O(1)(通过key进行哈希运算,得到哈希码,拿着哈希码找到对应的桶的位置,最终得到对应的value)。
1.3 定义Map
1.3.1 方式一:字面量定义
适合小批量定义map。
package main
import "fmt"
func main() {
// m1后面的map是类型,int是key的类型,string是value的类型。
var m1 = map[int]string{
// 也可以使用短格式定义
1: "abc",
2: "xyz",
3: "t",
}
// len返回key:value的对数,cap在map中不能使用。
fmt.Print("map的key value:", m1, "\nmap中的key value对数:", len(m1)

最低0.47元/天 解锁文章
628

被折叠的 条评论
为什么被折叠?



