Go 小知識之 Go 中如何使用 set

今天來聊一下 Go 如何使用 set,本文將會涉及 set 和 bitset 兩種數據結構。git

注:花了點時間,將這個專題錄製成了視頻,上B 站查看 視頻github

Go 的數據結構

Go 內置的數據結構並很少。工做中,咱們最經常使用的兩種數據結構分別是 slice 和 map,即切片和映射。 其實,Go 中也有數組,切片的底層就是數組,只不過由於切片的存在,咱們平時不多使用它。golang

除了 Go 內置的數據結構,還有一些數據結構是由 Go 的官方 container 包提供,如 heap 堆、list 雙向鏈表和ring 迴環鏈表。但今天咱們不講它們,這些數據結構,對於熟手來講,看看文檔就會使用了。數組

咱們今天未來聊的是 set 和 bitset。據我所知,其餘一些語言,好比 Java,是有這兩種數據結構。但 Go 當前尚未以任何形式提供。安全

實現思路

先來看一篇文章,訪問地址 2 basic set implementations 閱讀。文中介紹了兩種 go 實現 set 的思路, 分別是 map 和 bitset。bash

有興趣能夠讀讀這篇文章,咱們接下來具體介紹下。微信

map

咱們知道,map 的 key 確定是惟一的,而這剛好與 set 的特性一致,自然保證 set 中成員的惟一性。並且經過 map 實現 set,在檢查是否存在某個元素時可直接使用 _, ok := m[key] 的語法,效率高。markdown

先來看一個簡單的實現,以下:數據結構

set := make(map[string]bool) // New empty set
set["Foo"] = true            // Add
for k := range set {         // Loop
    fmt.Println(k)
}
delete(set, "Foo")    // Delete
size := len(set)      // Size
exists := set["Foo"]  // Membership
複製代碼

經過建立 map[string]bool 來存儲 string 的集合,比較容易理解。但這裏還有個問題,map 的 value 是布爾類型,這會致使 set 多佔必定內存空間,而 set 不應有這個問題。框架

怎麼解決這個問題?

設置 value 爲空結構體,在 Go 中,空結構體不佔任何內存。固然,若是不肯定,也能夠來證實下這個結論。

unsafe.Sizeof(struct{}{}) // 結果爲 0
複製代碼

優化後的代碼,以下:

type void struct{}
var member void

set := make(map[string]void) // New empty set
set["Foo"] = member          // Add
for k := range set {         // Loop
    fmt.Println(k)
}
delete(set, "Foo")      // Delete
size := len(set)        // Size
_, exists := set["Foo"] // Membership
複製代碼

以前在網上看到有人按這個思路作了封裝,還寫了一篇文章,能夠去讀一下。

其實,github 上已經有個成熟的包,名爲 golang-set,它也是採用這個思路實現的。訪問地址 golang-set,描述中說 Docker 用的也是它。包中提供了兩種 set 實現,線程安全的 set 和非線程安全的 set。

演示一個簡單的案例。

package main

import (
	"fmt"

	mapset "github.com/deckarep/golang-set"
)

func main() {
	// 默認建立的線程安全的,若是無需線程安全
	// 可使用 NewThreadUnsafeSet 建立,使用方法都是同樣的。
	s1 := mapset.NewSet(1, 2, 3, 4)  
	fmt.Println("s1 contains 3: ", s1.Contains(3))
	fmt.Println("s1 contains 5: ", s1.Contains(5))

	// interface 參數,能夠傳遞任意類型
	s1.Add("poloxue")
	fmt.Println("s1 contains poloxue: ", s1.Contains("poloxue"))
	s1.Remove(3)
	fmt.Println("s1 contains 3: ", s1.Contains(3))

	s2 := mapset.NewSet(1, 3, 4, 5)

	// 並集
	fmt.Println(s1.Union(s2))
}
複製代碼

輸出以下:

s1 contains 3:  true
s1 contains 5:  false
s1 contains poloxue:  true
s1 contains 3:  false
Set{4, polxue, 1, 2, 3, 5}
複製代碼

例子中演示了簡單的使用方式,若是有不明白的,看下源碼,這些數據結構的操做方法名都是很常見的,好比交集 Intersect、差集 Difference 等,一看就懂。

bitset

繼續聊聊 bitset,bitset 中每一個數子用一個 bit 即能表示,對於一個 int8 的數字,咱們能夠用它表示 8 個數字,能幫助咱們大大節省數據的存儲空間。

bitset 最多見的應用有 bitmap 和 flag,即位圖和標誌位。這裏,咱們先嚐試用它表示一些操做的標誌位。好比某個場景,咱們須要三個 flag 分別表示權限一、權限2和權限3,並且幾個權限能夠共存。咱們能夠分別用三個常量 F一、F二、F3 表示位 Mask。

示例代碼以下(引用自文章 Bitmasks, bitsets and flags):

type Bits uint8

const (
    F0 Bits = 1 << iota
    F1
    F2
)

func Set(b, flag Bits) Bits    { return b | flag }
func Clear(b, flag Bits) Bits  { return b &^ flag }
func Toggle(b, flag Bits) Bits { return b ^ flag }
func Has(b, flag Bits) bool    { return b&flag != 0 }

func main() {
    var b Bits
    b = Set(b, F0)
    b = Toggle(b, F2)
    for i, flag := range []Bits{F0, F1, F2} {
        fmt.Println(i, Has(b, flag))
    }
}
複製代碼

例子中,咱們原本須要三個數才能表示這三個標誌,但如今經過一個 uint8 就能夠。bitset 的一些操做,如設置 Set、清除 Clear、切換 Toggle、檢查 Has 經過位運算就能夠實現,並且很是高效。

bitset 對集合操做有着自然的優點,直接經過位運算符即可實現。好比交集、並集、和差集,示例以下:

  • 交集:a & b
  • 並集:a | b
  • 差集:a & (~b)

底層的語言、庫、框架常會使用這種方式設置標誌位。

以上的例子中只展現了少許數據的處理方式,uint8 佔 8 bit 空間,只能表示 8 個數字。那大數據場景可否可使用這套思路呢?

咱們能夠把 bitset 和 Go 中的切片結合起來,從新定義 Bits 類型,以下:

type Bitset struct {
    data []int64
}
複製代碼

但如此也會產生一些問題,設置 bit,咱們怎麼知道它在哪裏呢?仔細想一想,這個位置信息包含兩部分,即保存該 bit 的數在切片索引位置和該 bit 在數字中的哪位,分別將它們命名爲 index 和 position。那怎麼獲取?

index 能夠經過整除獲取,好比咱們想知道表示 65 的 bit 在切片的哪一個 index,經過 65 / 64 便可得到,若是爲了高效,也能夠用位運算實現,即用移位替換除法,好比 65 >> 6,6 表示移位偏移,即 2^n = 64 的 n。

postion 是除法的餘數,咱們能夠經過模運算得到,好比 65 % 64 = 1,一樣爲了效率,也有相應的位運算實現,好比 65 & 0b00111111,即 65 & 63。

一個簡單例子,以下:

package main

import (
	"fmt"
)

const (
	shift = 6
	mask  = 0x3f // 即0b00111111
)

type Bitset struct {
	data []int64
}

func NewBitSet(n int) *Bitset {
	// 獲取位置信息
	index := n >> shift

	set := &Bitset{
		data: make([]int64, index+1),
	}

	// 根據 n 設置 bitset
	set.data[index] |= 1 << uint(n&mask)

	return set
}

func (set *Bitset) Contains(n int) bool {
	// 獲取位置信息
	index := n >> shift
	return set.data[index]&(1<<uint(n&mask)) != 0
}

func main() {
	set := NewBitSet(65)
	fmt.Println("set contains 65", set.Contains(65))
	fmt.Println("set contains 64", set.Contains(64))
}
複製代碼

輸出結果

set contains 65 true
set contains 64 false
複製代碼

以上的例子功能很簡單,只是爲了演示,只有建立 bitset 和 contains 兩個功能,其餘諸如添加、刪除、不一樣 bitset 間的交、並、差尚未實現。有興趣的朋友能夠繼續嘗試。

其實,bitset 包也有人實現了,github地址 bit。能夠讀讀它的源碼,實現思路和上面介紹差很少。

下面是一個使用案例。

package main

import (
	"fmt"

	"github.com/yourbasic/bit"
)

func main() {
	s := bit.New(2, 3, 4, 65, 128)
	fmt.Println("s contains 65", s.Contains(65))
	fmt.Println("s contains 15", s.Contains(15))

	s.Add(15)
	fmt.Println("s contains 15", s.Contains(15))

	fmt.Println("next 20 is ", s.Next(20))
	fmt.Println("prev 20 is ", s.Prev(20))

	s2 := bit.New(10, 22, 30)

	s3 := s.Or(s2)
	fmt.Println("next 20 is ", s3.Next(20))

	s3.Visit(func(n int) bool {
		fmt.Println(n)
		return false  // 返回 true 表示終止遍歷
	})
}
複製代碼

執行結果:

s contains 65 true
s contains 15 false
s contains 15 true
next 20 is 65
prev 20 is 15
next 20 is 22
2
3
4
10
15
22
30
65
128
複製代碼

代碼的意思很好理解,就是一些增刪改查和集合的操做。要注意的是,bitset 和前面的 set 的區別,bitset 的成員只能是 int 整型,沒有 set 靈活。平時的使用場景也比較少,主要用在對效率和存儲空間要求較高的場景。

總結

本文介紹了Go 中兩種 set 的實現原理,並在此基礎介紹了對應於它們的兩個包簡單使用。我以爲,經過這篇文章,Go 中 set 的使用,基本均可以搞定了。

除這兩個包,再補充兩個。 zoumo/gosetgithub.com/willf/bitse…


波羅學的微信公衆號
相關文章
相關標籤/搜索