Go位集合相關(guān)操作bitset庫安裝使用
簡介
我們都知道計(jì)算機(jī)是基于二進(jìn)制的,位運(yùn)算是計(jì)算機(jī)的基礎(chǔ)運(yùn)算。位運(yùn)算的優(yōu)勢很明顯,CPU 指令原生支持、速度快?;谖贿\(yùn)算的位集合在有限的場景中替換集合數(shù)據(jù)結(jié)構(gòu)可以收到意想不到的效果。bitset庫實(shí)現(xiàn)了位集合及相關(guān)操作,不妨拿來即用。
安裝
本文代碼使用 Go Modules。
創(chuàng)建目錄并初始化:
$ mkdir -p bitset && cd bitset $ go mod init github.com/darjun/go-daily-lib/bitset
安裝bitset
庫:
$ go get -u github.com/bits-and-blooms/bitset
使用
位集合的基本操作有:
- 檢查位(Test):檢查某個(gè)索引是否為 1。類比檢查元素是否在集合中
- 設(shè)置位(Set):將某個(gè)索引設(shè)置為 1。類比向集合添加元素
- 清除位(Clear):將某個(gè)索引清除,設(shè)置為 0。類比從集合中刪除元素
- 翻轉(zhuǎn)位(Flip):如果某個(gè)索引為 1,則設(shè)置為 0,反之設(shè)置為 1
- 并(Union):兩個(gè)位集合執(zhí)行并操作。類比集合的并
- 交(Intersection):兩個(gè)位集合執(zhí)行交操作。類比集合的交
位集合一般用于小數(shù)值的非負(fù)整數(shù)的場景中。就拿游戲中簡單的簽到舉例吧,很多游戲都有簽到活動(dòng),短的有 7 天的,長的有 30 天。這種就很適合使用位集合。每個(gè)位的值表示其索引位置對應(yīng)的那天有沒有簽到。
type Player struct { sign *bitset.BitSet } func NewPlayer(sign uint) *Player { return &Player{ sign: bitset.From([]uint64{uint64(sign)}), } } func (this *Player) Sign(day uint) { this.sign.Set(day) } func (this *Player) IsSigned(day uint) bool { return this.sign.Test(day) } func main() { player := NewPlayer(1) // 第一天簽到 for day := uint(2); day <= 7; day++ { if rand.Intn(100)&1 == 0 { player.Sign(day - 1) } } for day := uint(1); day <= 7; day++ { if player.IsSigned(day - 1) { fmt.Printf("day:%d signed\n", day) } } }
bitset 提供了多種創(chuàng)建 BitSet 對象的方法。
首先 bitset.BitSet 零值可用,如果一開始不知道有多少元素,可以使用這種方式創(chuàng)建:
var b bitset.BitSet
BitSet 在設(shè)置時(shí)自動(dòng)調(diào)整大小。如果事先知道長度,創(chuàng)建 BitSet 時(shí)可傳入此值,能有效避免自動(dòng)調(diào)整的開銷:
b := bitset.New(100)
bitset 結(jié)構(gòu)支持鏈?zhǔn)秸{(diào)用,大部分方法返回自身的指針,所以可以這樣寫:
b.Set(10).Set(11).Clear(12).Flip(13);
注意,bitset 的索引是從 0 開始的。
記得之前在網(wǎng)上看過一道題:
一個(gè)農(nóng)夫帶著一只狼、一頭羊和一顆白菜來到河邊。他需要用船把他們帶到對岸。然而,這艘船只能容下農(nóng)夫本人和另外一種東西(要么是狼,要么是羊,要么是白菜)。如果農(nóng)夫不在場的話,狼就會吃掉羊,羊會吃掉白菜。請為農(nóng)夫解決這個(gè)問題。
這其實(shí)是一個(gè)狀態(tài)搜索的問題,用回溯法就能解決。農(nóng)夫、狼、羊、白菜都有兩個(gè)狀態(tài),即在河左岸(假設(shè)剛開始農(nóng)夫所處的是左岸)還是河右岸。這里實(shí)際上還有個(gè)船的狀態(tài),由于船一定和農(nóng)夫的狀態(tài)是一致的,就不用額外考慮了。這些狀態(tài)我們很容易用位集合來表示:
const ( FARMER = iota WOLF SHEEP CABBAGE )
編寫一個(gè)函數(shù)來判斷狀態(tài)是否合法。有兩種狀態(tài)不合法:
- 狼和羊在同一邊,并且不和農(nóng)夫在同一邊。此時(shí)狼會吃掉羊
- 羊和白菜在同一邊,并且不和農(nóng)夫在同一邊。此時(shí)羊會吃掉白菜
func IsStateValid(state *bitset.BitSet) bool { if state.Test(WOLF) == state.Test(SHEEP) && state.Test(WOLF) != state.Test(FARMER) { // 狼和羊在同一邊,并且不和農(nóng)夫在同一邊 // 狼會吃掉羊,非法 return false } if state.Test(SHEEP) == state.Test(CABBAGE) && state.Test(SHEEP) != state.Test(FARMER) { // 羊和白菜在同一邊,并且不和農(nóng)夫在同一邊 // 羊會吃掉白菜,非法 return false } return true }
接下來編寫搜索函數(shù):
func search(b *bitset.BitSet, visited map[string]struct{}) bool { if !IsStateValid(b) { return false } if _, exist := visited[b.String()]; exist { // 狀態(tài)已遍歷 return false } if b.Count() == 4 { return true } visited[b.String()] = struct{}{} for index := uint(FARMER); index <= CABBAGE; index++ { if b.Test(index) != b.Test(FARMER) { // 與農(nóng)夫不在一邊,不能帶上船 continue } // 帶到對岸去 b.Flip(index) if index != FARMER { // 如果 index 為 FARMER,表示不帶任何東西 b.Flip(FARMER) } if search(b, visited) { return true } // 狀態(tài)恢復(fù) b.Flip(index) if index != FARMER { b.Flip(FARMER) } } return false }
主函數(shù):
func main() { b := bitset.New(4) visited := make(map[string]struct{}) fmt.Println(search(b, visited)) }
初始時(shí),所有狀態(tài)為 0,都到對岸之后所有狀態(tài)為 1,故b.Count() == 4
表示都已到達(dá)對岸了。由于搜索是盲目的,可能會無限循環(huán):這次農(nóng)夫?qū)⒀驇У綄Π叮麓斡謱⑵鋸膶Π稁Щ貋砹?。所以我們需要做狀態(tài)去重。bitset.String()
返回當(dāng)前位集合的字符串表示,我們以此來判斷狀態(tài)是否重復(fù)。
for 循環(huán)依次嘗試帶各種物品,或什么也不帶。驅(qū)動(dòng)查找過程。
如果想得到農(nóng)夫正確的動(dòng)作序列,可以給 search 加一個(gè)參數(shù),記錄每一步的操作:
func search(b *bitset.BitSet, visited map[string]struct{}, path *[]*bitset.BitSet) bool { // 記錄路徑 *path = append(*path, b.Clone()) if b.Count() == 4 { return true } // ... *path = (*path)[:len(*path)-1] return false } func main() { b := bitset.New(4) visited := make(map[string]struct{}) var path []*bitset.BitSet if search(b, visited, &path) { PrintPath(path) } }
如果找到解,打印之:
var names = []string{"農(nóng)夫", "狼", "羊", "白菜"} func PrintState(b *bitset.BitSet) { fmt.Println("=======================") fmt.Println("河左岸:") for index := uint(FARMER); index <= CABBAGE; index++ { if !b.Test(index) { fmt.Println(names[index]) } } fmt.Println("河右岸:") for index := uint(FARMER); index <= CABBAGE; index++ { if b.Test(index) { fmt.Println(names[index]) } } fmt.Println("=======================") } func PrintMove(cur, next *bitset.BitSet) { for index := uint(WOLF); index <= CABBAGE; index++ { if cur.Test(index) != next.Test(index) { if !cur.Test(FARMER) { fmt.Printf("農(nóng)夫?qū)ⅰ?s】從河左岸帶到河右岸\n", names[index]) } else { fmt.Printf("農(nóng)夫?qū)ⅰ?s】從河右岸帶到河左岸\n", names[index]) } return } } if !cur.Test(FARMER) { fmt.Println("農(nóng)夫獨(dú)自從河左岸到河右岸") } else { fmt.Println("農(nóng)夫獨(dú)自從河右岸到河左岸") } } func PrintPath(path []*bitset.BitSet) { cur := path[0] PrintState(cur) for i := 1; i < len(path); i++ { next := path[i] PrintMove(cur, next) PrintState(next) cur = next } }
運(yùn)行結(jié)果:
=======================
河左岸:
農(nóng)夫
狼
羊
白菜
河右岸:
=======================
農(nóng)夫?qū)ⅰ狙颉繌暮幼蟀稁У胶佑野?br />=======================
河左岸:
狼
白菜
河右岸:
農(nóng)夫
羊
=======================
農(nóng)夫獨(dú)自從河右岸到河左岸
=======================
河左岸:
農(nóng)夫
狼
白菜
河右岸:
羊
=======================
農(nóng)夫?qū)ⅰ纠恰繌暮幼蟀稁У胶佑野?br />=======================
河左岸:
白菜
河右岸:
農(nóng)夫
狼
羊
=======================
農(nóng)夫?qū)ⅰ狙颉繌暮佑野稁У胶幼蟀?br />=======================
河左岸:
農(nóng)夫
羊
白菜
河右岸:
狼
=======================
農(nóng)夫?qū)ⅰ景撞恕繌暮幼蟀稁У胶佑野?br />=======================
河左岸:
羊
河右岸:
農(nóng)夫
狼
白菜
=======================
農(nóng)夫獨(dú)自從河右岸到河左岸
=======================
河左岸:
農(nóng)夫
羊
河右岸:
狼
白菜
=======================
農(nóng)夫?qū)ⅰ狙颉繌暮幼蟀稁У胶佑野?br />=======================
河左岸:
河右岸:
農(nóng)夫
狼
羊
白菜
=======================
即農(nóng)夫操作過程為:將羊帶到右岸->獨(dú)自返回->將白菜帶到右岸->再將羊帶回左岸->帶上狼到右岸->獨(dú)自返回->最后將羊帶到右岸->完成。
為什么要使用它?
似乎直接使用位運(yùn)算就可以了,為什么要使用 bitset 庫呢?
因?yàn)橥ㄓ茫厦媪信e的兩個(gè)例子都是很小的整數(shù)值,如果整數(shù)值超過 64 位,我們必然要通過切片來存儲。此時(shí)手寫操作就非常不便了,而且容易出錯(cuò)。
庫的優(yōu)勢體現(xiàn)在:
- 足夠通用
- 持續(xù)優(yōu)化
- 大規(guī)模使用
只要對外提供的接口保持不變,它可以一直優(yōu)化內(nèi)部實(shí)現(xiàn)。雖然我們也可以做,但是費(fèi)時(shí)費(fèi)力。而且有些優(yōu)化涉及到比較復(fù)雜的算法,自己實(shí)現(xiàn)的難度較高且易錯(cuò)。
有一個(gè)很典型的例子,求一個(gè) uint64 的二進(jìn)制表示中 1 的數(shù)量(popcnt,或 population count)。實(shí)現(xiàn)的方法有很多。
最直接的,我們一位一位地統(tǒng)計(jì):
func popcnt1(n uint64) uint64 { var count uint64 for n > 0 { if n&1 == 1 { count++ } n >>= 1 } return count }
考慮空間換時(shí)間,我們可以預(yù)先計(jì)算 0-255 這 256 個(gè)數(shù)字的二進(jìn)制表示中 1 的個(gè)數(shù),然后每 8 位計(jì)算一次,可能將計(jì)算次數(shù)減少到之前的 1/8。這也是標(biāo)準(zhǔn)庫中的做法:
const pop8tab = "" + "\x00\x01\x01\x02\x01\x02\x02\x03\x01\x02\x02\x03\x02\x03\x03\x04" + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + "\x04\x05\x05\x06\x05\x06\x06\x07\x05\x06\x06\x07\x06\x07\x07\x08" func popcnt2(n uint64) uint64 { var count uint64 for n > 0 { count += uint64(pop8tab[n&0xff]) n >>= 8 } return count }
最后是 bitset 庫中的算法:
func popcnt3(x uint64) (n uint64) { x -= (x >> 1) & 0x5555555555555555 x = (x>>2)&0x3333333333333333 + x&0x3333333333333333 x += x >> 4 x &= 0x0f0f0f0f0f0f0f0f x *= 0x0101010101010101 return x >> 56 }
對以上三種實(shí)現(xiàn)進(jìn)行性能測試:
goos: windows goarch: amd64 pkg: github.com/darjun/go-daily-lib/bitset/popcnt cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz BenchmarkPopcnt1-8 52405 24409 ns/op BenchmarkPopcnt2-8 207452 5443 ns/op BenchmarkPopcnt3-8 1777320 602 ns/op PASS ok github.com/darjun/go-daily-lib/bitset/popcnt 4.697s
popcnt3 相對 popcnt1 有 40 倍的性能提升。在學(xué)習(xí)上我們可以自己嘗試造輪子,以此來加深自己對技術(shù)的理解。但是在工程上,通常更傾向于使用穩(wěn)定的、高效的庫。
總結(jié)
本文借著 bitset 庫介紹了位集合的使用。并且應(yīng)用 bitset 解決了農(nóng)夫過河問題。最后我們討論了為什么要使用庫?
大家如果發(fā)現(xiàn)好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??
一點(diǎn)閑話
我發(fā)現(xiàn)人的惰性實(shí)在是太可怕了。雖然這半年來沒寫文章一開始是因?yàn)楣ぷ魃系脑颍髞韱渭兪且驗(yàn)閼T性,因?yàn)閼?。而且總?ldquo;裝著很忙”來逃避需要花時(shí)間、花精力的事情。在這種想動(dòng)又不想動(dòng)的角逐之間,時(shí)間就這么一晃而過。
我們總是在抱怨沒有時(shí)間,沒有時(shí)間。但仔細(xì)想想,仔細(xì)算算,我們花在刷短視頻,玩游戲上的時(shí)間其實(shí)并不少。
上周我在阮一峰老師的周刊上看到一篇文章《人生不短》,看了之后深有觸動(dòng)。人總該有所堅(jiān)持,生活才有意義。
參考
- bitset GitHub:github.com/bits-and-blooms/bitset
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
以上就是Go位集合相關(guān)操作bitset庫安裝使用的詳細(xì)內(nèi)容,更多關(guān)于Go位集合bitset庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang官方限流器庫實(shí)現(xiàn)限流示例詳解
這篇文章主要為大家介紹了Golang官方限流器庫使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08gorm+gin實(shí)現(xiàn)restful分頁接口的實(shí)踐
本文主要介紹了gorm+gin實(shí)現(xiàn)restful分頁接口的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12Golang微服務(wù)框架Kratos實(shí)現(xiàn)分布式任務(wù)隊(duì)列Asynq的方法詳解
任務(wù)隊(duì)列(Task Queue) 一般用于跨線程或跨計(jì)算機(jī)分配工作的一種機(jī)制,在Golang語言里面,我們有像Asynq和Machinery這樣的類似于Celery的分布式任務(wù)隊(duì)列,本文就給大家詳細(xì)介紹一下Golang微服務(wù)框架Kratos實(shí)現(xiàn)分布式任務(wù)隊(duì)列Asynq的方法,需要的朋友可以參考下2023-09-09go語言中切片與內(nèi)存復(fù)制 memcpy 的實(shí)現(xiàn)操作
這篇文章主要介紹了go語言中切片與內(nèi)存復(fù)制 memcpy 的實(shí)現(xiàn)操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04Go中g(shù)routine通信與context控制實(shí)例詳解
隨著context包的引入,標(biāo)準(zhǔn)庫中很多接口因此加上了context參數(shù),下面這篇文章主要給大家介紹了關(guān)于Go中g(shù)routine通信與context控制的相關(guān)資料,需要的朋友可以參考下2022-02-02