Go語言里的集合一般會用map[T]bool這種形式來表示,T代表元素類型。集合用map類型來表示雖然非常靈活,但我們可以以一種更好的形式來表示它。例如在數據流分析領域,集合元素通常是一個非負整數,集合會包含很多元素,並且集合會經常進行並集、交集操作,這種情況下,bit數組會比map表現更加理想。(譯注:這里再補充一個例子,比如我們執行一個http下載任務,把文件按照16kb一塊划分為很多塊,需要有一個全局變量來標識哪些塊下載完成了,這種時候也需要用到bit數組)
一個bit數組通常會用一個無符號數或者稱之為“字”的slice來表示,每一個元素的每一位都表示集合里的一個值。當集合的第i位被設置時,我們才說這個集合包含元素i。下面的這個程序展示了一個簡單的bit數組類型,並且實現了三個函數來對這個bit數組來進行操作:
// An IntSet is a set of small non-negative integers. // Its zero value represents the empty set. type IntSet struct { words []uint64 } // Has reports whether the set contains the non-negative value x. func (s *IntSet) Has(x int) bool { word, bit := x/64, uint(x%64) return word < len(s.words) && s.words[word]&(1<<bit) != 0 } // Add adds the non-negative value x to the set. func (s *IntSet) Add(x int) { word, bit := x/64, uint(x%64) for word >= len(s.words) { s.words = append(s.words, 0) } s.words[word] |= 1 << bit } // UnionWith sets s to the union of s and t. func (s *IntSet) UnionWith(t *IntSet) { for i, tword := range t.words { if i < len(s.words) { s.words[i] |= tword } else { s.words = append(s.words, tword) } } }
因為每一個字都有64個二進制位,所以為了定位x的bit位,我們用了x/64的商作為字的下標,並且用x%64得到的值作為這個字內的bit的所在位置。UnionWith這個方法里用到了bit位的“或”邏輯操作符號|來一次完成64個元素的或計算。(在練習6.5中我們還會程序用到這個64位字的例子。)
當前這個實現還缺少了很多必要的特性,我們把其中一些作為練習題列在本小節之后。但是有一個方法如果缺失的話我們的bit數組可能會比較難混:將IntSet作為一個字符串來打印。這里我們來實現它,讓我們來給上面的例子添加一個String方法,類似2.5節中做的那樣:
// String returns the set as a string of the form "{1 2 3}". func (s *IntSet) String() string { var buf bytes.Buffer buf.WriteByte('{') for i, word := range s.words { if word == 0 { continue } for j := 0; j < 64; j++ { if word&(1<<uint(j)) != 0 { if buf.Len() > len("{") { buf.WriteByte(' ') } fmt.Fprintf(&buf, "%d", 64*i+j) } } } buf.WriteByte('}') return buf.String() }
這里留意一下String方法,是不是和3.5.4節中的intsToString方法很相似;bytes.Buffer在String方法里經常這么用。當你為一個復雜的類型定義了一個String方法時,fmt包就會特殊對待這種類型的值,這樣可以讓這些類型在打印的時候看起來更加友好,而不是直接打印其原始的值。fmt會直接調用用戶定義的String方法。這種機制依賴於接口和類型斷言,在第7章中我們會詳細介紹。
現在我們就可以在實戰中直接用上面定義好的IntSet了:
var x, y IntSet x.Add(1) x.Add(144) x.Add(9) fmt.Println(x.String()) // "{1 9 144}" y.Add(9) y.Add(42) fmt.Println(y.String()) // "{9 42}" x.UnionWith(&y) fmt.Println(x.String()) // "{1 9 42 144}" fmt.Println(x.Has(9), x.Has(123)) // "true false"
這里要注意:我們聲明的String和Has兩個方法都是以指針類型*IntSet
來作為接收器的,但實際上對於這兩個類型來說,把接收器聲明為指針類型也沒什么必要。不過另外兩個函數就不是這樣了,因為另外兩個函數操作的是s.words對象,如果你不把接收器聲明為指針對象,那么實際操作的是拷貝對象,而不是原來的那個對象。因此,因為我們的String方法定義在IntSet指針上,所以當我們的變量是IntSet類型而不是IntSet指針時,可能會有下面這樣讓人意外的情況:
fmt.Println(&x) // "{1 9 42 144}" fmt.Println(x.String()) // "{1 9 42 144}" fmt.Println(x) // "{[4398046511618 0 65536]}"
在第一個Println中,我們打印一個*IntSet
的指針,這個類型的指針確實有自定義的String方法。第二Println,我們直接調用了x變量的String()方法;這種情況下編譯器會隱式地在x前插入&操作符,這樣相當遠我們還是調用的IntSet指針的String方法。在第三個Println中,因為IntSet類型沒有String方法,所以Println方法會直接以原始的方式理解並打印。所以在這種情況下&符號是不能忘的。在我們這種場景下,你把String方法綁定到IntSet對象上,而不是IntSet指針上可能會更合適一些,不過這也需要具體問題具體分析
上面實現的add方法和String方法也許有些人不太理解,我剛開始也不理解,重新復習了下位運算了,原來是這么簡單的騷操作:
先來簡單復習下位運算 左移動
1 << 1 0000 0001 -> 0000 0010 === 2
1 << 3 0000 0001 -> 0000 1000 === 8
&(位與):比較二進制數相對應的每一位,相對的位均為1,則對應位輸出 1,相對應有一位為0或無則為0
8 & 9 : 1000 & 1001 ==> 1000 ;16&8:10000 & 1000==>0
|(位或):比較二進制數相對應的每一位,相對的位有一個為1,則對應位輸出1,相對應的均為0則為0
8 | 9:1000 & 1001 ==> 1001 ; 16&8: 10000 & 1000 ==> 11000 (24)
^(位異或):比較二進制數相對應的每一位,相對的位相同,則對應位輸出0,相對應的位不同則為1
8 ^ 9 : 1000 ^ 1001 ⇒ 0001 ; 16&8: 10000 ^ 1000 ==> 11000(24)
2 | 1<<3 :
0000 0010 | 0000 1000 ==== 0000 1010
如果還沒有理解 請看下面這個數組
64*0+bit
0=>000000000000000000000000000000000000000000000000001001000010100
0=》1001000010100 對應數字是 4628
即0=》4628
------------------------------------------------------------------------------------------------------------
64*1+bit
1=>0000000000000000000000000000000000000000000000000010000000101000
1=》1001000010100 對應數字是 4628
即1=》4628
-----------------------------------------------------------------------------------------------------------
[4628,4628]
0=》1001000010100 對應數字是 4628
保存了 4位bit 分別是 [2,4,9,12]
1=》1001000010100 對應數字是 4628
保存了 4位bit 分別是 [64*1+2,64*1+4,64*1+9,64*1+12]
如果還不懂的 就復習一下 位運算,在來看看本篇幅文章