利用范型构建 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
相关文章
|
23天前
|
存储 JavaScript 前端开发
Set、Map、WeakSet 和 WeakMap 的区别
在 JavaScript 中,Set 和 Map 用于存储唯一值和键值对,支持多种操作方法,如添加、删除和检查元素。WeakSet 和 WeakMap 则存储弱引用的对象,有助于防止内存泄漏,适合特定场景使用。
|
2月前
|
存储 Java API
【数据结构】map&set详解
本文详细介绍了Java集合框架中的Set系列和Map系列集合。Set系列包括HashSet(哈希表实现,无序且元素唯一)、LinkedHashSet(保持插入顺序的HashSet)、TreeSet(红黑树实现,自动排序)。Map系列为双列集合,键值一一对应,键不可重复,值可重复。文章还介绍了HashMap、LinkedHashMap、TreeMap的具体实现与应用场景,并提供了面试题示例,如随机链表复制、宝石与石头、前K个高频单词等问题的解决方案。
34 6
【数据结构】map&set详解
|
25天前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
31 1
|
2月前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
32 5
|
2月前
|
存储 JavaScript 前端开发
js的map和set |21
js的map和set |21
|
2月前
|
存储 前端开发 API
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
该文章详细介绍了ES6中Set和Map数据结构的特性和使用方法,并探讨了它们在前端开发中的具体应用,包括如何利用这些数据结构来解决常见的编程问题。
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
|
3月前
|
存储 安全 Java
java集合框架复习----(4)Map、List、set
这篇文章是Java集合框架的复习总结,重点介绍了Map集合的特点和HashMap的使用,以及Collections工具类的使用示例,同时回顾了List、Set和Map集合的概念和特点,以及Collection工具类的作用。
java集合框架复习----(4)Map、List、set
|
3月前
|
Java
【Java集合类面试二十二】、Map和Set有什么区别?
该CSDN博客文章讨论了Map和Set的区别,但提供的内容摘要并未直接解释这两种集合类型的差异。通常,Map是一种键值对集合,提供通过键快速检索值的能力,而Set是一个不允许重复元素的集合。
|
3月前
|
存储 JavaScript 前端开发
ES6新特性(四): Set 和 Map
ES6新特性(四): Set 和 Map
|
3月前
|
存储 Java 索引