Goland实现Set操作
今日工程接近尾声,之前的一个set操作使用的redis里的set,但是这样很浪费,在内存中声明set就可以了,但是Goland中没有set,于是就在最后手动实现一个
使用Goland实现Set数据结构操作
一、背景说明
在其他语言中,set的底层都是利用hash Table实现的,在Goland语言中没有实现set,但拥有作为Hash Table实现的字典Map类型,比较两者之后会发现两者之间的一些特征是非常相似的,我们或许可以利用Map来实现Set。
- Map中的键值是不可重复的,Set中的元素是不可重复的
- 都只能使用迭代的方式获取其中的元素
- Map中的键值取出时不保证顺序,Set中的元素取出时也不保证任何有序性
我们可以发现Set更像是Map的一个简化版本,也就是value值只有存在或者不存在两种,到这里基本上已经清晰了,Map可以做的所有事情我们实现的Set都可以完成
二、基本定义
type HashSet struct { m map[interface{}] bool }
这个类型声明中的唯一字段就是map[interface{}] bool,之所以这样选择是有原因的,首先我们的数据结构选择的是Map,所以我们声明了一个value类型是bool的Map,其次,对于不同的key类型,我们使用空接口来实现泛型,Goland语言中没有泛型,我们使用空接口来实现,但是空接口的返回值问题我不知道怎么解决,所以在项目中就直接写了string类型,缺乏可扩展性。最后,value类型选择为bool型,因为我们只需要记录这个键值是否存在,所以选择占用空间最小的bool类型值,选用bool类型的两个预定义常量(false、true)可以简洁方便的判断键值是都存在,若使用非零常量判断,需要较复杂的代码
if v := m["a"]; v != 0 { // 如果“m“中不存在以”"a"作为键的键值对 // 省略若干语句 } // 对于map[interface{}]bool类型的值来说 if m["a"]{ // 如果“m“中不存在以”"a"作为键的键值对 // 省略若干语句 }
二、初始化
func NewHashSet() *HashSet{ return &HashSet{m: make(map[string]bool)} }
如果直接new(HashSet).m,那么Map类型的零值是nil,因此要编写一个专门用于创建和初始化HashSet类型值的函数
如上可以看到的,使用make函数对字段m进行了初始化。并且返回值是*HashSet,而非HashSet,这样做的好处在下面介绍
三、添加元素
func (set *HashSet) Add (e string) bool{ if !set.m[e]{ set.m[e] = true return true } return false }
Add前面的(set *HashSet)是作为接受者声明的,这样不仅在调用时可以直接使用HashSet.Add(好处1),并且从节约空间的角度出发,当接受者是HashSet时,每次调用都需要对当前的值进行一遍复制,虽然类型声明中只有一个字段,但是这也是一种开销,并且不能预估未来这个类型会变得多大,当接受者是*HashSet时,复制的是一个指针,指针是四个字节,基本上都会小于所指类型需要的内存空间(好处2)
四、删除元素
func (set *HashSet) Remove (e string){ delete(set.m , e) }
五、清除所有元素
func (set *HashSet) Clear(){ set.m = make(map[string]bool) }
这里的返回值若是HashSet,那么该方法中的赋值只是对当前的复制品中的字段m进行的复制而已,对original的HashSet没有进行操作(好处3)
关于垃圾回收,这样操作之后m绑定了新的字典值,之前绑定的字典值没有与任何程序实体存在关系,Goland会在之后的某一时刻进行垃圾回收,无需手动处理
六、是否包含某个元素
/方法Contains用于判断其值是否包含某个元素值。 //这里判断结果得益于元素类型为bool的字段m func (set *HashSet) Contains(e interface{}) bool { return set.m[e] }
七、获取元素值的数量
//方法Len用于获取HashSet元素值数量 func (set *HashSet) Len() int { return len(set.m) }
八、判断与其他HashSet类型值是否相同
/方法Same用来判断两个HashSet类型值是否相同 func (set *HashSet) Same(other *HashSet) bool { if other == nil { return false } if set.Len() != other.Len() { return false } for key := range set.m { if !other.Contains(key) { return false } } return true }
两个 HashSet 类型值相同的必要条件是,它们包含的元素应该是完全相同的。由于 HashSet 类型值中的元素的迭代顺序总是不确定的,所以也就不用在意两个值在这方面是否一致。如果要判断两个 HashSet 类型值是否是同一个值,就需要利用指针运算进行内存地址的比较。
九、获取所有元素值
func (set *HashSet) Elements() []string{ initialLen := len(set.m) snapshot := make([]string, initialLen) actualLen := 0 for key := range set.m{ if actualLen < initialLen { snapshot[actualLen] = key } else { snapshot = append(snapshot, key) } actualLen++ } if actualLen < initialLen{ snapshot = snapshot[:actualLen] } return snapshot }
之所以我们使用这么多条语句来实现这个方法是因为需要考虑到在从获取字段m的值的长度到对m的值迭代完成的这个时间段内,m的值中的元素数量可能发生变化。如果在迭代完成之前,m的值中的元素数量有所增加,使得实际迭代的次数大于先前初始化的快照值的长度 ,那么我们再使用appeng函数向快照值追加元素值。这样做既提高了生成的效率,又不至于在元素数量增加时引发索引越界的运行时恐慌。
对于已被初始化的[]interface{}类型的切片值来说,未被显示初始化的元素位置上的值均为nil。如果在迭代完成前,m的值中的元素数量有所减少, 致使快照值的尾部存在若干个没有任何意义的值为nil的元素,我们需要把这些无用的元素从快照中去掉。可以通过snapshot = snapshot[:actualLen]将无用的元素值从中去掉。
十、我的调用
func ListUnion() ([]string, error){ Union_Set := NewHashSet() Union_Set.Clear() res := []string{} dbList, err := redisClient.SMembers(DB_SET).Result() if err != nil{ return res, err } onlyDumpList, err := redisClient.SMembers(List_Only_Dump).Result() if err != nil{ return res, err } for _, dbName := range dbList{ Union_Set.Add(dbName) } for _, dbName := range onlyDumpList{ Union_Set.Add(dbName) } res = Union_Set.Elements() return res, nil }
---------------------------------------------------------------------------------
原文作者:https://yangchenglong11.github.io/2016/10/17/Go-%E5%AE%9E%E7%8E%B0set/