利用范型构建 Set

简介: 利用范型构建 Set

你可能对Set这个非常有用的数据集合类型很熟悉。一个集合是一个无序的唯一的集合。通常情况下,集合是使用哈希图实现的,它可以使数值的查询达到O(1)(假设没有哈希图的碰撞)。集合有4个主要操作,使它们特别有用:


  • Union (AB):是包含集合A和B中所有元素的集合。
  • Intersection (AB):是包含在集合A和B中的所有元素的集合。
  • Complement (Ac):是在普遍集合S中但不在A中的元素集合。
  • Difference (AB):是属于A但不属于B的元素的集合。


让我们开始在Go中定义我们的Set类型。首先,我们要定义什么是集合,通过泛型,我们可以利用约束条件来轻松地扩展集合类型,以处理许多数据类型。


package collections
// Set 用一个 map 来保存唯一的 key
type Set[T comparable] map[T]bool
// NewSet 根据传入的 size 构建一个空的 Set
func NewSet[T comparable](size int) Set[T] {
  return make(Set[T], size)
}
// Add 添加一个 key 到 set 中
func (s Set[T]) Add(key T) {
  s[key] = true
}
// Remove 在 set 中移除一个 key
func (s Set[T]) Remove(key T) {
  delete(s, key)
}
// Contains 检查某个 key 是否在 set 中
func (s Set[T]) Contains(key T) bool {
  return s[key]
}


在第一节中,我们创建了我们的Set类型,它利用了内置的map类型。我们将map的键限制为可比较类型。从文档中,我们知道可比较类型包括:


(booleans, numbers, strings, pointers, channels, arrays of comparable types, structs whose fields are all comparable types)


我们还在我们的类型上添加了一些基本方法,用于添加、删除和检查是否存在。有了这些,我们还没有准备好开始实现我们上面定义的集合操作。让我们从Difference开始:


// Difference 比较两个 set 的不同点
func (a Set[T]) Difference(b Set[T]) Set[T] {
  resultSet := NewSet[T](0)
  for key := range a {
    if !b.Contains(key) {
      resultSet.Add(key)
    }
  }
  return resultSet
}


相当简单的例子。我们简单地创建一个容量为0的新集合(因为我们不知道这个新集合会有多大),然后在集合a上进行迭代,只添加不包含在b中的元素。


接下来的两个操作UnionIntersection遵循类似的模式,但这次我们增加了一个轻微的(或潜在的大)优化。


// Union A union B
func (a Set[T]) Union(b Set[T]) Set[T] {
    small, large := smallLarge(a, b)
    
    for key := range small {
        large.Add(key)
    }
    return large
}
// Intersection A intersect B
func (a Set[T]) Intersection(b Set[T]) Set[T] {
    small, large := smallLarge(a, b)
    
    resultSet := NewSet[T](0)
    for key := range small {
        if large.Contains(key) {
            resultSet.Add(key)
        }
    }
    return resultSet
}
// smallLarge 根据其长度返回小集和大集
func smallLarge[T comparable](a, b Set[T]) (Set[T], Set[T]) {
    small, large := b, a
    if len(b) > len(a) {
        small, large = a, b
    }
    
    return small, large
}


这两种方法都相当简单。在Union中,我们只是在一个集合上进行迭代,将值添加到另一个集合中。在Intersection中,我们要检查a中的值是否也在b中,并返回一个只包含两者元素的集合。


优化来自于在smallLarge(a, b)的调用中区分哪个集合是小的。通过这样做,我们允许循环只迭代两个集合中较小的那个。如果一个集合很大而另一个很小,这有可能节省大量的迭代次数。


然而,在Union中,我们覆盖了大集合,它可能是a或b。如果我们想在Union时保留原始集合,我们必须在两个集合上循环。


我们现在有一个功能齐全的Set包。再做一点工作,我们可以为切片添加helper方法,并添加更多的实用方法,比如检查Equal的方法。


// Equals 比较两个 set 是否相等
func (a Set[T]) Equals(b Set[T]) bool {
  return len(a.Difference(b)) == 0 && len(b.Difference(a)) == 0
}
// ToSlice 将 set 转换为 slice
func (a Set[T]) ToSlice() []T {
  s := make([]T, 0)
  for key := range a {
    s = append(s, key)
  }
  return s
}
// SliceToSet
func SliceToSet[T comparable](s []T) Set[T] {
  set := NewSet[T](len(s))
  for _, item := range s {
    set.Add(item)
  }
  return set
}
// SliceUnion Union 两个 slice
func SliceUnion[T comparable](a, b []T) []T {
  aSet, bSet := SliceToSet(a), SliceToSet(b)
  union := aSet.Union(bSet)
  return union.ToSlice()
}
// SliceIntersection 两个切片的交集。提供的切片不需要是唯一的。不保证顺序。
func SliceIntersection[T comparable](a, b []T) []T {
  aSet, bSet := SliceToSet(a), SliceToSet(b)
  intersection := aSet.Intersection(bSet)
  return intersection.ToSlice()
}


通过上述所有工作,我们能够很轻松执行以下测试代码:


func TestSets(t *testing.T) {
   A := SliceToSet([]int{1, 3, 5})
   B := SliceToSet([]int{0, 1, 2, 3, 4})
  
   union := A.Union(B)
   fmt.Println(union) // map[0:true 1:true 2:true 3:true 4:true 5:true]
  
   C := SliceToSet([]string{"a", "b", "noah"})
   D := SliceToSet([]string{"a", "noah"})
   intersection := C.Intersection(D)
   fmt.Println(intersection) // map[a:true noah:true]
  
   fmt.Println(C.Equals(D)) // false
}


输出:


go test -timeout 30s -run ^TestSets$ go-test/collections -v 
=== RUN TestSets 
map[0:true 1:true 2:true 3:true 4:true 5:true] 
map[a:true noah:true] 
false 
--- PASS: TestSets (0.00s) 
PASS 
ok go-test/collections 0.006s
相关文章
|
6月前
|
存储 JavaScript Java
(Python基础)新时代语言!一起学习Python吧!(四):dict字典和set类型;切片类型、列表生成式;map和reduce迭代器;filter过滤函数、sorted排序函数;lambda函数
dict字典 Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。 我们可以通过声明JS对象一样的方式声明dict
424 1
|
9月前
|
存储 缓存 JavaScript
Set和Map有什么区别?
Set和Map有什么区别?
652 1
|
6月前
|
存储 算法 容器
set_map的实现+set/map加持秒杀高频算法题锻炼算法思维
`set`基于红黑树实现,支持有序存储、自动去重,增删查效率为O(logN)。通过仿函数可自定义排序规则,配合空间配置器灵活管理内存。不支持修改元素值,迭代器失效需注意。`multiset`允许重复元素。常用于去重、排序及查找场景。
|
10月前
|
存储 JavaScript 前端开发
for...of循环在遍历Set和Map时的注意事项有哪些?
for...of循环在遍历Set和Map时的注意事项有哪些?
708 156
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
358 2
|
10月前
|
存储 C++ 容器
unordered_set、unordered_multiset、unordered_map、unordered_multimap的介绍及使用
unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。在unordered_set中,元素的值同时也是唯一地标识它的key。在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。它的迭代器至少是前向迭代器。前向迭代器的特性。
614 0
|
10月前
|
编译器 C++ 容器
用一棵红黑树同时封装出map和set
再完成上面的代码后,我们的底层代码已经完成了,这时候已经是一个底层STL的红黑树了,已经已符合库里面的要求了,这时候我们是需要给他穿上对应的“衣服”,比如穿上set的“衣服”,那么这个穿上set的“衣服”,那么他就符合库里面set的要求了,同样map一样,这时候我们就需要实现set与map了。因此,上层容器map需要向底层红黑树提供一个仿函数,用于获取T当中的键值Key,这样一来,当底层红黑树当中需要比较两个结点的键值时,就可以通过这个仿函数来获取T当中的键值了。我们就可以使用仿函数了。
155 0
|
10月前
|
存储 编译器 容器
set、map、multiset、multimap的介绍及使用以及区别,注意事项
set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。set默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。
377 0
|
编译器 容器
哈希表模拟封装unordered_map和unordered_set
哈希表模拟封装unordered_map和unordered_set

热门文章

最新文章