哈希表 Map
官方定义:
哈希表是一个无序的key/value键值对的集合,通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
在go创建一个map的时候,默认会在底层创建一个哈希表,并且让map引用这个哈希表。
一个map就是一个哈希表的引用,map类型可以写为map[key的类型]value的类型。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。
其中key必须是支持==比较运算符的数据类型(所以可以用数组作为key,但是不能通过切片作为key),所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法。
我们可以通过以下方式创建一个map哈希表
例子1:make创建哈希表
map1 := make(map[string]string) // 初始化一个key和value都是字符串的哈希表
map1["name"] = "zbp" // 通过下表的方式添加元素,这里不能用单引号,单引号表示字符类型,双引号才是字符串类型!
map1["age"] = "24" // 哈希表的value必须是相同类型,所以这里不能是数字24,只能是字符串24
fmt.Println(map1)
可以通过在make中传递第三参限定这个哈希表的容量
make(map[string]string, 100)
如果只声明了map1,但是没有赋值,map1就是一个零值map(是nil),此时往map1中添加元素会报错:不能往一个nil哈希表中添加元素。
例子2:直接创建哈希表
map2 := map[string]string {
"name" : "zbp",
"age" : "24", // 这里必须要有逗号,反则会报错
}
fmt.Println(map2)
例子3:通过下表赋值的方式添加元素
map3 := map[string]string{}
map3["name"] = "zbp"
map3["age"] = "24"
fmt.Println(map3)
fmt.Println(map3["hobby"]) // 获取哈希表中不存在的元素不会报错,会默认返回value类型的零值
例子4: delete删除某个元素
map4_score := map[string]int{ // 班级数学平均分
"class1" : 86,
"class2" : 90,
}
fmt.Println(map4_score)
delete(map4_score, "class2")
fmt.Println(map4_score)
delete(map4_score, "class4") // 删除不存在的key不会报错
fmt.Println(map4_score)
在go中,获取或者删除一个不存在的元素都不会报错,获取一个不存在的元素会返回一个value类型的零值。比如,string类型的value会返回空字符串,int类型的value会返回一个0。而且x += y
和x++
等简短赋值语法也可以用在map上
map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配(开辟)更大的内存空间,从而可能导致之前的地址无效。
_ = &ages["bob"] // compile error: cannot take address of map element
例子5: Map的遍历
map5 := map[string]string {
"name" : "zbp",
"age" :"24",
"hobby" : "programme",
}
for key,value := range map5 {
fmt.Printf("key : %s, value : %s\n", key, value)
}
例子6:定义map的时候,map默认是一个nil,没有指向任何哈希表
var map6 map[string]int
fmt.Println(map6 == nil) // true
map6["num1"] = 1 // 报错,不能为一个没有指向哈希表的map变量进行元素赋值
fmt.Println(map6)
// var声明后,正确的定义方式是
var map6 map[string]int
map6 = map[string]int{} // 先给map6赋值一个空的哈希表,底层创建出一个哈希表,此时才可以为map6添加元素。或者直接在赋值这里创一个有元素的哈希表
map6["num1"] = 1
fmt.Println(map6)
这里对比一下 m := make(map[string]string) 与 var m map[string]string的区别,前者在底层创建了哈希表并且让m指向这个哈希表,后者只是声明了m为哈希表类型,但是底层没有创建哈希表,m是一个nil。所以前者的m可以直接通过下标的方式创建元素,后者不行。
例子7:判断哈希表是否存在某个key的元素
map7 := make(map[string]string)
map7["name"] = "zbp"
age, ok := map7["age"]
fmt.Println(age, ok)
if _, ok := map7["age"]; !ok{ // 用在if中可以用;隔开,if只判断分号后面的表达式
fmt.Println("Key of age does not exist!")
}
我们知道,获取一个哈希表中不存在的元素会返回一个对应类型的零值但不会报错,因此我们不知道这个key是否存在。此时我们把 map7[“xxx”] 返回的返回值给两个变量,第二个返回值是一个bool描述这个元素是否存在于这个map中。
例子8:比较两个map是否相同
func equal(x, y map[string]string) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if _, ok := y[k]; !ok || y[k] != xv { // 如果y[k]不存在,或者y[k]不等于x[k]则两个map不相等
return false
}
}
return true
}
func main(){
map8 := map[string]string{
"k1" : "v1",
"k2" : "v2",
}
map9 := map[string]string{
"k1" : "v1",
"k2" : "v2",
}
fmt.Println(equal(map8, map9))
}
哈希表和slice一样不能用 == 比较。只能遍历的方式比较每一个元素,并且不能单纯的比较每一个元素是否相等,还要判断这个元素是否存在。
如果只判断元素是否相等,那么下面这种情况也会返回true
equal(map[string]int{"A": 0}, map[string]int{"B": 42})
例子9:key为不能比较的类型该如何办到
我们知道,map的key必须是能用 == 比较的类型,但是如果我们非要让key是一个slice该怎么办?很简单,将这个slice转为字符串再作为key即可
func main(){
slice1 := []string {"a", "b", "c"}
slice2 := []string {"e", "f"}
map10 := map[string]bool{}
map10[k(slice1)] = true
map10[k(slice2)] = false
fmt.Println(map10)
}
func k(slice []string) string {
return fmt.Sprintf("%q", slice)
}
PS:fmt.Printf与Sprintf的相同点是都是格式化字符串。区别是前者会打印,后者不会打印而是会将格式化结果返回给变量。
%q和%s的区别,前者会格式化为带双引号的字符串,后者没有双引号。
例子10:vlaue为复合类型
map11 := map[string]map[string]int{} // 创建一个vlaue是map的变量,这个变量记录不同班级每个学生的数学成绩
class1 := map[string]int{
"小明": 100,
"小红": 98,
}
class2 := map[string]int{
"小黑" : 99,
"小白" : 60,
}
map11["class1"] = class1
map11["class2"] = class2
fmt.Println(map11)
在go中没有set集合类型,我们知道集合类型的特点就是没有重复值,而且元素是乱序的,其实map都符合这个条件,所以我们可以用map来间接的实现一个set类型,思路是只用到map的key而不去使用map的value(因为map的key就是不可重复且乱序的),当然vlaue也可能有用,但是最多是放true和false,而不会放其他东西(我们一般会以空结构体struct{}作为集合哈希表的值,因为空结构体不占任何空间)。
例子11:对值为nil的map进行操作
我们知道map类型的零值是nil,如果我们对值为nil的哈希表进行访问或者修改会发生什么呢?
func main() {
var x map[string]string // 此时哈希表x的值是一个nil
fmt.Println(x["name"]) // 得到空字符
x["name"] = "zbp" // 直接报错
fmt.Println(x["name"])
fmt.Printf("%T, %v", x, x)
}
结论是:对一个nil的哈希表访问不存在的元素不会报错,但是会返回value类型的零值,由于value类型是string类型,所以会返回字符串的零值即空字符串。
对一个nil的哈希表添加或修改元素会报错。
如果我们想对一个空哈希表添加元素,我们就应该加一句 x = map[string]string {} ,这样x就不是nil,x就可以添加和修改元素了。
但是对于切片而言(我们知道切片的零值也是nil),对一个nil的切片进行访问其中的元素会报错,但append添加元素不会报错,因为append内部做了很多兼容性的措施,如果检测到传进来的是一个nil的切片就会为它赋值一个空切片再做操作。
例如:
var y []string // y是nil
y = append(y, "hello") // y变成 {“hello”}
但是:
var y []string
fmt.Println(y[0]) // 报错,报错内容是下标0超出下标范围