Go容器:映射(map)

目录

1 散列表

2 map

2.1 map的声明和初始化

2.2 遍历 map 的键值对

2.3 从map中删除键值对

2.4 清空map中的所有元素

2.5 能够在并发环境中使用的map —— sync.Map

2.6 在函数间传递映射

参考


1 散列表

散列表,也称为哈希表,是设计精妙、用途广泛的数据结构之一。它是一个拥有键值对元素的无序集合。在这个集合中,键的值是唯一的,键对应的值可以通过键来获取、更新和删除。无论这个散列表有多大,这些操作基本上是通过常量时间的键比较就可以完成。

<提示> 大多数编程语言中映射关系容器使用两种算法:散列表和平衡树。

【散列表】

散列表可以简单描述为一个“数组”(俗称“桶”),数组的每个元素是一个列表。根据散列函数(也称为哈希函数)获得每个元素的特征值,将特征值作为映射的键。如果特征值重复,表示元素发生碰撞。碰撞的元素将放在同一个特征值的列表中进行保存。散列表查找时间复杂度为O(1),和数组一致。最坏的情况为O(n),n为元素总数。散列表需要尽量避免元素碰撞以提供查找效率,这样就需要对“通”进行扩容,每次扩容,元素需要重新放入“桶”中,较为耗时。

映射的散列表包含一组桶。在存储、删除或者查找键值对的时候,所有操作都要先选择一个桶。把操作映射时指定的键传给映射的散列函数,就能选中对应的桶。这个散列函数的目的是生成一个索引,这个索引最终将键值对分布到所有可用的桶里。

随着映射存储的增加,索引分布越均匀,访问键值对的速度就越快。如果你在映射里存储了10 000 个元素,你不希望每次查找都要访问 10 000 个键值对才能找到需要的元素,你希望查找键值对的次数越少越好。对于有 10 000 个元素的映射,每次查找只需要查找 8 个键值对才是一个分布得比较好的映射。映射通过合理数量的桶来平衡键值对的分布。

映射生成散列键的大体过程如下图所示:

在上面的例子中,键时字符串,代表颜色。这些字符串会通过散列函数(或者称为哈希函数)转换成一个数值(散列值)。这个
数值落在映射已有桶的序号范围内表示一个可以用于存储的桶的序号,比如,Brick red 字符串经过散列函数转换后得到的散列值为05,那么05号这个桶就可以用来存储或者查找指定的键值对。

总而言之,我们只要记住一件事:映射是一个存储键值对的无序集合。

【平衡树】

平衡树类似于有父子关系的一颗数据树,每个元素放入树中时,都要与一些结点进行比较。平衡树的查找时间复杂度始终为O(log n)。

2 map

Go语言提供的映射关系容器为map。map使用散列表(hash)实现。map 是散列表的引用,map的类型是 map[K]V,其中K 和 V 是字典的键和值的数据类型。map 中所有的键都拥有相同的数据类型,同时所有的值也都拥有相同的数据类型,但是键的类型和值的类型不一定相同。键的数据类型必须是可以通过操作符== 来进行比较的数据类型,所以map 可以检测一个键是否已经存在。

2.1 map的声明和初始化

Go语言中,map的声明格式如下:

map[keyType]valueType
  • keyType:键类型。
  • valueType:值类型。

1、可以使用Go语言的内置函数make()创建一个map。

//创建一个映射,键的类型是string,值的类型是int
ages := make(map[string]int)
//添加键值对
ages["alice"] = 24
ages["charlie"] = 34

2、也可以使用map字面量来新建一个带初始化键值对的映射。

ages := map[string]int{
    "alice": 24,
    "charlie": 34,
}

3、创建一个空map的方法。

//方式1-使用make()函数
dict1 := make(map[string]int)

//方式2-使用map字面量
dict2 := map[string]int{}

 《说明》上面代码中使用make()函数或者使用map字面量创建的空map,只是说明map中没有任何键值对,并不是说它是nil map,即此时的dict1 和 dict2 是不等于nil 的。空映射和nil映射的区别:(1)前者已经有内存分配了,而后者没有内存分配;(2)空映射可以添加键值对数据,而nil映射是无法添加键值对数据的,否则会导致运行时错误,即宕机。所以,在向map添加元素前,必须初始化map。

虽然,不能向nil映射添加元素,但是可以对nil映射执行包括查找元素,删除元素,获取map元素个数(len),执行for...range循环等操作都是可以的,因为这些和空map的行为一致。

4、map的元素访问也是通过下标的方式。

//根据指定的键输出对应的值
fmt.Println(ages["alice"])  //24

//尝试查找一个不存在的键,那么返回的将是ValueType的默认值
fmt.Println(ages["jack"])   //0

//某些情况下,需要明确知道查询的键是否在map中,可以使用一种特殊的写法来实现
age, ok := ages["charlie"]
fmt.Println(age, ok)        //34 true

age, ok = ages["mike"]
fmt.Println(age, ok)        //0 false

《说明》

(1)通过下标的方式访问map中的元素总是会有值的。如果键在map中,将得到键对应的值;如果键不在map中,将得到map中值类型的零值。

(2)和slice一样,两个map之间是不可比较的,唯一合法的比较就是和nil 做比较。如果要判定两个map是否是相等的,即是否拥有相同的键值对,必须通过循环的方法实现。示例代码如下:

//判定两个[string]int类型的ma是否相等
func equal(x, y map[string]int) bool {
    if len(x) != len(y) {
        return false
    }
    for k, xv := range x {
        if yv, ok := y[k]; !ok || yv != xv {
            return false
        }
    }
    return true
}

代码说明:注意我们如何使用 !ok 来区分“元素不存在” 和 “元素存在当值为零” 的情况。如果简单地写成 xv != y[k], 那么下面的函数调用将错误地判定两个map是相等的。

equal(map[string]int{"A": 0}, map[string]int{"B": 42})

代码说明:因为 y["A"] 是不存在的,所以返回的值 yv=0, 而 x["A"]=0,即返回的值xv=0,所以就会错误地判定这两个map是相等的。

 【##】map的键和值类型要求

map的键类型必须是可以比较的类型。而map的值类型除了基础数据类型外,还可以是复合数据类型,例如slice类型、map类型等。

2.2 遍历 map 的键值对

map的遍历可以使用 for...range 循环来完成。

ages := map[string]int{
    "alice": 24,
    "charlie": 34,
    "jack": 36,
    "mike": 45,
}

//遍历键值对
for k, v := range ages {
    fmt.Println(k, v)
}

//只遍历值,将不需要的键改成匿名变量的形式即可
for _, v := range ages {
    fmt.Println(v)
}

//只遍历键,无须将值改为匿名变量的形式,忽略值即可
for k := range ages {
    fmt.Println(k)
}

运行结果:

alice 24
charlie 34
jack 36
mike 45
24
34
36
45
alice
charlie
jack
mike

<注意> 遍历map时,输出键值对的顺序与填充顺序无关。不能期望map 在遍历时返回某种期望顺序的结果。这是因为不同的实现方法使用不同的散列算法,得到不同的元素顺序。

 如果需要特定顺序的遍历结果(如按key的字典顺序输出键值对),正确的做法是显式地给键排序,修改上面的代码如下:

package main

import (
    "fmt"
    "sort"              //sort函数的使用需要导入sort包
)

func main(){
    ages := map[string]int{
        "alice": 24,
        "mike": 45,
        "jack": 36,
        "charlie": 34,
    }
    fmt.Printf("未排序的输出结果:\n")
    for k, v := range ages {
        fmt.Println(k, v)
    }
    
    //声明一个切片保存map的key数据
    var agesSlice []string

    //将map中的字符串类型的键添加到字符串切片中
    for k := range ages {
        agesSlice = append(agesSlice, k)
    }

    //对切片进行排序,对传入的字符串切片进行字符串字符的升序排列
    sort.Strings(agesSlice)

    fmt.Printf("\n排序后的输出结果:\n")
    for _, s := range agesSlice {
        fmt.Println(s, ages[s])
    }
}

运行结果:

未排序的输出结果:
jack 36
charlie 34
alice 24
mike 45

排序后的输出结果:
alice 24
charlie 34
jack 36
mike 45

代码说明:通过输出结果可以看到,未排序的输出结果与初始化map时的填充顺序并不是一致的。而使用sort包中的Strings()函数对map中字符串类型的键进行升序排列后,输出的结果是按照字符串的升序排列输出map的键值对的。

【##】对nil 映射赋值时的语言运行时错误

//通过声明映射创建一个 nil 映射
var colors map[string]string

//添加一个键值对
colors["Red"] = "#da1337"

fmt.Println(colors["Red"])

/**
**运行时会报如下错误:
Runtime Error:
panic: runtime error: assignment to entry in nil map
*/

 《说明》可以通过声明一个未初始化的映射(map)来创建一个值为nil 的映射(称为nil 映射)。nil 映射不能用于存储键值对,否则会产生一个运行时错误。map类型的零值是nil,表示没有引用任何散列表。

【##】从map获取值并判断键是否存在

//获取键对应的值
colors := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
value, exists := colors["Blue"]

//这个键是否存在
if exists {
    fmt.Println(value)
}

另一种选择是,只返回键对应的值,然后判断这个value值是不是零值来确定键是否存在。这种方法只能用在映射存储的值都是非零值的情况。

//获取键Blue对应的值
value := colors["Blue"]

//判断这个键是否存在
if value != "" {
    fmt.Println(value)
}

<提示> 在Go语言中,通过键来获取对应的值时,即使这个键不存在也总会返回一个值。在这种情况下,返回的是该值对应类型的零值。如整型返回的是0,string类型返回的是空字符串("")。

<注意> map元素不是一个变量,不可以获取它的地址。

address := &scene["china"]  //编译错误,无法获取map元素的地址

 <说明> 我们无法获取map元素的地址的一个原因是map的增长可能会导致已有元素被重新散列到新的存储位置,这样就可能使得获取到的地址是无效的。

2.3 从map中删除键值对

在Go语言中,可以使用内置的delete() 函数从map中删除一组键值对,delete() 函数的格式如下:

delete(map, key)
  • map:为map实例。
  • key:为要被删除的map键值对中的键。
scene := map[string]int{
    "route": 66,
    "brazil": 4,
    "china": 960,
}
delete(scene, "brazil")

for k, v := range scene {
    fmt.Println(k, v)
}

运行结果:

route 66
china 960

上例中,使用delete() 函数将 "brazil": 4,从scene这个map中删除了。

2.4 清空map中的所有元素

其实,Go语言并没有为map提供任何清空map所有元素的函数或是方法。清空map的唯一方法是重新创建一个新的map,并使旧的map的引用变量指向这个新的map实例。由于旧的map实例没有引用变量指向它了,Go语言的垃圾回收器会自动回收旧map的内存空间,从而达到清空map的目的。

2.5 能够在并发环境中使用的map —— sync.Map

Go语言内置的map 不是线程安全的,并发安全的map 可以使用标准包sync 中的map,即sync.Map。

下面来看并发情况下读写map时会出现的问题,代码如下:

//创建一个[int]int类型的map
m := make(map[int]int)

//开启一段并发代码
go func() {
    //不停地对map执行写入操作
    for{
        m[1] = 1
    }
}()

//开启另一段并发代码
go func(){
    //不停地对map执行读取操作
    for{
        _ = m[1]
    }
}()

//无限循环,让并发程序在后台执行
for{

}

程序运行时,报了错误:fatal error: concurrent map read and map write。并发的map读写。也就是说使用了两个并发函数不断地对map进行读写操作而发生了竞态问题。map会对这种并发操作进行检查并提前发现。

需要并发读写时,一般的做法是加锁,但是这种处理方式的性能不高。Go语言在1.9版本中提供了一种效率较高的并发安全的map,即sync.Map。sync.Map 和 map 不同,它不是以Go语言原生形态提供,而是在标准库sync包中实现的。

【##】sync.Map 的特性

  • 无须初始化,直接声明即可使用。
  • sync.Map 不能使用map的方式进行取值和设置值等操作,而是使用sync.Map的方法进行调用。可以使用Store()方法添加键值对,使用Load()方法获取指定键的对应值,Delete()方法删除键值对。
  • 使用 Range()方法配合一个回调函数进行map的遍历操作,通过回调函数返回内部遍历出来的键值对。Range()方法中回调函数的返回值功能是:需要继续迭代遍历时,返回true;终止迭代遍历时,返回false。

示例:并发安全的sync.Map的演示代码。

package main

import (
    "fmt"
    "sync"
)

func main(){
    var scene sync.Map
    
    //将键值对保存到sync.Map
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)

    //从sync.Map读取指定键的值
    fmt.Println(scene.Load("london"))   //输出结果:100 true
    fmt.Println(scene.Load("shenzhen")) //输出结果:<nil> false

    //根据键删除指定的元素
    scene.Delete("london")

    //遍历所有的sync.Map中的键值对
    scene.Range(func(k, v interface{}) bool {
        fmt.Println("iterate:", k, v)
        return true
    })
}

运行结果:go run syncMap.go

100 true
<nil> false
iterate: egypt 200
iterate: greece 97

代码说明:

  • 声明了一个sync.Map类型的变量scene。需要注意的是,sync.Map 不能使用内置的 make() 函数创建。
  • 使用Load()方法查询指定键的值,Load()方法有两个返回值,第一个是返回键对应的值,第二个是返回元素是否存在的一个布尔值。如果元素存在,返回值和true;如果元素不存在,返回nil 和 false。
  • 使用Range()方法遍历sync.Map,遍历时需要提供一个回调函数,我们这里使用匿名函数来实现,函数参数类型为 interface{},每次Range()在遍历一个元素时,都会调用这个匿名函数,将得到的键值存放到 k, v中。当这个匿名函数返回值为true时,表示继续遍历下一个元素,直到读取完所有的元素。

<提示> sync.Map没有提供获取map元素数量的方法,替代方法是在遍历sync.Map时自行计算元素数量。sync.Map为了保证并发安全有一些性能上的损失,因此在非并发情况下,使用Go语言内置的原生map相比使用sync.Map会有更好的性能表现。

2.6 在函数间传递映射

在函数间传递映射(map)并不会复制该映射的副本,而只是传递该映射的引用副本,然后通过映射的引用副本在函数中访问映射的元素,这个特性和切片(slice)是类似的,这样做可以保证很小的成本来访问映射。

package main

import (
    "fmt"
)

func main(){
    // 创建一个映射,存储颜色以及颜色对应的十六进制代码
    colors := map[string]string{
        "AliceBlue":   "#f0f8ff",
        "Coral":       "#ff7F50",
        "DarkGray":    "#a9a9a9",
        "ForestGreen": "#228b22",
    }

    // 显示映射里的所有颜色
    for key, value := range colors {
        fmt.Printf("Key: %s Value: %s\n", key, value)
    }

    // 调用函数来移除指定的键
    removeColor(colors, "Coral")

    // 显示映射里的所有颜色
    fmt.Println()
    for key, value := range colors {
        fmt.Printf("Key: %s Value: %s\n", key, value)
    }
}

// removeColor 将指定映射里的键值对删除
func removeColor(colors map[string]string, key string) {
    delete(colors, key)
}

运行结果:go run mapDemo.go

Key: AliceBlue Value: #f0f8ff
Key: Coral Value: #ff7F50
Key: DarkGray Value: #a9a9a9
Key: ForestGreen Value: #228b22

Key: AliceBlue Value: #f0f8ff
Key: DarkGray Value: #a9a9a9
Key: ForestGreen Value: #228b22

代码说明:可以看到,在调用了removeColor()函数之后,main函数里引用的映射colors中不再有Coral颜色了。

参考

《Go语言从入门到进阶实战(视频教学版)》

《Go语言实战》

《Go程序设计语言》

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值