大家好,我是藍胖子,我一直相信程式設計是一門實踐性的技術,其中演算法也不例外,初學者可能往往對它可望而不可及,覺得很難,學了又忘,忘其實是由於沒有真正搞懂演算法的應用場景,所以我準備出一個系列,囊括我們在日常開發中常用的演算法,並結合實際的應用場景,真正的感受演算法的魅力。
今天,我們就來學習下點陣圖bitmap的原理以及作用。
程式碼已經上傳github
https://github.com/HobbyBear/codelearning/tree/master/bitmap
點陣圖作用
bitmap 是一種高效的且佔用記憶體很小的 判斷 某個值 存在與否的資料結構。它用二進位制的某一位去表示某個值是否存在。
比如我們需要統計10億使用者是否簽到,正常的做法是你可以設計一個10億長度的map,將使用者的uid設定為key,是否簽到設計為value,假設uid是int64 型別,佔用8個位元組,10億使用者就需要大約8G的記憶體 ,而如果 設計成bitmap去儲存,則只需要大約125M 。極大的節約了記憶體。
原理
因為bitmap中用二進位制位代表某個uid是否存在,所以一個位元組能夠代表8個uid是否存在,如下圖所示:
bit位為1代表所在uid的使用者已經簽到,0則代表未簽到。圖中,uid為1和5的使用者都沒有簽到,uid為2,3,4,6,7,8的使用者都已經簽到。
實現
要實現這樣一個bitmap,我們可以用一個位元組陣列來儲存所有的bit位,將bit位 設定為1 就是確認某個數字或者說是像例子裡的uid,定位uid對應在這個位元組陣列的哪個位置,將特定位置的位元組定位到以後,再定位這個uid在位元組中的bit位是在什麼位置。
整個過程看程式碼會更加清晰,如下程式碼所示,我們在bitmap的建構函式里定義了整個bitmap的最大長度。
type BitMap struct {
flags []byte
}
func New(max int64) *BitMap {
flagLen := max/8 + 1
return &BitMap{flags: make([]byte, flagLen)}
}
接著看下它的set方法,找到某個數字index再bitmap中的bit位,將其設定為1,一個位元組是8位,透過index/8 得到其bit位是在哪個位元組上,透過index%8 取餘得到設定的bit位 在位元組的第幾個bit為上,然後透過或運算將特定bit位設定為1。
func (b *BitMap) Set(index int64) {
arrIndex := index / 8
offset := index % 8
// 將offset位置設定為1,或運算,0 | 1 = 1 1|1= 1, 0|0 =0, 1的| 將原值設定為1 ,0的| 不改變原值
b.flags[arrIndex] = b.flags[arrIndex] | (0x1 << offset)
}
我還定義了Exits 方法快速判斷某個值是否被bitmap記錄,同樣也是先找到index對應的bit位 在陣列中的位置,然後透過與運算去判斷特定bit位是0還是1。
func (b *BitMap) Exits(index int64) bool {
arrIndex := index / 8
offset := index % 8
res := b.flags[arrIndex] & (0x1 << offset)
if res == 0 {
return false
}
return true
}
除此以外,你還可以定義一個remove方法,用於清除特定bit位上的值,
func (b *BitMap) Clean(index int64) {
arrIndex := index / 8
offset := index % 8
// 0 & 1 = 0 ,0 & 0 = 0, 1&1 =1 1的& 不會改變原來的值, 0的& 將原值變為0
b.flags[arrIndex] = b.flags[arrIndex] & ^(0x1 << offset)
}
你可以看到bitmap用到的位運算其實本質上是用到下面的性質:
1, 1 與 0或者 1的 & 運算不會改變原值, 0 的& 會將特定bit位設定為0。
2, 0的或運算 不會改變原來的值, 1的或運算是將原來的bit位設定為1。
整個實現並不難,但這種結構的確在大資料量下達到了節約記憶體進行排重的目的,後續講到的布隆過濾器也是在這種資料結構上實現的。