Go 字典排序

GolangBeginner
立即练习

介绍

与其他语言不同,在 Go 中,字典(map)是无序集合。在本实验中,我们将学习如何对字典进行排序,以及如何更灵活地使用它们。

核心概念:

  • 字典排序
  • 交换字典中的键和值
  • 字典切片
  • 以切片作为值的字典
  • 字典的引用类型特性
这是一个引导实验,提供逐步指导以帮助你学习和练习。请仔细遵循说明完成每个步骤并获得实践经验。历史数据表明,这是一个初学者级别的实验,完成率为 83%。它获得了学习者 88% 的好评率。

字典排序

~/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("\nInsert Data")

 // 演示如何向 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

Insert Data
David 25
Charlie 84
Bob 38
Alice 99

输出结果可能会有所不同,因为插入数据的顺序是不固定的。这是 Go map 的一个核心特性——当你遍历它们时,不保证元素的任何特定顺序。

你可以尝试多次运行该程序,并观察插入数据的顺序可能会发生变化。这说明你不能依赖 map 中元素的顺序。

但有时我们需要在插入数据后对字典进行排序。我们该如何实现呢?

由于 map 本身无法排序,我们可以将 map 转换为切片,然后对切片进行排序。

按键排序

首先,让我们学习如何按键(key)对字典进行排序。

步骤如下:

  • 将字典的键转换到切片中。切片是有序的动态数组,我们可以对其进行排序。
  • 使用 Go 内置的 sort 包对切片进行排序。
  • 使用字典的查找方法获取相应的值。由于我们知道切片中键的顺序,我们可以在 map 中查找这些值,它们将按照排序后的键顺序出现。

将以下代码写入 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 的输出顺序可能会有所不同。

按值排序

结合按键排序的逻辑和交换键值的逻辑,我们可以实现按值(value)对字典进行排序。

其工作原理是:我们交换键和值,然后将交换后的键(即原始值)作为排序的基础。接着,我们使用排序后的“键”(原始值)在交换后的 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("Sorted in ascending order by key:")
 for _, pair := range s1 {
  fmt.Printf("%d, %d\n", pair.Key, pair.Value)
 }
}

运行程序:

go run ~/project/map.go

输出如下:

Sorted in ascending order by key:
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
Sorted in descending order by value:
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("Initialization")
 // 为切片的第一个元素赋值
 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[]
Initialization
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 数据。

将以下代码写入 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 的高级用法,包括:

  • 按键或值对 map 进行排序,这涉及将 map 转换为切片然后对切片进行排序。
  • 交换 map 中的键和值,从而创建一个角色反转的新 map。
  • 使用 map 切片,允许创建相关 map 数据的集合。
  • 使用以切片作为值的 map,允许将多个值与单个键关联。
  • map 的引用类型特性,即对传递的 map 进行的更改会反映在原始 map 中。

掌握这些概念将使你在实际应用中更有效地使用 Go map。