7.数据标签:映射

Saturday, July 22, 2023

当需要使用一个特定的值,为了找到它,需要从数组或切片中查看每一个元素,有一种集合,其中的每个值都有一个标签,那么就可以快速找到需要的值,映射就是做这个工作的。

从文件中读取名字

有一个文件 votes.txt :

Amber Graham
Brian Martin
Amber Graham
Brian Martin
Amber Graham

每一行代表一次投票,需要处理文件的每一行并统计每个名称出现的总次数,获得多票的胜出。

首先需要读取votes.txt文件的内容,之前有GetFloats函数来读取文件中的每一行,并转换为一个切片,但只能读取float64值,所以需要增加一个GetStrings函数,把每行作为string值添加到返回的切片中。

package datafile

import (
	"bufio"
	"os"
)

func GetStrings(fileName string) ([]string, error) {
	var lines []string
	file, err := os.Open(fileName)
	if err != nil {
		return nil, err
	}
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		lines = append(lines, line)
	}
	err = file.Close()
	if err != nil {
		return nil, err
	}
	if scanner.Err() != nil {
		return nil, scanner.Err()
	}
	return lines, nil
}
package main

import (
	"fmt"
	"xxx/xxx/datafile"
	"log"
)

func main() {
	lines, err: = datafile.GetStrings("votes.txt")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(lines)
}

使用困难的切片方法对名字计数

上述代码从文件中读取名字,如何计算名字出现了多少次?有两种方式,一种是切片,另一种是数据结构映射。

关于使用切片的方式:创建两个切片,每个的长度都是元素的总个数,并是指定的顺序,第一个切片用来保存在文件中找到的名字,每个名字只出现一次,命名为 names;第二个切片命名为counts,保存文件中的名字出现的次数,元素 counts[0] 保存 names[0] 出现的次数,以次类推。

func main() {
	lines, err := datafile.GetStrings("votes.txt")
	if err != nil {
		log.Fatal(err)
	}
	var names []string   //这个变量保存人名
	var counts []int     //这个变量保存每个人名出现的次数
	for _, line := range lines {
		matched := false
		for i, name := range names {   //循环处理names切片中的每一个值
			if name == line {      //若line与当前名称匹配
				counts[i]++    //增加对应的count值
				matched = true  //标识找到了匹配的名字
			}
		}
		if matched == false {       //若没有找到匹配名字
			names = append(names, line)  //添加它作为一个新名字
			counts = append(counts, 1)   //并增加一个新的计数(这行是第一次出现)
		}
	}
	for i, name := range names {
		fmt.Printf("%s: %d\n", name, counts[i])   //输出names切片中的每一个元素,和对应的counts切片中的元素
	}
}

程序使用一个循环嵌套在另一个循环中的方式来统计名字的次数,外面的循环把文件中的每行以每次一行的方式赋值给line变量。内部循环通过遍历names切片中的每个元素来查找与文件中的当前行匹配的名称。

若某人在选票上写一个名字,会导致文件中加载一个字符串,程序会一个一个确认names的元素中是否有等于这个名字。若没有找到匹配项,程序会在names切片末尾追加这个名字,并在counts切片中相应的位置增加1。假如下一行的这个名字已经存在于names切片中,程序会找到位置,在对应位置增加1。

映射

保存名字使用的是切片,对于文件中的每一行,必须在许多names切片的值中寻找来进行比较,这样会导致性能差。

go有一种方法保存数据集合: 映射 。 一个映射是通过来访问每一个值的集合,键是一个简单的方式从映射中找出数据。

相对于数组和切片只能使用整型数字作为索引,映射可以使用任意类型的键(只要这个类型可以使用 == 来比较),包括数字、字符串和其他。所有的值只能是相同的类型,所有的健也需要是相同的类型,但是键和值的类型不必相同。

声明一个保存映射的变量,需要 map 关键字,后跟一对包含键类型的方括号[],然后在方括号后跟提供值的类型。
var myMap map[string]float64

与切片一样,声明一个映射变量并不会自动创建一个映射,需要调用make函数(与创建切片相同的函数),可以传递给make想要创建的映射类型(与需要赋值到的类型相同)。

var ranks map[string]int    //声明一个映射变量
ranks = make(map[string]int) //真正创建一个映射

短变量声明方式:

ranks := make(map[string]int)   //创建一个映射并声明一个用于保存它的变量

映射的赋值与取值语法跟数组和切片类似,但数组和切片仅允许使用整型作为元素索引,映射可以选择几乎所有的类型来作为键。

这是一个名为ranks的映射:

ranks["gold"] = 1
ranks["silver"] = 2
ranks["bronze"] = 3
fmt.Println(ranks["bronze"])
fmt.Println(ranks["gole"])

一个键和值都使用string的映射:

elements := make(map[string]string)
elements["H"] = "Hydrogen"
elements["Li"] = "Lithium"
fmt.Println(elements["Li"])
fmt.Println(elements["H"])

使用整型作为键,bool类型作为值:

isPrime := make(map[int]bool)
isPrime[4] = false
isPrime[7] = true
fmt.Println(isPrime[4])
fmt.Println(isPrime[7])

映射字面量

若预先知道映射的键和值,可以使用字面量来创建映射,映射字面量是以映射类型(以“映射[键类型]值类型”的形式)开始,后面跟花括号,内含想要映射初始就包含的 键/值 对。

myMap := map[string]float64{"a": 1.2, "b": 5.6}

例:

ranks := map[string]int{"bronze": 3, "silver": 2, "gold": 1}
fmt.Println(ranks["gold"])
fmt.Println(ranks["bronze"])
elements := map[string]string{
        "H": "Hydrogen",
        "Li": "Lithium",
}   //多行映射字面量
fmt.Println(elements["H"])
fmt.Println(elements["Li"])

与切片字面量一样,花括号为空创建一个空的映射:

emptyMap := map[string]float64{}

映射中的零值

对于数组和切片,若访问一个没有赋值过的键,会得到一个零值,根据值类型不同,零值不一定是0。比如映射的值类型是string,零值就是空字符串。

与数组和切片一样,零值可以更安全地修改映射的值,即使在没有给它赋值的情况下。

counters := make(map[string]int)
counters["a"]++
counters["a"]++
counters["c"]++
fmt.Println(counters["a"], counters["b"], counters["c"])

映射变量的零值是nil

与切片一样,映射变量的零值是nil,若声明了一个映射变量但未赋值,是nil,意味着没有映射存在来增加键或值。

var nilMap map[int]string
fmt.Printf("%#v\n", nilMap)
nilMap[3] = "three"   //映射是nil,无法添加新值,会导致panic

在增加一个新的键值对前,需要使用make或映射字面量来创建一个映射,并赋值给映射变量。

var myMap map[int]string = make(map[int]string)
myMap[3] = "three"
fmt.Printf("%#v\n", myMap)

如何区分已经赋值的值和零值

虽然零值很有用,但无法判断一个键是被赋值成了零值还是未赋值。

有一个例子:错误输出了名为 Carl 的人没有通过,实际上并没有记录他的成绩。

func status(name string) {
        grades := map[string]float64{"Alma": 0, "Rohit": 86.5}
        grade := grades[name]
        if grade < 60 {
                fmt.Printf("%s is failing!\n", name)
        }
}
func main() {
        status("Alma")   //一个映射中已经被赋值为0的键
        status("Carl")   //一个映射中未被赋值的键
}

输出:

Alma is failing!
Carl is failing!

为解决此问题,访问映射键的时候可选地获取第2个布尔类型的值,若这个键已经被赋过值,返回true,否则返回false。通常情况下,开发者会将这个布尔值赋给一个名为ok的变量。

counters := map[string]int{"a": 3, "b": 0}
var value int
var ok bool
value, ok = counters["a"]   //访问一个已经被赋值过的值
fmt.Println(value, ok)      //ok会返回true
value, ok = counters["b"]   //访问一个已经被赋值过的值
fmt.Println(value, ok)      //ok会返回true
value, ok = counters["c"]   //访问一个未赋值过的值
fmt.Println(value, ok)      //ok会返回false

若仅仅要测试值是否存在,可以通过将其赋值给 _ 空白标识符来忽略值。

counters := map[string]int{"a": 3, "b": 0}
var ok bool
_, ok = counters["b"]   //测试值是否存在,但忽略值
fmt.Println(ok)
_, ok = counters["c"]   //测试值是否存在,但忽略值
fmt.Println(ok)

第二个返回值可以用来判断如何处理这个值,是已经赋值了但是正好等于零值,还是从未被赋值过。

修改后:在输出不及格前测试请求的名字是否已经被赋值过。

func  status(name string) {
       grades := map[string]float64{"Alma": 0, "Rohit": 86.4}
       grade, ok := grades[name]
       if !ok {
               fmt.Printf("No grade recorded for %s.\n", name)
       } else if grade < 60 {
               fmt.Printf("%s is failing!\n", name)
       }
}

func main() {
       status("Alma")
       status("Carl")
}

练习:

data := []string{"a", "c", "e", "a", "e"}   //需计算在切片中的每个字母的次数
counts := make(map[string]int)    //保存计数的映射
for _, item := range data {
        counts[item]++     //增加当前字母的次数
}    //处理每个字符
letters := []string{"a", "b", "c", "d", "e"}   //是否这些字符都在映射中存在
for _, letter := range letters {
        count, ok := counts[letter]    //获取当前字符的计数以及是否出现的指示
        if !ok {   //如果字母未找到
                fmt.Printf("%s: not found\n", letter)
        } else {   //如果字母找到
                fmt.Printf("%s: %d\n", letter, count)
        }
}

输出:

a:2
b:not found
c:1
d:not found
e:2

使用"delete"函数删除键/值对

在分配了值之后,某个时候希望将其从映射中移除,go提供了内建的delete函数,只需传递给delete两个参数:希望删除数据的映射和希望删除的键,然后键和其关联的值都会被删除。

如下代码,给两个映射的键分配了值,然后将其删除,之后,尝试访问,获取到零值。(对于ranks映射是0,对于isPrime映射是false)。第二个布尔返回值也是false,说明键已经被删除。

var ok bool
ranks := make(map[string]int)
var rank int
ranks["bronze"] = 3   //给“bronze”键分配值
rank, ok = ranks["bronze"]  //由于值存在,ok会返回true
fmt.Printf("rank: %d, ok: %v\n", rank, ok)
delete(ranks, "bronze")   //删除键“bronze”和相关的值
rank, ok = ranks["bronze"]   //由于值已经被删除了ok返回false
fmt.Printf("rank: %d, ok: %v\n", rank, ok)

isPrime := make(map[int]bool)
var prime bool
isPrime[5] = true  //给键5分配值
prime, ok = isPrime[5]  //由于值存在,ok会返回true
fmt.Printf("prime: %v, ok: %v\n", prime, ok)
delete(isPrime, 5)   //删除键5和相关的值
prime, ok = isPrime[5]   //由于值被删除,ok会返回false
fmt.Printf("prime: %v, ok: %v\n", prime, ok)

输出:

rank: 3, ok: true
rank: 0, ok: false
prime: true, ok: true
prime: false, ok: false

使用映射来更新程序

package main
import (
        "fmt"
        "xxx/xxx/datafile"
        "log"
)
func main() {
        lines, err := datafile.GetStrings("votes.txt")
        if err != nil {
                log.Fatal(err)
        }
        counts := make(map[string]int)   //声明一个以人名为键,次数为值的映射
        for _, line := range lines {
                counts[lines]++     //为人名增加计数
        }
        fmt.Println(counts)   //输出填充的映射
}

对映射进行"for…range"循环

为了从映射中格式化每个键和值作为一行,需要使用循环遍历映射中的每一条。

与数组和切片的 for…range 循环一样,与将一个整数索引赋值给第一个变量不同,映射将键赋给了第一个变量。

for…range 循环让遍历映射中的键和值更方便,仅用一个变量保存键,再用一个变量保存值,并会自动遍历映射中的每一个条目。

package main
import "fmt"
func main() {
        grades := map[string]float64{"Alma": 74.2, "Rohit": 86.5, "Carl": 59.7}
        for name, grade := range grades {
                fmt.Printf("%s has a grade of %0.1f%%\n", name, grade)  //打印每一个键和它对应的值
        }  //循环遍历每一个键/值对
}

输出:

Carl has a grade of 59.7%
Alma has a grade of 74.2%
Rohit has a grade of 86.5%

若只需要循环所有的键,可以忽略对应的值变量:

fmt.Println("Class roster:")
for name := range grades {
        fmt.Println(name)
}

输出:

Class roster:
Alma
Rohit
Carl

若只需要值,可以将键赋 _ 空白标识符:

fmt.Println("Grades:")
for _, grade := range grades {
        fmt.Println(grade)
}

输出:

Grades:
59.7
74.2
86.5

这里有个问题,如果将之前的结果存入一个文件,并执行,会发现映射的键和值是按照随机顺序打印,若多次执行,每次的结果顺序是不一样的。

“for…range"循环以随机顺序处理映射

for…range 以随机的顺序遍历映射的键和值,因为映射是一个非有序的键/值对集合。但有时需要按照特定的顺序遍历时,要自己写一些代码了。

例子:使名字按照字母表的顺序输出,使用了两个for循环,第一个循环遍历映射里边所有的键,忽略值,并把其增加到一个字符串的切片上,然后把切片传递给sort包中的Strings函数来以字母表顺序排序。第二个for循环并不遍历映射,而是遍历变量排序后的名字的切片,输出名字,并从映射中获取与名字对应的值,仍然处理映射中的每一个键和值,但是从已排序好的切片中获取而不是从映射中获取的键。

package main

import (
        "fmt"
        "sort"
)

func main() {
        grades := map[string]float64{"Alma": 74.2, "Rohit": 86.5, "Carl": 59.7}
        var names []string
        for name := range grades {
                names = append(names, name)
        }
        sort.Strings(names)
        for _, name := range names {
                fmt.Printf("%s has a grade of %0.1f%%\n", name, grades[name])
        }
}

按照以上代码执行,名字是按照字母表顺序排列,不管执行多少次都是如此。

若不在乎映射中的数据如何处理,使用for…range循环就可以,但若是需要顺序,就需要自己写代码处理排序问题。

使用for…range更新程序

将打印整个映射的行用for…range替换,把键赋给name变量,把映射的值赋值给count变量,然后用printf输出人名和得票数。

package main

import (
        "fmt"
        "xxx/xxx/datafile"
        "log"
)

func main() {
        lines, err := datafile.GetStrings("votes.txt")
        if err != nil {
                log.Fatal(err)       
        }
        counts := make(map[string]int)
        for _, line := range lines {
                counts[line]++
        }
        for name, count := range counts {
                fmt.Printf("Votes for %s: %d\n", name, count)
        }
}

当可用的数据集合是数组和切片时,需要很多额外的代码和处理时间来查找,但使用映射处理起来更简便。

当需要查找集合中的值的时候,可以考虑映射!

Golang打怪升级

8.结构体:struct

6.切片

comments powered by Disqus