Go数组arr,切片slice和映射map
- 1 数组
- 1.1 快速入门
- 1.2 数组的内存布局
- 1.3 四种初始化数组的方式
- 1.4 数组的遍历
- 1.5 注意事项以及分析
- 1.6 数组反转
- 1.7 二维数组
- 2 切片
- 2.1 快速入门
- 2.2 内存解析
- 2.3 切片的使用和遍历
- 2.4 注意事项和细节说明
- 2.5 string和slice关系
- 2.6 切片练习题
- 3 map映射
- 3.1 快速入门
- 3.2 map的crud操作
- 3.3 map的遍历
- 3.4 map切片
- 3.5 map排序
- 3.6 使用细节
- 4 综合题目(难度较大,可略~)
1 数组
数组可以存放多个
同一类型数据。数组也是一种数据类型,在Go中,数组是值类型。
1.1 快速入门
数组定义格式:
var 数组名 [数组大小]数据类型
步骤:
- 定义数组
- 将数据存入数组
- 根据所需获取数据进行计算
案例讲解:
func main() {
//实现的功能:给出五个学生的成绩,求出成绩的总和,平均数:
//给出五个学生的成绩:--->数组存储:
//1.定义一个数组:
var scores [5]int
//20 将成绩存入数组:
scores[0] = 95
scores[1] = 91
scores[2] = 39
scores[3] = 60
scores[4] = 21
//3.求和:
//定义一个变量专门接收成绩的和:
sum := 0
for i := 0; i < len(scores); i++ { //i: 0,1,2,3,4
sum += scores[i]
}
//4.平均数:
avg := sum / len(scores)
//输出
fmt.Printf("成绩的总和为:%v,成绩的平均数为:%v", sum, avg)
}
输出结果:成绩的总和为:306,成绩的平均数为:61
1.2 数组的内存布局
首先我们来看一下数组的地址
func main() {
//声明数组:
var arr [3]int
//获取数组的长度:
fmt.Println(len(arr))
//打印数组:
fmt.Println(arr) //[0 0 0]
//证明arr中存储的是地址值:
fmt.Printf("arr的地址为:%p\n", &arr)
//第一个空间的地址:
fmt.Printf("arr第一个空间的地址为:%p\n", &arr[0])
//第二个空间的地址:
fmt.Printf("arr第二个空间的地址为:%p\n", &arr[1])
//第三个空间的地址:
fmt.Printf("arr第三个空间的地址为:%p\n", &arr[2])
}
输出结果:
3
[0 0 0]
arr的地址为:0xa00a0d0
arr第一个空间的地址为:0xa00a0d0
arr第二个空间的地址为:0xa00a0d4
arr第三个空间的地址为:0xa00a0d8
从上面的结果可以看出以下几点:
- 如果是int类型数组,默认不赋值都为0,那么可以得出,不赋值的情况,默认值就是基本数据类型的默认值。
- 获取数组的地址值,跟获取基本数据类型一样:
&arr
即可 - arr第一个空间的地址值跟直接获取的地址值相同:
&arr = &arr[0]
- 因为int类型根据电脑是
int32
,那么一个int数据类型占用4个字节
,所以第二个数组的空间地址值与第一个空数组就相差4个数
,并且发现这是16进制
的数。
1.3 四种初始化数组的方式
func main() {
// 方式一
var arr1 [3]int = [3]int{1, 2, 3}
// 方式二
var arr2 = [3]int{1, 2, 3}
// 方式三
var arr3 = [...]int{1, 2, 3}
// 方式四
var arr4 = [3]string{1: "Tom", 0: "Jack", 2: "marry"}
fmt.Println(arr1)
fmt.Println(arr2)
fmt.Println(arr3)
fmt.Println(arr4)
}
输出结果:
[1 2 3]
[1 2 3]
[1 2 3]
[Jack Tom marry]
一般情况,我们定义数组都是用这种方式:arr := [5]int{1,2,3}
这样,后面两个会给默认值0
1.4 数组的遍历
方式一:常规的遍历
前面的方式就是,这里不写了
方式二:使用for-range结构遍历
(键值循环) for range结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道,for range 语法上类似于其它语言中的 foreach 语句,一般形式为:
for index, value := range arr {
...
}
(1)arr就是你要遍历的数组名
(2)每次遍历得到的索引用index接收,每次遍历得到的索引位置上的值用value
(3)index、value的名字随便起名 i、v index、value
(4)index、value属于在这个循环中的局部变量
(5)你想忽略某个值:用_就可以了:
for _, val := range scores {
...
}
简单使用:
func main() {
arr := [5]int{1, 2, 3}
for k, v := range arr {
fmt.Printf("数组第%v个的值为:%v\n", k, v)
}
for _, v := range arr {
fmt.Printf("数组值为:%v\n", v)
}
}
输出结果:
数组第0个的值为:1
数组第1个的值为:2
数组第2个的值为:3
数组第3个的值为:0
数组第4个的值为:0
数组值为:1
数组值为:2
数组值为:3
数组值为:0
数组值为:0
1.5 注意事项以及分析
-
数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其
长度是固定的,不能动态变化
。 -
var arr []int 这时arr就是一个slice切片,切片下一步讲,等等说。
-
数组中的元素可以是任何数据类型,包括值类型和引用类型,
但是不能混用
。 -
数组创建后,如果没有赋值,有默认值
- 数值类型数组:默认: 0
- 字符串数组:默认:“”
- bool数组:默认值:false
-
使用数组的步骤 1. 声明数组并开辟空间 2.给数组各个元素赋值 3.使用数组
-
数组的下标是从0开始的
-
数组下标必须在指定范围内使用,否则报 panic;数组越界。
-
Go的数组属于
值类型
,在默认情况下是值传递,因此会进行值拷贝。数组间不会互相影响。 -
如果想在其他函数中,去修改原来的数组,可以使用引用传递(指针方式)
案例讲解:
// 通过普通方式,修改数组的值 func updateArr(num int) { num = 10 } // 通过指针方式,修改数组的值 func updateArrPointer(num *int) { *num = 10 } func main() { arr := [5]int{1, 2, 3} fmt.Println("普通值传递方式") fmt.Println("修改前,数组的值:", arr[0]) updateArr(arr[0]) fmt.Println("修改后,数组的值:", arr[0]) fmt.Println("通过指针方式") fmt.Println("修改前,数组的值:", arr[0]) updateArrPointer(&arr[0]) fmt.Println("修改后,数组的值:", arr[0]) }
输出结果:
普通值传递方式
修改前,数组的值: 1
修改后,数组的值: 1
通过指针方式
修改前,数组的值: 1
修改后,数组的值: 10
1.6 数组反转
要求:随机生成五个数,并将其反转打印
思路:
- 随机生成五个数 , rand.Intn() 函数
- 当我们得到随机数后,就放到一个数组 int数组
- 反转打印 , 交换的次数是 len / 2, 倒数第一个和第一个元素交换, 倒数第2个和第2个元素交换
概念一:随机数生成
在随机数生成中,种子(seed)是一个用于初始化随机数生成器的值。通过给定特定的种子,我们可以确保每次运行程序时生成的随机数序列是不同的。
在 Go 语言中,种子通常以整数形式表示,并且可以通过调用
rand.Seed(seed int64)
方法来设置种子值。种子值可以是任何整数,但是如果种子值相同,那么生成的随机数序列也将相同。常用的设置种子的方法是使用当前时间作为种子。在这种情况下,我们可以使用
time.Now().UnixNano()
函数来获取当前时间的纳秒级别时间戳,并将其作为种子值传递给rand.Seed(seed int64)
方法。例如,
rand.Seed(time.Now().UnixNano())
将使用当前时间的纳秒级别时间戳作为种子值,从而使得每次运行程序生成的随机数序列都是不同的。这样做的目的是为了增加随机性,使得生成的随机数更加随机和分散。需要注意的是,在实际应用中,如果我们需要可重复的随机数序列,可以使用相同的种子值来初始化随机数生成器。这样可以确保每次运行程序时,生成的随机数序列都是相同的。
概念二:生成随机数新方法
根据Go 1.17版本,新增了一个
math/rand.Source
接口和math/rand.NewSource
函数,用于生成随机种子。在新版本中,随机数生成器的实现和种子值的管理分离开来,使得随机数生成器更加灵活而且易于维护。
所以生成随机数的代码
func main() {
intArr := [5]int{}
randGen := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < len(intArr); i++ {
intArr[i] = randGen.Intn(100)
}
fmt.Println("随机数:", intArr)
}
输出结果:随机数: [66 6 59 99 64]
最后的代码为:
func main() {
//要求:随机生成五个数,并将其反转打印
//思路
//1. 随机生成五个数 , rand.Intn() 函数
//2. 当我们得到随机数后,就放到一个数组 int数组
//3. 反转打印 , 交换的次数是 len / 2, 倒数第一个和第一个元素交换, 倒数第2个和第2个元素交换
intArr := [5]int{}
len := len(intArr)
for i := 0; i < len; i++ {
intArr[i] = rand.Intn(100)
}
fmt.Println("交换前~=", intArr)
//反转打印 , 交换的次数是 len / 2,
//倒数第一个和第一个元素交换, 倒数第2个和第2个元素交换
temp := 0 //做一个临时变量
for i := 0; i < len/2; i++ {
temp = intArr[len-1-i]
intArr[len-1-i] = intArr[i]
intArr[i] = temp
}
fmt.Println("交换后~=", intArr)
}
输出结果:
交换前~= [7 65 83 17 12]
交换后~= [12 17 83 65 7]
1.7 二维数组
二维数组是一种常见的数据结构,通常用于表示表格、矩阵等具有行列结构的数据。在 Go 语言中,二维数组的定义和初始化方式如下:
// 定义一个 3 行 4 列的二维数组
var arr2D [3][4]int
// 初始化二维数组
arr2D = [3][4]int{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
}
上面的代码中,arr2D
是一个 3 行 4 列的二维数组,每个元素的类型都是 int。我们可以使用数组字面量初始化二维数组,每个数组字面量表示一行数据。
输出结果为:[[1 2 3 4] [5 6 7 8] [9 10 11 12]]
二维数组的遍历:
func main() {
//定义二维数组:
var arr [3][3]int = [3][3]int{{1, 4, 7}, {2, 5, 8}, {3, 6, 9}}
fmt.Println(arr)
fmt.Println("------------------------")
//方式1:普通for循环:
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Print(arr[i][j], "\t")
}
fmt.Println()
}
fmt.Println("------------------------")
//方式2:for range循环:
for key, value := range arr {
for k, v := range value {
fmt.Printf("arr[%v][%v]=%v\t", key, k, v)
}
fmt.Println()
}
}
输出结果:
[[1 4 7] [2 5 8] [3 6 9]]
------------------------
1 4 7
2 5 8
3 6 9
------------------------
arr[0][0]=1 arr[0][1]=4 arr[0][2]=7
arr[1][0]=2 arr[1][1]=5 arr[1][2]=8
arr[2][0]=3 arr[2][1]=6 arr[2][2]=9
2 切片
切片是一个拥有动态长度的可索引序列,它是对底层数组的引用(所以是一个引用类型)。切片提供了一种灵活、方便的方式来管理数组,并允许数组可以自动扩容。切片通常用于表示动态长度的集合,如列表、队列和栈等。
2.1 快速入门
-
切片的英文slice
-
切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
-
切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样。
-
切片的长度是可以变化的,因此切片是一个可以动态变化数组。
-
切片定义的基本语法:
var 变量名 []类型
,比如:var a []int
,这里的[]不需要写长度,只要不写东西就是切片,写就是数组。
案例讲解:
func main() {
//定义二维数组:
var intArr [5]int = [...]int{1, 22, 33, 66, 99}
//声明/定义一个切片
//slice := intArr[1:3]
//1. slice 就是切片名
//2. intArr[1:3] 表示 slice 引用到intArr这个数组
//3. 引用intArr数组的起始下标为 1 , 最后的下标为3(但是不包含3)
slice := intArr[1:3]
fmt.Println("intArr=", intArr)
fmt.Println("slice 的元素是 =", slice) // 22, 33
fmt.Println("slice 的元素个数 =", len(slice)) // 2
fmt.Println("slice 的容量 =", cap(slice)) // 切片的容量是可以动态变化
// 切片是引用类型,也就是,实际存储的是intArr地址值
fmt.Println()
fmt.Printf("intArr[1]的地址=%p\n", &intArr[1])
fmt.Printf("slice[0]的地址=%p slice[0]=%v\n", &slice[0], slice[0])
// 修改了切片的数值,同样会把intArr的值修改,也就是直接修改了地址对应的数据值
fmt.Println()
slice[1] = 34
fmt.Println("intArr=", intArr)
fmt.Println("slice 的元素是 =", slice) // 22, 33
}
输出结果:
intArr= [1 22 33 66 99]
slice 的元素是 = [22 33]
slice 的元素个数 = 2
slice 的容量 = 4
intArr[1]的地址=0x9d16004
slice[0]的地址=0x9d16004 slice[0]=22
intArr= [1 22 34 66 99]
slice 的元素是 = [22 34]
2.2 内存解析
通过上面的代码,我们已经出门了解了slice,现在从底层内存了解一下
从这个图可以看出:
-
slice在内存中的展示,的确是一个引用类型
-
slice从底层来说,其实就是一个数据结构(struct结构体)
type slice struct{ ptr *[2] int // 存储被切的数组的地址值,例如:这里的22是被切的首部,那就存储它的地址值 len int // 存储元素的数量 cap int // 存储切片的容量(底层数组的长度) }
2.3 切片的使用和遍历
方式1:定义一个切片,然后让切片去引用一个已经创建好的数组,比如:前面的案例就是这样的。
略。。。
方式2:通过make来创建切片。
基本语法:var 切片名 []type = make([], len, [cap])
参数说明:
type:就是数据类型 len:大小 cap(可选):指定切片的容量,如果你分配了cap,则要cap >=l en
func main() {
// 基本语法
var sliceName []int = make([]int, 5)
fmt.Println(sliceName)
// 创建一个带有 5 个元素的切片
// 这样创建更舒服
s := make([]int, 5)
// 在切片中添加元素
s = append(s, 1, 2, 3)
fmt.Println("s切片的元素:", s)
// 输出切片的长度和容量
fmt.Println("长度为:", len(s), ",容量为:", cap(s))
// 访问切片的元素
fmt.Println("第一个元素为:", s[0])
}
输出结果:
[0 0 0 0 0]
s切片的元素: [0 0 0 0 0 1 2 3]
长度为: 8 ,容量为: 12
第一个元素为: 0
注意:通过这种方式创建的切片,其地址指向的数组是对外不可见
,但在内存中就是会有占用这么一块内存空间。
方式3:定义一个切片的同时,直接指定具体数组
func main() {
var sliceName []string = []string{"Tom", "Jack", "Mary"}
// sliceName := []string{"Tom", "Jack", "Mary"}
fmt.Println("sliceName", sliceName)
fmt.Println("sliceName size=", len(sliceName))
fmt.Println("sliceName cap=", cap(sliceName))
}
输出结构:
sliceName [Tom Jack Mary]
sliceName size= 3
sliceName cap= 3
切片的遍历
func main() {
sliceName := []string{"Tom", "Jack", "Mary"}
// 使用常规方式
for i := 0; i < len(sliceName); i++ {
fmt.Printf("slice[%v] = %v", i, sliceName[i])
}
fmt.Println()
// 使用for--range方式遍历
for i, v := range sliceName {
fmt.Printf("slice[%v] = %v", i, v)
}
}
输出结果:
slice[0] = Tomslice[1] = Jackslice[2] = Mary
slice[0] = Tomslice[1] = Jackslice[2] = Mary
2.4 注意事项和细节说明
- 切片初始化时,var slice = arr[startIndex:endIndex]
说明:从arr数组下标为startIndex,取到下标为endIndex的元素(不含arr[endIndex])
-
切片初始化时,仍然不能越界。范围在[0-len(arr)]之间,但是可以动态增长。
- var slice = arr[0:end] 可以简写: var slice = arr[:end]
- var slice = arr[start:len(arr)] 可以简写:var slice = arr[start:]
- var slice = arr[0:len(arr)] 可以简写:var slice = arr[:]
-
cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。
-
切片可以继续切片
-
用切片的内置函数append,可以第切片进行动态追加
切片append操作的底层原理分析:
- 切片append操作的本质就是对数组扩容
- go底层会创建一下新的数组newArr(安装扩容后大小)
- 将slice原来包含的元素拷贝到新的数组newArr
- slice重新引用到newArr
- 注意newArr是在底层来维护的,程序员不可见
案例说明:
通过这个图就可以看出,扩容后,go底层会创建行的数组,而且面对程序员不可见。
2.5 string和slice关系
-
string底层是一个byte数组,因此string也可以进行切片处理
-
string和切片在内存的形式,以"abcd"画出内存示意图
-
string是不可变的,也就是说不能通过 str[0] = 'z’方式来修改字符串
-
如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string
func main() {
//string底层是一个byte数组,因此string也可以进行切片处理
str := "hello@atguigu"
//使用切片获取到 atguigu
slice := str[6:]
fmt.Println("slice=", slice)
//string是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
//str[0] = 'z' [编译不会通过,报错,原因是string是不可变]
//如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string
// "hello@atguigu" =>改成 "zello@atguigu"
// arr1 := []byte(str)
// arr1[0] = 'z'
// str = string(arr1)
// fmt.Println("str=", str)
// 细节,我们转成[]byte后,可以处理英文和数字,但是不能处理中文
// 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码
// 解决方法是 将 string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字
arr1 := []rune(str)
arr1[0] = '北'
str = string(arr1)
fmt.Println("str=", str)
}
2.6 切片练习题
写一个函数,可以打印一个斐波那契的数列,要求为数组格式
- 可以接收一个 n int
- 能够将斐波那契的数列放到切片中
- 提示, 斐波那契的数列形式:arr[0] = 1; arr[1] = 1; arr[2]=2; arr[3] = 3; arr[4]=5; arr[5]=8
思路
- 声明一个函数 fbn(n int) ([]uint64)
- 编程fbn(n int) 进行for循环来存放斐波那契的数列
func fbn(n int) ([]uint64) {
//声明一个切片,切片大小 n
fbnSlice := make([]uint64, n)
//第一个数和第二个数的斐波那契 为1
fbnSlice[0] = 1
fbnSlice[1] = 1
//进行for循环来存放斐波那契的数列
for i := 2; i < n; i++ {
fbnSlice[i] = fbnSlice[i - 1] + fbnSlice[i - 2]
}
return fbnSlice
}
func main() {
fnbSlice := fbn(20)
fmt.Println("fnbSlice=", fnbSlice)
}
输出结果:
fnbSlice= [1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765]
3 map映射
map是key-value数据结构,又称为字段或者关联数组。类似其他编程语言的集合,在编程中是经常用到的。
3.1 快速入门
切片的语法:var map变量名 map[keytype]valuetype
- key、value的类型:bool、数字、string、指针、channel 、还可以是只包含前面几个类型的接口、结构体、数组
- key通常为int 、string类型,value通常为数字(整数、浮点数)、string、map、结构体
- key:slice、map、function不可以,因为这几个没发用 == 来判断
map的特点:
- map集合在使用前一定要make
- map的key-value是无序的
- key是不可以重复的,如果遇到重复,后一个value会替换前一个value
- value可以重复的
func main() {
//map的声明
var a map[string]string
//在使用map前,需要先make , make的作用就是给map分配数据空间
a = make(map[string]string, 10)
// 或者一步解决:a := make(map[string]string, 10)
a["no1"] = "宋江" //ok?
a["no2"] = "吴用" //ok?
a["no1"] = "武松" //ok?
a["no3"] = "吴用" //ok?
fmt.Println(a)
}
输出结果:map[no1:武松 no2:吴用 no3:吴用]
声明map的三种方法:
- 如上所示,分开来使用
- 声明就直接make:
var a = make(map[string]string, 10)
- 声明直接赋值:
a := map[string]string{"no1": "北京","no2": "深圳",}
还可以套多一层,例如下面:
课堂练习:演示一个key-value 的value是map的案例
比如:我们要存放3个学生信息, 每个学生有 name和sex 信息
思路: map[string]map[string]string
func main() {
studentMap := make(map[string]map[string]string)
studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "tom"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "北京长安街~"
studentMap["stu02"] = make(map[string]string, 3) //这句话不能少!!
studentMap["stu02"]["name"] = "mary"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "上海黄浦江~"
fmt.Println(studentMap)
fmt.Println(studentMap["stu02"])
}
输出结果:
map[stu01:map[address:北京长安街~ name:tom sex:男] stu02:map[address:上海黄浦江~ name:mary sex:女]]
map[address:上海黄浦江~ name:mary sex:女]
3.2 map的crud操作
map增加和更新:
map[“key”] = value // 如果key还没有,就是增加,如果key存在就是修改。
map删除
delete(map, “key”),delete是一个内置函数,如果key存在,就删除改key-value,如果key不存在,不操作,但是也不会报错。
细节说明:
- 如果我们要删除map的所有key,没有一个专门的方法一次删除,可以遍历一下key,逐个删除
- 或者 map = make(…),make一个新的,让原来的成为垃圾,被gc回收
map查找
使用这种方式查找:要查找的值val, 结果(true/false) := map的名字["需要查找的key"]
func main() {
heroes := make(map[string]string, 10)
heroes["no1"] = "宋江"
heroes["no2"] = "卢俊义"
val, findRes := heroes["no1"]
if findRes {
fmt.Println("找到了val=", val)
} else {
fmt.Println("没有no1这个key")
}
}
输出结果:找到了val= 宋江
说明:如果heroes这个map中存在”no1“,那么findRes就会返回true,否则返回false
3.3 map的遍历
map的遍历只能使用for-range的结构遍历
简单案例:
func main() {
heroes := make(map[string]string, 10)
heroes["no1"] = "北京"
heroes["no2"] = "上海"
for k, v := range heroes {
fmt.Printf("k=%v v=%v\n", k, v)
}
}
输出结果:
k=no1 v=北京
k=no2 v=上海
复杂案例:
func main() {
studentMap := make(map[string]map[string]string)
studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "tom"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "北京长安街~"
studentMap["stu02"] = make(map[string]string, 3) //这句话不能少!!
studentMap["stu02"]["name"] = "mary"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "上海黄浦江~"
for k1, v1 := range studentMap {
fmt.Println("k1=", k1)
for k2, v2 := range v1 {
fmt.Printf("\t k2=%v2 v2=%v \n", k2, v2)
}
fmt.Println()
}
}
输出结果:
k1= stu01
k2=name2 v2=tom
k2=sex2 v2=男
k2=address2 v2=北京长安街~
k1= stu02
k2=name2 v2=mary
k2=sex2 v2=女
k2=address2 v2=上海黄浦江~
3.4 map切片
切片的数据类型如果是map,则我们称为 slice of map,map切片,这样使用则map个数就可以动态变化了。
案例演示:
要求:使用一个map来记录monster的信息 name 和 age,
也就是说一个monster对应一个map,并且妖怪的个数可以动态的增加=>map切片
func main() {
monster := make([]map[string]string, 2) // 存入两个妖怪
monster[0] = make(map[string]string, 2)
monster[0]["name"] = "牛魔王"
monster[0]["age"] = "500"
monster[1] = make(map[string]string, 2)
monster[1]["name"] = "玉兔精"
monster[1]["age"] = "200"
// 因为monster只配置了两个容量,所以如果还要添加就会越界,必须用另外的方法
// 1.先定义monster信息
newMonster := map[string]string{
"name": "新的妖怪",
"age": "200",
}
// 2.再通过append接上
monster = append(monster, newMonster, make(map[string]string))
fmt.Println(monster)
}
输出结果:
[map[age:500 name:牛魔王] map[age:200 name:玉兔精] map[age:200 name:新的妖怪] map[]]
3.5 map排序
- golang中没有一个专门的方法针对map的key进行排序
- golang中的map默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历。得到的输出可能不一样。
- golang中map的排序,需要使用切片储存map,然后再用sort包进行排序。
在 Go 语言中,map 的 key 是无序的。这是由 Go 语言的实现决定的。map 的底层实现是哈希表,哈希表对 key 进行哈希运算,将 key 映射到底层数组的某个位置上,并存储对应的 value。由于哈希表的内部实现与 key 的顺序无关,因此 map 的 key 是无序的。
map 的无序性可以带来一些好处,例如更高的访问速度和更小的内存占用。由于哈希表是按照 key 所计算的哈希值来访问元素的,因此访问元素的速度是非常快的,不会因为 key 的顺序而降低速度。此外,由于底层数组存储的是键值对,而不是一个个具体的元素,所以在空间占用上,与 key 的顺序无关,可以充分利用底层数组的空间。
如果你需要有序的 key,可以使用 sort
包对 map 中的 key 进行排序。示例如下:
package main
import (
"fmt"
"sort"
)
func main() {
a := map[string]string{
"no1": "成都",
"no3": "北京",
"no2": "深圳",
}
keys := make([]string, len(a))
i := 0
for k := range a {
keys[i] = k
i++
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, a[k])
}
}
在上述代码中,我们首先将 map a
中的 key 存储到一个切片中,并使用 sort
包对切片进行排序。然后,我们可以使用排序后的切片来遍历 map,并以有序的方式输出所有的 key 和对应的 value。
补充:
递增排序
sort.Strings(keys)
递减排序
sort.Slice(keys, func(i, j int) bool {
return keys[i] > keys[j]
})
上面这个代码:使用 sort.Slice
函数对 keys
切片进行排序,并传入一个匿名的比较函数作为参数。在比较函数中,我们使用递减(大于)的方式进行比较,从而实现递减排序。
3.6 使用细节
- map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map。
- map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map能动态的增长键值对。
- map的value也经常使用struct类型,更适合管理复杂的数据(比前面value是一个map更好),比如value为Student结构体。
以下代码将对第2句话进行讲解和验证:
package main
import "fmt"
func main() {
a := make(map[string]string, 2)
a["apple"] = "red"
a["banana"] = "yellow"
a["carrot"] = "orange"
fmt.Println(a)
}
上述代码创建了一个初始容量为 2 的空 map,并依次向其中添加三个键值对。根据提示的特性,当 map 容量达到后,再想往 map 中增加元素时,它会自动扩容。
输出结果如下:
map[apple:red banana:yellow carrot:orange]
从输出结果中可以看出,map 成功地添加了三个键值对,并没有发生 panic。这证明了 map 的确能够动态增长键值对。
需要注意的是,尽管 map 可以动态增长键值对,但其增长策略是根据实现方式(哈希表)和负载因子(current size / current capacity)来决定的。当负载因子超过一定阈值时,map 会重新分配更大的内存空间,将已有的元素重新哈希到新的内存位置上,并拷贝到新的底层数组中。这个过程可能会导致一些性能消耗,因此,合理估算和设置初始容量可以提高 map 的性能。
4 综合题目(难度较大,可略~)
题目:某餐馆进行食物点菜记录,要求记录每桌点的菜品,桌号为1、2、3…,要求能够添加、查看和删除菜品,并且能够查询每桌点了多少菜。
解决思路:
- 使用 map 建立桌号与菜品的对应关系,key 是桌号,value 是一个切片,表示该桌点的菜品。
- 使用 map 存储每桌点菜的数量,key 是桌号,value 是整数,表示该桌点了多少菜。
实现步骤:
- 创建一个空的 map
orders
,用来记录每桌点的菜品。 - 创建一个空的 map
total
,用来记录每桌点了多少菜。 - 添加点菜功能:输入桌号和菜品名称,判断该桌号是否存在于
orders
中,如果存在,则在对应的切片中添加菜品,并在total
中该桌号对应的值加一。如果不存在,则创建该桌号的切片,并添加菜品,同时在total
中该桌号对应的值初始化为 1。 - 删除菜品功能:输入桌号和菜品名称,判断该桌号是否存在于
orders
中,如果存在,则在对应的切片中删除菜品,并在total
中该桌号对应的值减一。如果删除后切片为空,则从orders
和total
中删除该桌号。 - 查询菜品功能:输入桌号,从
orders
中查找该桌号对应的菜品切片,并输出菜品数量。
下面是具体的实现代码:
package main
import "fmt"
func main() {
orders := make(map[string][]string)
total := make(map[string]int)
// 添加点菜
addOrder(orders, total, "1", "鱼香肉丝")
addOrder(orders, total, "1", "宫保鸡丁")
addOrder(orders, total, "2", "水煮鱼")
addOrder(orders, total, "3", "红烧肉")
addOrder(orders, total, "3", "糖醋排骨")
// 输出每桌点菜结果
for table, dishes := range orders {
fmt.Printf("桌号:%s,点了以下菜品:", table)
for _, dish := range dishes {
fmt.Printf(" %s,", dish)
}
fmt.Printf(" 共计%d道菜\n", total[table])
}
fmt.Println()
// 删除一道菜
deleteOrder(orders, total, "1", "宫保鸡丁")
// 输出每桌点菜结果
for table, dishes := range orders {
fmt.Printf("桌号:%s,点了以下菜品:", table)
for _, dish := range dishes {
fmt.Printf(" %s,", dish)
}
fmt.Printf(" 共计%d道菜\n", total[table])
}
fmt.Println()
// 查询菜品数量
queryOrder(total, "2")
queryOrder(total, "3")
}
func addOrder(orders map[string][]string, total map[string]int, table string, dish string) {
_, ok := orders[table]
if ok {
orders[table] = append(orders[table], dish)
total[table]++
} else {
orders[table] = []string{dish}
total[table] = 1
}
}
func deleteOrder(orders map[string][]string, total map[string]int, table string, dish string) {
dishes, ok := orders[table]
if ok {
for i, d := range dishes {
if d == dish {
orders[table] = append(orders[table][:i], orders[table][i+1:]...)
total[table]--
if total[table] == 0 {
delete(orders, table)
delete(total, table)
}
break
}
}
}
}
func queryOrder(total map[string]int, table string) {
count, ok := total[table]
if ok {
fmt.Printf("桌号:%s,点了%d道菜\n", table, count)
} else {
fmt.Printf("桌号:%s,无点菜记录\n", table)
}
}
代码很长,需要慢慢看~~~
Over!!!结束啦~~下一步就是面向对象,冲冲冲!!!