1. 程式人生 > 實用技巧 >golang 實現Bit陣列

golang 實現Bit陣列

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指標上可能會更合適一些,不過這也需要具體問題具體分析