Go 字典排序

GolangGolangBeginner
立即练习

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

介绍

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

关键概念:

  • 字典排序
  • 交换字典中的键和值
  • 字典切片
  • 以切片为值的字典
  • 字典的引用类型特性

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("`Golang`")) -.-> go/BasicsGroup(["`Basics`"]) go(("`Golang`")) -.-> go/DataTypesandStructuresGroup(["`Data Types and Structures`"]) go(("`Golang`")) -.-> go/FunctionsandControlFlowGroup(["`Functions and Control Flow`"]) go(("`Golang`")) -.-> go/AdvancedTopicsGroup(["`Advanced Topics`"]) go/BasicsGroup -.-> go/values("`Values`") go/DataTypesandStructuresGroup -.-> go/slices("`Slices`") go/DataTypesandStructuresGroup -.-> go/maps("`Maps`") go/DataTypesandStructuresGroup -.-> go/structs("`Structs`") go/FunctionsandControlFlowGroup -.-> go/for("`For`") go/FunctionsandControlFlowGroup -.-> go/functions("`Functions`") go/AdvancedTopicsGroup -.-> go/sorting("`Sorting`") subgraph Lab Skills go/values -.-> lab-149095{{"`Go 字典排序`"}} go/slices -.-> lab-149095{{"`Go 字典排序`"}} go/maps -.-> lab-149095{{"`Go 字典排序`"}} go/structs -.-> lab-149095{{"`Go 字典排序`"}} go/for -.-> lab-149095{{"`Go 字典排序`"}} go/functions -.-> lab-149095{{"`Go 字典排序`"}} go/sorting -.-> lab-149095{{"`Go 字典排序`"}} end

字典排序

~/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 转换为切片,然后对切片进行排序。

按键排序

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

以下是步骤:

  • 将字典的键转换为切片。切片是一个有序的动态数组,我们可以对其进行排序。
  • 使用 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 的输出顺序可能会有所不同。

按值排序

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

以下是实现方式:我们交换键和值,然后将交换后的键(原始值)作为排序的基础。接着,我们使用排序后的“键”(原始值)从交换后的 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 的高级用法,包括:

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

理解这些概念将使你能够在实际应用中更有效地使用 Go 的 map。

您可能感兴趣的其他 Golang 教程