介绍
与其他语言不同,在 Go 中,字典(maps)是无序的集合。在这个实验中,我们将学习如何对字典进行排序以及如何更灵活地使用它们。
关键概念:
- 字典排序
- 交换字典中的键和值
- 字典切片
- 以切片为值的字典
- 字典的引用类型特性
与其他语言不同,在 Go 中,字典(maps)是无序的集合。在这个实验中,我们将学习如何对字典进行排序以及如何更灵活地使用它们。
关键概念:
在 ~/project
目录下创建一个 map.go
文件:
touch ~/project/map.go
将以下代码写入 map.go
文件中:
package main
import (
"fmt"
)
func main() {
// 声明并初始化一个键为字符串、值为整数的 map
// 该 map 存储学生姓名及其分数
m := map[string]int{
"Alice": 99, // 每个键值对代表一个学生及其分数
"Bob": 38,
"Charlie": 84,
}
// 使用 for-range 循环遍历 map
// 'key' 代表学生姓名,'value' 代表分数
for key, value := range m {
fmt.Println(key, value)
}
fmt.Println("\n插入数据")
// 演示如何向 map 中添加新的键值对
// 语法为:map[key] = value
m["David"] = 25
// 再次遍历以显示更新后的 map 内容
// 注意每次执行的顺序可能不同
for key, value := range m {
fmt.Println(key, value)
}
}
运行程序:
go run ~/project/map.go
程序的输出可能如下:
Charlie 84
Bob 38
Alice 99
插入数据
David 25
Charlie 84
Bob 38
Alice 99
输出可能会有所不同,因为插入数据的顺序是不固定的。这是 Go 中 map 的核心特性——当你遍历它们时,它们不保证元素的任何特定顺序。
你可以尝试多次运行程序,并观察插入数据的顺序可能会有所不同。这说明你不能依赖 map 中元素的顺序。
但有时我们需要在插入数据后对字典进行排序。如何实现这一点呢?
由于 map 本身无法排序,我们可以将 map 转换为切片,然后对切片进行排序。
首先,让我们学习如何按键对字典进行排序。
以下是步骤:
sort
包对切片进行排序。将以下代码写入 map.go
文件中:
package main
import (
"fmt"
"sort"
)
func main() {
// 初始化字典
m1 := map[int]string{
3: "Bob",
1: "Alice",
2: "Charlie",
}
keys := make([]int, 0, len(m1)) // 初始化切片并设置容量。这是一种性能优化——切片将初始分配与 map 大小相等的内存,避免重新分配。
for key := range m1 {
// 将键转换为切片
keys = append(keys, key)
}
// 使用 sort 包对键切片进行排序。`sort.Ints()` 函数专门用于对整数切片进行排序。
sort.Ints(keys)
for _, key := range keys {
// 现在输出是有序的
fmt.Println(key, m1[key])
}
}
运行程序:
go run ~/project/map.go
程序的输出如下:
1 Alice
2 Charlie
3 Bob
通过这种方法,我们实现了基于键对字典进行排序。键被提取到切片中,排序后用于从 map 中查找并打印对应的值。
在解释如何按值排序之前,让我们先学习如何交换字典中的键和值。
交换键和值意味着在字典中互换键和值的位置,如下图所示:
实现代码非常简单。将以下代码写入 map.go
文件中:
package main
import "fmt"
func main() {
m := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
m2 := map[int]string{}
for key, value := range m {
m2[value] = key
}
fmt.Println(m2)
}
运行程序:
go run ~/project/map.go
程序的输出如下:
map[38:Bob 84:Charlie 99:Alice]
这段代码的本质是从原始字典中提取键和值,然后将它们重新插入字典中,但角色互换。它简单而直接。请注意,由于 map 是无序的,交换后的 map 的输出顺序可能会有所不同。
结合按键排序字典和交换字典中键和值的逻辑,我们可以按值对字典进行排序。
以下是实现方式:我们交换键和值,然后将交换后的键(原始值)作为排序的基础。接着,我们使用排序后的“键”(原始值)从交换后的 map 中查找对应的原始键。
将以下代码写入 map.go
文件中:
package main
import (
"fmt"
"sort"
)
func main() {
// 初始化字典
m1 := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
// 初始化反转后的字典
m2 := map[int]string{}
for key, value := range m1 {
// 通过交换键值对生成反转后的字典 m2
m2[value] = key
}
values := make([]int, 0) // 初始化排序切片
for _, value := range m1 {
// 将原始字典的值转换为切片
values = append(values, value)
}
// 使用 sort 包对值切片进行排序
sort.Ints(values)
for _, value := range values {
// 现在输出是有序的
fmt.Println(m2[value], value)
}
}
运行程序:
go run ~/project/map.go
程序的输出如下:
Bob 38
Charlie 84
Alice 99
现在我们已经按值对字典进行了排序。我们将值转换为切片,对切片进行排序,然后使用排序后的值从交换后的 map 中检索并打印对应的原始键。
sort.Slice
排序如果 Go 版本高于 1.7,我们可以使用 sort.Slice
函数快速按键或值对 map 进行排序。sort.Slice
允许你指定一个自定义的比较函数。
以下是一个示例:
package main
import (
"fmt"
"sort"
)
func main() {
m1 := map[int]int{
21: 99,
12: 98,
35: 17,
24: 36,
}
type kv struct {
Key int
Value int
}
var s1 []kv
for k, v := range m1 {
s1 = append(s1, kv{k, v})
}
sort.Slice(s1, func(i, j int) bool {
return s1[i].Key < s1[j].Key
})
fmt.Println("按键升序排序:")
for _, pair := range s1 {
fmt.Printf("%d, %d\n", pair.Key, pair.Value)
}
}
运行程序:
go run ~/project/map.go
输出如下:
按键升序排序:
12, 98
21, 99
24, 36
35, 17
在这个程序中,我们使用了一个结构体 kv
来存储 map 中的键值对。然后,我们使用 sort.Slice()
函数和一个匿名比较函数对结构体切片进行排序。这个比较函数(func(i, j int) bool
)根据结构体的 Key
字段确定排序顺序。
我们还可以通过修改这个比较函数,使用 sort.Slice
按键降序或按值升序对 map 进行排序。这为我们提供了极大的灵活性,可以根据需要以不同方式对 map 数据进行排序。
创建一个 map2.go
文件,并修改上一节的代码,以按值降序对 map 进行排序。
预期输出:
运行程序:
go run ~/project/map2.go
按值降序排序:
21, 99
12, 98
24, 36
35, 17
要求:
map2.go
文件应放置在 ~/project
目录中。sort.Slice
函数。你需要修改上一节示例中 sort.Slice
使用的比较函数。正如我们可以使用数组或切片来存储相关数据一样,我们也可以使用元素为字典的切片。这使我们能够保存 map 数据的集合,这对于处理结构化信息非常有用。
将以下代码写入 map.go
文件中:
package main
import "fmt"
func main() {
// 声明一个 map 切片并使用 make 初始化
var mapSlice = make([]map[string]string, 3) // 创建一个容量为 3 的切片,其中每个元素可以是一个 `map[string]string`。
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("初始化")
// 为切片的第一个元素赋值
mapSlice[0] = make(map[string]string, 10) // 在第一个索引处创建一个 map。
mapSlice[0]["name"] = "labex"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "Paris"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
运行程序:
go run ~/project/map.go
程序的输出如下:
index:0 value:map[]
index:1 value:map[]
index:2 value:map[]
初始化
index:0 value:map[address:Paris name:labex password:123456]
index:1 value:map[]
index:2 value:map[]
这段代码演示了如何初始化一个 map 切片。最初,切片中的每个元素都是一个空的 map。然后我们创建并赋值一个 map 给第一个元素,并填充数据。这展示了如何管理一个 map 列表。
我们也可以使用以切片为值的字典来存储更多的数据。这使我们能够将多个值与 map 中的单个键关联起来,从而有效地创建“一对多”的关系。
将以下代码写入 map.go
文件中:
package main
import "fmt"
func main() {
var sliceMap = make(map[string][]string, 3) // 声明一个键为字符串、值为字符串切片的 map。3 是容量提示。
key := "labex"
value, ok := sliceMap[key] // 检查键是否存在
if !ok {
value = make([]string, 0, 2) // 如果不存在,初始化一个新的切片并设置容量。
}
value = append(value, "Paris", "Shanghai") // 将值追加到切片中。
sliceMap[key] = value // 将切片设置为 map 中键的值。
fmt.Println(sliceMap)
}
运行程序:
go run ~/project/map.go
程序的输出如下:
map[labex:[Paris Shanghai]]
这段代码首先声明了一个值类型为切片的 map。它在向关联切片添加新项之前检查键是否存在,展示了处理切片 map 的常见模式。
数组是值类型,因此对它们进行赋值或传递给函数时会创建副本。对副本的修改不会影响原始数组。然而,map 是引用类型。这意味着对 map 进行赋值或传递给函数时不会创建完整的副本,而是通过引用传递。
这一点非常重要,因为函数内部的修改会影响原始的 map 数据。
将以下代码写入 map.go
文件中:
package main
import "fmt"
func modifyMap(x map[string]int) {
x["Bob"] = 100 // 修改作为参数传递的 map。
}
func main() {
a := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
// 由于 map 是通过引用传递的,modifyMap 中的修改会改变原始字典
modifyMap(a)
fmt.Println(a) // map[Alice:99 Bob:100 Charlie:84]
}
运行程序:
go run ~/project/map.go
程序的输出如下:
map[Alice:99 Bob:100 Charlie:84]
在这个示例中,我们展示了字典的引用传递特性。modifyMap
函数会修改原始 map,因为 a
是对同一底层 map 数据的引用。在将 map 传递给函数时,理解这种行为至关重要。
在本实验中,我们学习了 Go 中 map 的高级用法,包括:
理解这些概念将使你能够在实际应用中更有效地使用 Go 的 map。