1. Map(映射)
Map,也有语言称为字典,还可以称为哈希表、Dic、映射,在go语言中称为Map。
特点如下:
- 长度与容量可变。
- 存储的元素是key-value对(键值对),value可变。
- key无序且不重复,由于map的key是无序存储的,千万不要从遍历结果来推测其内部顺序。
- 不可索引,因为map是非线性数据结构,需要通过key来访问。
- 零值不可用,也就是说,必须要用make或字面常量构造。
- 引用类型 ,也有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示例
go
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示例
go
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。
go
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))
}
==========调试结果==========
map的key value:map[1:abc 2:xyz 3:t]
map中的key value对数:3
1.3.2 方式二:make定义
如果有大量的key value需要写入,可以事先定义好一个大容量的map。
go
package main
import "fmt"
func main() {
// 注意:string是key的类型,int是value的类型,map容量为10。容量可以多给点。
m2 := make(map[string]int, 10) // 容量可以不指定,但默认容量非常小。
fmt.Print("map的key value:", m2, "\nmap中的key value对数:", len(m2))
}
==========调试结果==========
map的key value:map[]
map中的key value对数:0
1.3.3 错误的定义方式
在使用map前,必须要初始化,否则未初始化的map为nil,运行就报错(panic),也就是上面说的0值不可用。
换句话说就是哈希表一旦创建,就必须得有哈希空间,哪怕没有东西放里面。
go
```go
package main
import "fmt"
func main() {
// 这样不可以,在使用map前,必须要初始化,否则未初始化的map为nil,运行就报错(panic),也就是上面说的0值不可用。
var m1 map[string]int
m1["1"] = 1
fmt.Println(m1)
}
1.4 新增或修改key
go
package main
import "fmt"
func main() {
m1 := make(map[string]int)
m1["100"] = 200 // 新增
fmt.Println(m1)
m1["100"] = 300 // 修改(key存在则修改value,不存在就新增key)
fmt.Println(m1)
}
==========调试结果==========
map[100:200]
map[100:300]
1.5 查询key
通过key查,时间复杂度O(1)
go
package main
import "fmt"
func main() {
m1 := make(map[string]int)
m1["100"] = 200
fmt.Println(m1)
fmt.Println(m1["100"]) // 通过指定key找value
// 如果查找的key不存在,则返回0
// 但是这种方式没办法判断key是不是真的存在
fmt.Println(m1["1"]) // 可以通过下面的方式解决
v, ok := m1["1"] // ok默认是bool类型,key存在为true,不存在为false
if !ok {
fmt.Println("查询的key不存在!", v)
}
}
//合起来写就是这样的
// 快捷方式:m1["1"].ex然后ide会出现一个exits给你选择,这时tab或者点击exits都会自动生成if
if v, ok := m1["1"]; !ok {
fmt.Println("查询的key不存在!", v)
}
==========调试结果==========
map[100:200]
200
0
查询的key不存在! 0
查询的key不存在! 0
1.6 遍历key
map中不能使用索引,所以遍历只能用for range,不能使用for i。
注意:map的key是无序的,千万不要从遍历结果来推测其内部顺序。不论什么数据结构,遍历这种操作的时间复杂度都是O(n),所以尽可能的避免遍历操作。
go
package main
import "fmt"
func main() {
m1 := make(map[string]int)
m1["zhagnsan"] = 99
m1["lisi"] = 60
for k, v := range m1 {
fmt.Println(k, v)
}
}
==========调试结果==========
zhagnsan 99
lisi 60
1.7 删除key
go
package main
import "fmt"
func main() {
m1 := make(map[string]int)
m1["zhagnsan"] = 99
m1["lisi"] = 60
fmt.Println("删除前:", m1)
delete(m1, "lisi")
delete(m1, "waner") //被删除key不存在,不会报错
fmt.Println("删除后:", m1)
}
==========调试结果==========
删除前: map[lisi:60 zhagnsan:99]
删除后: map[zhagnsan:99]
1.8 取map长度
注意:cap是无法对map进行统计的。
go
package main
import "fmt"
func main() {
m1 := make(map[string]int)
m1["zhagnsan"] = 99
m1["lisi"] = 60
fmt.Println(len(m1))
}
==========调试结果==========
2
2. 排序
Go的标准库提供了sort库,用来给线性数据结构排序、二分查找。
注意:sort库只能用于切片,常用的以下几种:
- sort.Ints:对 int 类型的切片进行排序。
- sort.Float64s:对 float64 类型的切片进行排序。
- sort.Strings:对 string 类型的切片进行排序。
sort库下的所有排序方法默认输出都是升序。
常见的排序算法如下:
- 冒泡排序(Bubble Sort)
- 选择排序(Selection Sort)
- 插入排序(Insertion Sort)
- 堆排序(Heap Sort)
- 快速排序(Quick Sort)等
2.1 int排序
sort.Int使用的是快速排序算法。
go
package main
import (
"fmt"
"sort"
)
func main() {
a := []int{1, 0, 3, 7, 5, 8}
fmt.Printf("%v, %p, %p\n", a, &a, &a[0])
sort.Ints(a)
fmt.Printf("%v, %p, %p\n", a, &a, &a[0])
//降序排序
sort.Sort(sort.Reverse(sort.IntSlice(a)))
fmt.Printf("%v, %p, %p\n", a, &a, &a[0])
}
==========调试结果==========
[1 0 3 7 5 8], 0xc0000a8060, 0xc0000d6030
[0 1 3 5 7 8], 0xc0000a8060, 0xc0000d6030
[8 7 5 3 1 0], 0xc0000a8060, 0xc0000d6030
2.2 string排序
go
package main
import (
"fmt"
"sort"
)
func main() {
arr := []string{"banana", "apple", "pear", "grape"}
fmt.Println("Original array:", arr)
sort.Strings(arr)
fmt.Println("Sorted array: ", arr)
}
==========调试结果==========
Original array: [banana apple pear grape]
Sorted array: [apple banana grape pear]
3. 二分查找
二分查找的前提是数据必须升序排序。
语法:sort.SearchInts(a []int, x int) int
- a:一个有序的整数切片。
- x:要搜索的目标整数。
- int:返回值。如果找到目标值,返回其在切片中的索引;如果没有找到,返回一个插入点的索引,这个索引是目标值应该被插入的位置,以便保持切片的有序性。
3.1 二分查找的运行逻辑
1. 确定搜索范围: 初始化两个指针,一个指向数据的起始位置,另一个指向数据的结束位置。
2. 计算中间位置: 在每一步中,计算搜索范围的中间索引。
3. 比较中间元素: 将数组中间位置的元素与目标值进行比较。
4. 更新搜索范围: 根据比较结果,更新搜索范围。如果目标值小于中间元素,更新结束位置指针;如果目标值大于中间元素,更新起始位置指针。
5. 重复过程: 继续步骤2到4,直到找到目标值或搜索范围为空。
3.2 示例
3.2.1 排序前
go
package main
import (
"fmt"
"sort"
)
func main() {
a := []int{1, 0, 3, 7, 5, 8}
// 找6在什么地方,没有就返回适合的插入点索引
i := sort.SearchInts(a, 6)
fmt.Println(i)
}
==========调试结果==========
3 // 没找到6,返回索引3,意思是系统觉得6最佳的插入点是在7这里,7包括后面的元素都往后挪,但实际插入顺序并不对
3.2.2 升序排序后
go
package main
import (
"fmt"
"sort"
)
func main() {
a := []int{1, 0, 3, 7, 5, 8}
sort.Ints(a) // 排序
fmt.Println(a)
i := sort.SearchInts(a, 6)
fmt.Println(i)
}
==========调试结果==========
[0 1 3 5 7 8]
4 // 排序后插入点就对了