gopl-zh.github.com/ch4/ch4-03.md
2015-12-29 17:45:01 +08:00

195 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 4.3. Map
哈希表是一種巧妙併且實用的數據結構。它是一個無序的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數據類型則沒有任何的限製。
內置的make函數可以創建一個map
```Go
ages := make(map[string]int) // mapping from strings to ints
```
我們也可以用map字面值的語法創建map同時還可以指定一些最初的key/value
```Go
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
```
這相當於
```Go
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
```
因此另一種創建空的map的表達式是map[string]int{}。
Map中的元素通過key對應的下標語法訪問
```Go
ages["alice"] = 32
fmt.Println(ages["alice"]) // "32"
```
使用內置的delete函數可以刪除元素
```Go
delete(ages, "alice") // remove element ages["alice"]
```
所有這些操作是安全的卽使這些元素不在map中也沒有關繫如果一個査找失敗將返迴value類型對應的零值例如卽使map中不存在“bob”下面的代碼也可以正常工作因爲ages["bob"]失敗時將返迴0。
```Go
ages["bob"] = ages["bob"] + 1 // happy birthday!
```
而且x += y和x++等簡短賦值語法也可以用在map上所以上面的代碼可以改寫成
```Go
ages["bob"] += 1
```
更簡單的寫法
```Go
ages["bob"]++
```
但是map中的元素併不是一個變量因此我們不能對map的元素進行取址操作
```Go
_ = &ages["bob"] // compile error: cannot take address of map element
```
禁止對map元素取址的原因是map可能隨着元素數量的增長而重新分配更大的內存空間從而可能導致之前的地址無效。
要想遍歷map中全部的key/value對的話可以使用range風格的for循環實現和之前的slice遍歷語法類似。下面的迭代語句將在每次迭代時設置name和age變量它們對應下一個鍵/值對:
```Go
for name, age := range ages {
fmt.Printf("%s\t%d\n", name, age)
}
```
Map的迭代順序是不確定的併且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中遍歷的順序是隨機的每一次遍歷的順序都不相同。這是故意的每次都使用隨機的遍歷順序可以強製要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對我們必鬚顯式地對key進行排序可以使用sort包的Strings函數對字符串slice進行排序。下面是常見的處理方式
```Go
import "sort"
var names []string
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
```
因爲我們一開始就知道names的最終大小因此給slice分配一個合適的大小將會更有效。下面的代碼創建了一個空的slice但是slice的容量剛好可以放下map中全部的key
```Go
names := make([]string, 0, len(ages))
```
在上面的第一個range循環中我們隻關心map中的key所以我們忽略了第二個循環變量。在第二個循環中我們隻關繫names中的名字所以我們使用“_”空白標識符來忽略第一個循環變量也就是迭代slice時的索引。
map類型的零值是nil也就是沒有引用任何哈希表。
```Go
var ages map[string]int
fmt.Println(ages == nil) // "true"
fmt.Println(len(ages) == 0) // "true"
```
map上的大部分操作包括査找、刪除、len和range循環都可以安全工作在nil值的map上它們的行爲和一個空的map類似。但是向一個nil值的map存入元素將導致一個panic異常
```Go
ages["carol"] = 21 // panic: assignment to entry in nil map
```
在向map存數據前必鬚先創建map。
通過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條件判斷部分。
和slice一樣map之間也不能進行相等比較唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value我們必鬚通過一個循環實現
```Go
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
```
要註意我們是如何用!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實現類似的功能。爲了説明這一點下面的dedup程序讀取多行輸入但是隻打印第一次出現的行。它是1.3節中出現的dup程序的變體。dedup程序通過map來表示所有的輸入行所對應的set集合以確保已經在集合存在的行不會被重複打印。
```Go
gopl.io/ch4/dedup
func main() {
seen := make(map[string]bool) // a set of strings
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] {
seen[line] = true
fmt.Println(line)
}
}
if err := input.Err(); err != nil {
fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
os.Exit(1)
}
}
```
Go程序員將這種忽略value的map當作一個字符串集合併非所有map[string]bool類型value都是無關緊要的有一些則可能會同時包含tue和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來記録提交相同的字符串列表的次數。它使用了fmt.Sprintf函數將字符串列表轉換爲一個字符串以用於map的key通過%q參數忠實地記録每個字符串元素的信息
```Go
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }
```
TODO