背景
Golang語言本身未實作set,但是實作了map
golang的map是一種無序的鍵值對的集合,其中鍵是唯一的
而set是鍵的不重復的集合,因此可以用map來實作set
Empty
由于map是key-value集合,如果使用map來實作set,則不需要關注value的具體型別和值
struct{}是具有零個元素的struct,struct{}的大小為0,不占用空間,因此十分適合作為value使用
type Empty struct{}
Int64HashSet
Golang是靜態強型別語言,對于int8、uint8、int64、uint64、 string基礎資料型別的set,均需要實作類似的代碼
定義
type Int8HashSet map[int8]Empty type UintHashSet map[uint8]Empty type Int64HashSet map[int64]Empty type Uint64HashSet map[uint64]Empty type Int64HashSet map[string]Empty
以int64為例,實作set的基本操作
初始化
func NewInt64HashSet(cap ...int) Int64HashSet {
var set Int64HashSet
if len(cap) == 0 {
set = make(Int64HashSet)
} else {
set = make(Int64HashSet, cap[0])
}
return set
}
插入
func (set Int64HashSet) Insert(items ...int64) {
for _, item := range items {
set[item] = Empty{}
}
}
洗掉
func (set Int64HashSet) Delete(items ...int64) {
for _, item := range items {
delete(set, item)
}
}
串列
func (set Int64HashSet) List() []int64 {
list := make([]int64, 0, len(set))
for item := range set {
list = append(list, item)
}
return list
}
弊端
采用上面的方法實作,會充斥著大量重復代碼,對于其它型別如int8,uint8,string等型別,需要單獨實作,盡管邏輯基本一致,
在Go 1.18版本之前,我們可以使用反射來避免這個問題,
使用反射在運行時推斷具體的型別,雖然有性能上的損耗,但是單次納秒級別的操作,基本可以忽略不計,
HashSet
interface{}是沒有方法的空介面,所有型別都實作了空介面
通過反射可以從interface獲取物件的值和型別
定義
type HashSet map[interface{}]Empty
初始化
func NewHashSet(cap ...int) HashSet {
var set HashSet
if len(cap) == 0 {
set = make(HashSet)
} else {
set = make(HashSet, cap[0])
}
return set
}
插入
func (set HashSet) Insert(items ...interface{}) {
for _, item := range items {
set[item] = Empty{}
}
}
洗掉
func (set HashSet) Delete(items ...interface{}) {
for _, item := range items {
delete(set, item)
}
}
串列
// 通過反射獲取到具體的型別
// 可以將int64替換為其它型別,如uint8, string等
func (set HashSet) ListInt64() []int64 {
list := make([]int64, 0, len(set))
for item := range set {
if val, ok := item.(int64); ok {
list = append(list, val)
}
}
return list
}
func (set HashSet) ListString() []string {
list := make([]string, 0, len(set))
for item := range set {
if val, ok := item.(string); ok {
list = append(list, val)
}
}
return list
}
GenericHashSet
反射在編譯時缺少型別檢查,比如對于同一個set,先后插入int型別和string型別資料,在編譯和運行階段均不會報錯,
hash := NewHashSet(8)
// 插入int型別
hash.Insert(111)
// 插入string型別
hash.Insert("string")
使用反射在一定程度上避免了大量的重復代碼,但是將set轉換為slice還是會存在重復的相似邏輯的代碼
并且需要在運行時獲取/判斷物件的型別和值,存在一定的性能損耗
在Go 1.18版本提供了范型(Generics)的支持,
范型可以在編譯期間進行型別檢查和型別推斷,相對于反射機制而言,性能有所提升
定義
type GenericHashSet[T comparable] map[T]Empty
初始化
func NewGenericHashSet[T comparable](cap ...int) *GenericHashSet[T] {
var set GenericHashSet[T]
if len(cap) == 0 {
set = make(GenericHashSet[T])
} else {
set = make(GenericHashSet[T], cap[0])
}
return &set
}
插入
func (set *GenericHashSet[T]) Insert(items ...T) {
for _, item := range items {
(*set)[item] = Empty{}
}
}
洗掉
func (set *GenericHashSet[T]) Delete(items ...T) {
for _, item := range items {
delete(*set, item)
}
}
串列
func (set *GenericHashSet[T]) List() []T {
list := make([]T, 0, len(*set))
for item := range *set {
list = append(list, item)
}
return list
}
性能對比
插入操作測驗代碼
func BenchmarkInt64HashSetInsert(b *testing.B) {
intHashSet := NewInt64HashSet()
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
intHashSet.Insert(rand.Int63())
}
}
func BenchmarkGenericHashSetInsert(b *testing.B) {
gHashSet := NewGenericHashSet[int64]()
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
gHashSet.Insert(rand.Int63())
}
}
func BenchmarkHashSetInsert(b *testing.B) {
hashSet := NewHashSet()
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
hashSet.Insert(rand.Int63())
}
}
插入操作測驗結果
zbwdeAir:set zbw$ go test -bench="BenchmarkInt64HashSetInsert|BenchmarkGenericHashSetInsert|BenchmarkHashSetInsert" -benchmem goos: darwin goarch: arm64 pkg: set/set BenchmarkInt64HashSetInsert-8 10051916 119.2 ns/op 40 B/op 0 allocs/op BenchmarkGenericHashSetInsert-8 13957741 123.7 ns/op 57 B/op 0 allocs/op BenchmarkHashSetInsert-8 6526810 188.9 ns/op 63 B/op 1 allocs/op PASS ok set/set 4.897s
可以看出來,Int64HashSet性能最優,GenericHashSet次之,HashSet性能最差,
從實際使用角度看
對于Go < 1.18版本,使用HashSet即可,如果追求性能的極致,不介意大量重復代碼,那還是使用Int64HashSet
對于單次操作的時間在ns級別,對于大部分業務場景,反射帶來的性能損耗基本可以忽略,性能的瓶頸并不在這里,
對于Go >= 1.18版本,可以使用GenericHashSet
其它
如果需要實作有序set,則需要鏈表輔助實作
詳細代碼,見github
如果你覺得還可以,點一下Star??
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/501901.html
標籤:其他
