mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-08-07 15:52:24 +00:00
回到简体
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
## 4.3. Map
|
||||
|
||||
哈希表是一種巧妙併且實用的數據結構。它是一個無序的key/value對的集合,其中所有的key都是不同的,然後通過給定的key可以在常數時間複雜度內檢索、更新或刪除對應的value。
|
||||
哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
|
||||
|
||||
在Go語言中,一個map就是一個哈希表的引用,map類型可以寫爲map[K]V,其中K和V分别對應key和value。map中所有的key都有相同的類型,所有的value也有着相同的類型,但是key和value之間可以是不同的數據類型。其中K對應的key必須是支持==比較運算符的數據類型,所以map可以通過測試key是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的,但是將浮點數用做key類型則是一個壞的想法,正如第三章提到的,最壞的情況是可能出現的NaN和任何浮點數都不相等。對於V對應的value數據類型則沒有任何的限製。
|
||||
在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型则没有任何的限制。
|
||||
|
||||
內置的make函數可以創建一個map:
|
||||
内置的make函数可以创建一个map:
|
||||
|
||||
```Go
|
||||
ages := make(map[string]int) // mapping from strings to ints
|
||||
```
|
||||
|
||||
我們也可以用map字面值的語法創建map,同時還可以指定一些最初的key/value:
|
||||
我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
|
||||
|
||||
```Go
|
||||
ages := map[string]int{
|
||||
@@ -19,7 +19,7 @@ ages := map[string]int{
|
||||
}
|
||||
```
|
||||
|
||||
這相當於
|
||||
这相当于
|
||||
|
||||
```Go
|
||||
ages := make(map[string]int)
|
||||
@@ -27,48 +27,48 @@ ages["alice"] = 31
|
||||
ages["charlie"] = 34
|
||||
```
|
||||
|
||||
因此,另一種創建空的map的表達式是`map[string]int{}`。
|
||||
因此,另一种创建空的map的表达式是`map[string]int{}`。
|
||||
|
||||
Map中的元素通過key對應的下標語法訪問:
|
||||
Map中的元素通过key对应的下标语法访问:
|
||||
|
||||
```Go
|
||||
ages["alice"] = 32
|
||||
fmt.Println(ages["alice"]) // "32"
|
||||
```
|
||||
|
||||
使用內置的delete函數可以刪除元素:
|
||||
使用内置的delete函数可以删除元素:
|
||||
|
||||
```Go
|
||||
delete(ages, "alice") // remove element ages["alice"]
|
||||
```
|
||||
|
||||
所有這些操作是安全的,卽使這些元素不在map中也沒有關繫;如果一個査找失敗將返迴value類型對應的零值,例如,卽使map中不存在“bob”下面的代碼也可以正常工作,因爲ages["bob"]失敗時將返迴0。
|
||||
所有这些操作是安全的,即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages["bob"]失败时将返回0。
|
||||
|
||||
```Go
|
||||
ages["bob"] = ages["bob"] + 1 // happy birthday!
|
||||
```
|
||||
|
||||
而且`x += y`和`x++`等簡短賦值語法也可以用在map上,所以上面的代碼可以改寫成
|
||||
而且`x += y`和`x++`等简短赋值语法也可以用在map上,所以上面的代码可以改写成
|
||||
|
||||
```Go
|
||||
ages["bob"] += 1
|
||||
```
|
||||
|
||||
更簡單的寫法
|
||||
更简单的写法
|
||||
|
||||
```Go
|
||||
ages["bob"]++
|
||||
```
|
||||
|
||||
但是map中的元素併不是一個變量,因此我們不能對map的元素進行取址操作:
|
||||
但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:
|
||||
|
||||
```Go
|
||||
_ = &ages["bob"] // compile error: cannot take address of map element
|
||||
```
|
||||
|
||||
禁止對map元素取址的原因是map可能隨着元素數量的增長而重新分配更大的內存空間,從而可能導致之前的地址無效。
|
||||
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
|
||||
|
||||
要想遍歷map中全部的key/value對的話,可以使用range風格的for循環實現,和之前的slice遍歷語法類似。下面的迭代語句將在每次迭代時設置name和age變量,它們對應下一個鍵/值對:
|
||||
要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:
|
||||
|
||||
```Go
|
||||
for name, age := range ages {
|
||||
@@ -76,7 +76,7 @@ for name, age := range ages {
|
||||
}
|
||||
```
|
||||
|
||||
Map的迭代順序是不確定的,併且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中,遍歷的順序是隨機的,每一次遍歷的順序都不相同。這是故意的,每次都使用隨機的遍歷順序可以強製要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對,我們必須顯式地對key進行排序,可以使用sort包的Strings函數對字符串slice進行排序。下面是常見的處理方式:
|
||||
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:
|
||||
|
||||
```Go
|
||||
import "sort"
|
||||
@@ -91,15 +91,15 @@ for _, name := range names {
|
||||
}
|
||||
```
|
||||
|
||||
因爲我們一開始就知道names的最終大小,因此給slice分配一個合適的大小將會更有效。下面的代碼創建了一個空的slice,但是slice的容量剛好可以放下map中全部的key:
|
||||
因为我们一开始就知道names的最终大小,因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key:
|
||||
|
||||
```Go
|
||||
names := make([]string, 0, len(ages))
|
||||
```
|
||||
|
||||
在上面的第一個range循環中,我們隻關心map中的key,所以我們忽略了第二個循環變量。在第二個循環中,我們隻關心names中的名字,所以我們使用“_”空白標識符來忽略第一個循環變量,也就是迭代slice時的索引。
|
||||
在上面的第一个range循环中,我们只关心map中的key,所以我们忽略了第二个循环变量。在第二个循环中,我们只关心names中的名字,所以我们使用“_”空白标识符来忽略第一个循环变量,也就是迭代slice时的索引。
|
||||
|
||||
map類型的零值是nil,也就是沒有引用任何哈希表。
|
||||
map类型的零值是nil,也就是没有引用任何哈希表。
|
||||
|
||||
```Go
|
||||
var ages map[string]int
|
||||
@@ -107,30 +107,30 @@ fmt.Println(ages == nil) // "true"
|
||||
fmt.Println(len(ages) == 0) // "true"
|
||||
```
|
||||
|
||||
map上的大部分操作,包括査找、刪除、len和range循環都可以安全工作在nil值的map上,它們的行爲和一個空的map類似。但是向一個nil值的map存入元素將導致一個panic異常:
|
||||
map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:
|
||||
|
||||
```Go
|
||||
ages["carol"] = 21 // panic: assignment to entry in nil map
|
||||
```
|
||||
|
||||
在向map存數據前必須先創建map。
|
||||
在向map存数据前必须先创建map。
|
||||
|
||||
通過key作爲索引下標來訪問map將産生一個value。如果key在map中是存在的,那麽將得到與key對應的value;如果key不存在,那麽將得到value對應類型的零值,正如我們前面看到的ages["bob"]那樣。這個規則很實用,但是有時候可能需要知道對應的元素是否眞的是在map之中。例如,如果元素類型是一個數字,你可以需要區分一個已經存在的0,和不存在而返迴零值的0,可以像下面這樣測試:
|
||||
通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值,正如我们前面看到的ages["bob"]那样。这个规则很实用,但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可以需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:
|
||||
|
||||
```Go
|
||||
age, ok := ages["bob"]
|
||||
if !ok { /* "bob" is not a key in this map; age == 0. */ }
|
||||
```
|
||||
|
||||
你會經常看到將這兩個結合起來使用,像這樣:
|
||||
你会经常看到将这两个结合起来使用,像这样:
|
||||
|
||||
```Go
|
||||
if age, ok := ages["bob"]; !ok { /* ... */ }
|
||||
```
|
||||
|
||||
在這種場景下,map的下標語法將産生兩個值;第二個是一個布爾值,用於報告元素是否眞的存在。布爾變量一般命名爲ok,特别適合馬上用於if條件判斷部分。
|
||||
在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。
|
||||
|
||||
和slice一樣,map之間也不能進行相等比較;唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value,我們必須通過一個循環實現:
|
||||
和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:
|
||||
|
||||
```Go
|
||||
func equal(x, y map[string]int) bool {
|
||||
@@ -146,14 +146,14 @@ func equal(x, y map[string]int) bool {
|
||||
}
|
||||
```
|
||||
|
||||
要註意我們是如何用!ok來區分元素缺失和元素不同的。我們不能簡單地用xv != y[k]判斷,那樣會導致在判斷下面兩個map時産生錯誤的結果:
|
||||
要注意我们是如何用!ok来区分元素缺失和元素不同的。我们不能简单地用xv != y[k]判断,那样会导致在判断下面两个map时产生错误的结果:
|
||||
|
||||
```Go
|
||||
// True if equal is written incorrectly.
|
||||
equal(map[string]int{"A": 0}, map[string]int{"B": 42})
|
||||
```
|
||||
|
||||
Go語言中併沒有提供一個set類型,但是map中的key也是不相同的,可以用map實現類似set的功能。爲了説明這一點,下面的dedup程序讀取多行輸入,但是隻打印第一次出現的行。(它是1.3節中出現的dup程序的變體。)dedup程序通過map來表示所有的輸入行所對應的set集合,以確保已經在集合存在的行不會被重複打印。
|
||||
Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。为了说明这一点,下面的dedup程序读取多行输入,但是只打印第一次出现的行。(它是1.3节中出现的dup程序的变体。)dedup程序通过map来表示所有的输入行所对应的set集合,以确保已经在集合存在的行不会被重复打印。
|
||||
|
||||
<u><i>gopl.io/ch4/dedup</i></u>
|
||||
```Go
|
||||
@@ -175,11 +175,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
Go程序員將這種忽略value的map當作一個字符串集合,併非所有`map[string]bool`類型value都是無關緊要的;有一些則可能會同時包含true和false的值。
|
||||
Go程序员将这种忽略value的map当作一个字符串集合,并非所有`map[string]bool`类型value都是无关紧要的;有一些则可能会同时包含true和false的值。
|
||||
|
||||
有時候我們需要一個map或set的key是slice類型,但是map的key必須是可比較的類型,但是slice併不滿足這個條件。不過,我們可以通過兩個步驟繞過這個限製。第一步,定義一個輔助函數k,將slice轉爲map對應的string類型的key,確保隻有x和y相等時k(x) == k(y)才成立。然後創建一個key爲string類型的map,在每次對map操作時先用k輔助函數將slice轉化爲string類型。
|
||||
有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。
|
||||
|
||||
下面的例子演示了如何使用map來記録提交相同的字符串列表的次數。它使用了fmt.Sprintf函數將字符串列表轉換爲一個字符串以用於map的key,通過%q參數忠實地記録每個字符串元素的信息:
|
||||
下面的例子演示了如何使用map来记录提交相同的字符串列表的次数。它使用了fmt.Sprintf函数将字符串列表转换为一个字符串以用于map的key,通过%q参数忠实地记录每个字符串元素的信息:
|
||||
|
||||
```Go
|
||||
var m = make(map[string]int)
|
||||
@@ -190,9 +190,9 @@ func Add(list []string) { m[k(list)]++ }
|
||||
func Count(list []string) int { return m[k(list)] }
|
||||
```
|
||||
|
||||
使用同樣的技術可以處理任何不可比較的key類型,而不僅僅是slice類型。這種技術對於想使用自定義key比較函數的時候也很有用,例如在比較字符串的時候忽略大小寫。同時,輔助函數k(x)也不一定是字符串類型,它可以返迴任何可比較的類型,例如整數、數組或結構體等。
|
||||
使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。
|
||||
|
||||
這是map的另一個例子,下面的程序用於統計輸入中每個Unicode碼點出現的次數。雖然Unicode全部碼點的數量鉅大,但是出現在特定文檔中的字符種類併沒有多少,使用map可以用比較自然的方式來跟蹤那些出現過字符的次數。
|
||||
这是map的另一个例子,下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大,但是出现在特定文档中的字符种类并没有多少,使用map可以用比较自然的方式来跟踪那些出现过字符的次数。
|
||||
|
||||
<u><i>gopl.io/ch4/charcount</i></u>
|
||||
```Go
|
||||
@@ -246,15 +246,15 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
ReadRune方法執行UTF-8解碼併返迴三個值:解碼的rune字符的值,字符UTF-8編碼後的長度,和一個錯誤值。我們可預期的錯誤值隻有對應文件結尾的io.EOF。如果輸入的是無效的UTF-8編碼的字符,返迴的將是unicode.ReplacementChar表示無效字符,併且編碼長度是1。
|
||||
ReadRune方法执行UTF-8解码并返回三个值:解码的rune字符的值,字符UTF-8编码后的长度,和一个错误值。我们可预期的错误值只有对应文件结尾的io.EOF。如果输入的是无效的UTF-8编码的字符,返回的将是unicode.ReplacementChar表示无效字符,并且编码长度是1。
|
||||
|
||||
charcount程序同時打印不同UTF-8編碼長度的字符數目。對此,map併不是一個合適的數據結構;因爲UTF-8編碼的長度總是從1到utf8.UTFMax(最大是4個字節),使用數組將更有效。
|
||||
charcount程序同时打印不同UTF-8编码长度的字符数目。对此,map并不是一个合适的数据结构;因为UTF-8编码的长度总是从1到utf8.UTFMax(最大是4个字节),使用数组将更有效。
|
||||
|
||||
作爲一個實驗,我們用charcount程序對英文版原稿的字符進行了統計。雖然大部分是英語,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符:
|
||||
作为一个实验,我们用charcount程序对英文版原稿的字符进行了统计。虽然大部分是英语,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符:
|
||||
|
||||

|
||||
|
||||
下面是不同UTF-8編碼長度的字符的數目:
|
||||
下面是不同UTF-8编码长度的字符的数目:
|
||||
|
||||
```
|
||||
len count
|
||||
@@ -264,7 +264,7 @@ len count
|
||||
4 0
|
||||
```
|
||||
|
||||
Map的value類型也可以是一個聚合類型,比如是一個map或slice。在下面的代碼中,圖graph的key類型是一個字符串,value類型map[string]bool代表一個字符串集合。從概念上將,graph將一個字符串類型的key映射到一組相關的字符串集合,它們指向新的graph的key。
|
||||
Map的value类型也可以是一个聚合类型,比如是一个map或slice。在下面的代码中,图graph的key类型是一个字符串,value类型map[string]bool代表一个字符串集合。从概念上将,graph将一个字符串类型的key映射到一组相关的字符串集合,它们指向新的graph的key。
|
||||
|
||||
<u><i>gopl.io/ch4/graph</i></u>
|
||||
```Go
|
||||
@@ -284,8 +284,8 @@ func hasEdge(from, to string) bool {
|
||||
}
|
||||
```
|
||||
|
||||
其中addEdge函數惰性初始化map是一個慣用方式,也就是説在每個值首次作爲key時才初始化。addEdge函數顯示了如何讓map的零值也能正常工作;卽使from到to的邊不存在,graph[from][to]依然可以返迴一個有意義的結果。
|
||||
其中addEdge函数惰性初始化map是一个惯用方式,也就是说在每个值首次作为key时才初始化。addEdge函数显示了如何让map的零值也能正常工作;即使from到to的边不存在,graph[from][to]依然可以返回一个有意义的结果。
|
||||
|
||||
**練習 4.8:** 脩改charcount程序,使用unicode.IsLetter等相關的函數,統計字母、數字等Unicode中不同的字符類别。
|
||||
**练习 4.8:** 修改charcount程序,使用unicode.IsLetter等相关的函数,统计字母、数字等Unicode中不同的字符类别。
|
||||
|
||||
**練習 4.9:** 編寫一個程序wordfreq程序,報告輸入文本中每個單詞出現的頻率。在第一次調用Scan前先調用input.Split(bufio.ScanWords)函數,這樣可以按單詞而不是按行輸入。
|
||||
**练习 4.9:** 编写一个程序wordfreq程序,报告输入文本中每个单词出现的频率。在第一次调用Scan前先调用input.Split(bufio.ScanWords)函数,这样可以按单词而不是按行输入。
|
||||
|
Reference in New Issue
Block a user