Go学习第七章——数组arr,切片slice和映射map

1 数组

数组可以存放多个同一类型数据。数组也是一种数据类型,在Go中,数组是值类型。

1.1 快速入门

数组定义格式:
var 数组名 [数组大小]数据类型

步骤:

  1. 定义数组
  2. 将数据存入数组
  3. 根据所需获取数据进行计算

案例讲解:

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

从上面的结果可以看出以下几点:

  1. 如果是int类型数组,默认不赋值都为0,那么可以得出,不赋值的情况,默认值就是基本数据类型的默认值。
  2. 获取数组的地址值,跟获取基本数据类型一样:&arr即可
  3. arr第一个空间的地址值跟直接获取的地址值相同:&arr = &arr[0]
  4. 因为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 注意事项以及分析
  1. 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化

  2. var arr []int 这时arr就是一个slice切片,切片下一步讲,等等说。

  3. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用

  4. 数组创建后,如果没有赋值,有默认值

    • 数值类型数组:默认: 0
    • 字符串数组:默认:“”
    • bool数组:默认值:false
  5. 使用数组的步骤 1. 声明数组并开辟空间 2.给数组各个元素赋值 3.使用数组

  6. 数组的下标是从0开始的

  7. 数组下标必须在指定范围内使用,否则报 panic;数组越界。

  8. Go的数组属于值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会互相影响。

  9. 如果想在其他函数中,去修改原来的数组,可以使用引用传递(指针方式)

    案例讲解:

    // 通过普通方式,修改数组的值
    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 数组反转

要求:随机生成五个数,并将其反转打印
思路:

  1. 随机生成五个数 , rand.Intn() 函数
  2. 当我们得到随机数后,就放到一个数组 int数组
  3. 反转打印 , 交换的次数是 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 快速入门
  1. 切片的英文slice

  2. 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。

  3. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样。

  4. 切片的长度是可以变化的,因此切片是一个可以动态变化数组。

  5. 切片定义的基本语法:

    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,现在从底层内存了解一下

在这里插入图片描述

从这个图可以看出:

  1. slice在内存中的展示,的确是一个引用类型

  2. 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 注意事项和细节说明
  1. 切片初始化时,var slice = arr[startIndex:endIndex]

​ 说明:从arr数组下标为startIndex,取到下标为endIndex的元素(不含arr[endIndex])

  1. 切片初始化时,仍然不能越界。范围在[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[:]
  2. cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。

  3. 切片可以继续切片

  4. 用切片的内置函数append,可以第切片进行动态追加

    切片append操作的底层原理分析:

    1. 切片append操作的本质就是对数组扩容
    2. go底层会创建一下新的数组newArr(安装扩容后大小)
    3. 将slice原来包含的元素拷贝到新的数组newArr
    4. slice重新引用到newArr
    5. 注意newArr是在底层来维护的,程序员不可见

    案例说明:

    在这里插入图片描述

通过这个图就可以看出,扩容后,go底层会创建行的数组,而且面对程序员不可见。

2.5 string和slice关系
  1. string底层是一个byte数组,因此string也可以进行切片处理

  2. string和切片在内存的形式,以"abcd"画出内存示意图

    在这里插入图片描述

  3. string是不可变的,也就是说不能通过 str[0] = 'z’方式来修改字符串

  4. 如果需要修改字符串,可以先将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 切片练习题

写一个函数,可以打印一个斐波那契的数列,要求为数组格式

  1. 可以接收一个 n int
  2. 能够将斐波那契的数列放到切片中
  3. 提示, 斐波那契的数列形式:arr[0] = 1; arr[1] = 1; arr[2]=2; arr[3] = 3; arr[4]=5; arr[5]=8

思路

  1. 声明一个函数 fbn(n int) ([]uint64)
  2. 编程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

  1. key、value的类型:bool、数字、string、指针、channel 、还可以是只包含前面几个类型的接口、结构体、数组
  2. key通常为int 、string类型,value通常为数字(整数、浮点数)、string、map、结构体
  3. key:slice、map、function不可以,因为这几个没发用 == 来判断

map的特点:

  1. map集合在使用前一定要make
  2. map的key-value是无序的
  3. key是不可以重复的,如果遇到重复,后一个value会替换前一个value
  4. 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的三种方法:

  1. 如上所示,分开来使用
  2. 声明就直接make:var a = make(map[string]string, 10)
  3. 声明直接赋值: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不存在,不操作,但是也不会报错。

细节说明:

  1. 如果我们要删除map的所有key,没有一个专门的方法一次删除,可以遍历一下key,逐个删除
  2. 或者 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排序
  1. golang中没有一个专门的方法针对map的key进行排序
  2. golang中的map默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历。得到的输出可能不一样。
  3. 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 使用细节
  1. map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map。
  2. map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map能动态的增长键值对
  3. 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…,要求能够添加、查看和删除菜品,并且能够查询每桌点了多少菜。

解决思路:

  1. 使用 map 建立桌号与菜品的对应关系,key 是桌号,value 是一个切片,表示该桌点的菜品。
  2. 使用 map 存储每桌点菜的数量,key 是桌号,value 是整数,表示该桌点了多少菜。

实现步骤:

  1. 创建一个空的 map orders,用来记录每桌点的菜品。
  2. 创建一个空的 map total,用来记录每桌点了多少菜。
  3. 添加点菜功能:输入桌号和菜品名称,判断该桌号是否存在于 orders 中,如果存在,则在对应的切片中添加菜品,并在 total 中该桌号对应的值加一。如果不存在,则创建该桌号的切片,并添加菜品,同时在 total 中该桌号对应的值初始化为 1。
  4. 删除菜品功能:输入桌号和菜品名称,判断该桌号是否存在于 orders 中,如果存在,则在对应的切片中删除菜品,并在 total 中该桌号对应的值减一。如果删除后切片为空,则从 orderstotal 中删除该桌号。
  5. 查询菜品功能:输入桌号,从 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!!!结束啦~~下一步就是面向对象,冲冲冲!!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值