哈希集合的原理及代码实现

我讲解前面每种数据结构时,都会把原理和代码实现分到两篇文章里讲解,而这里讲哈希集合时,把原理和实现同时放在本文讲解,且本章节只有本文一篇文章,你有没有觉得奇怪?

哈哈,因为哈希集合没什么好讲的,它就是把前文讲的哈希表简单封装了一下:哈希表的键,其实就是哈希集合。

这么一句话就可以讲完了,不过我们还是稍微具体讲一下,照顾一下哈希集合的面子。

哈希集合原理

哈希集合的主要使用场景是「去重」,因为它的特性是:不会出现重复元素,可以在 O(1) 的时间增删元素,可以在 O(1) 的时间判断一个元素是否存在。

哈希集合的主要 API 如下:

type HashSet struct {}

// 增,向哈希集合中添加一个元素,复杂度 O(1)
// 如果元素已存在,则什么都不会发生
func (hs *HashSet) Add(key int) {}

// 删,从哈希集合中移除一个元素,复杂度 O(1)
// 如果元素不存在,则什么都不会发生
func (hs *HashSet) Remove(key int) {}

// 查,判断元素是否存在,复杂度 O(1)
func (hs *HashSet) Contains(key int) bool {}

这几个 API 咋实现?其实根本用不着实现,直接把前文教给你的哈希表拿来用就行了。

你往哈希表里插入一个键值对 put(key, value),时间复杂度是 O(1);你往哈希表里删除一个键值对 remove(key),时间复杂度是 O(1);判断一个键是否存在哈希表中,就是判断 get(key) 是否得到空指针 null,所以时间复杂度也是 O(1)。

综上,这几个哈希集合的 API 可以直接复用哈希表的 API 来实现。操作哈希集合,其实就是操作哈希表的键

那就有读者问了,哈希表里面的值呢,如何处理?答案是不处理,直接忽略掉就行了,我们可以用一个占位符来填充值,等会看代码实现你就懂了。

虽然只是简单封装了哈希表,但是大部分高级编程语言还是提供哈希集合的数据结构的,比如 Python 的 set,Java 的 HashSet,C++ 的 unordered_set 等等。但是像 Go 语言,标准库干脆就不提供哈希集合这种数据结构,你要用的话需要自己用哈希表 map 来模拟。

因为哈希集合的元素就是哈希表里面的键,所以**哈希集合和哈希表具有相同的限制**,比如不能依赖哈希集合的元素遍历顺序、哈希集合中的元素应该是不可变的等等,具体限制和原因可以回顾前文
[哈希表特性及原理](./11-哈希表核心原理.md)。

哈希集合代码实现

有了上面的铺垫,代码实现应该没有任何难度了。因为太简单了,我就只给出一个 Java 实现,其他语言可以自行使用标准库提供的哈希表,或者我们前文动手实现的哈希表来封装哈希集合:

package main

type MyHashSet[K comparable] struct {
	// 底层 map,用于存储集合元素,值用空结构体作为占位符
	data map[K]struct{}
}

func NewMyHashSet[K comparable]() *MyHashSet[K] {
	return &MyHashSet[K]{
		data: make(map[K]struct{}),
	}
}

func (s *MyHashSet[K]) Add(key K) {
	// 向哈希表添加一个键值对,值用空结构体作为占位符
	s.data[key] = struct{}{}
}


func (s *MyHashSet[K]) Remove(key K) {
	// 从哈希表中移除键 key
	delete(s.data, key)
}

func (s *MyHashSet[K]) Contains(key K) bool {
	// 判断哈希表中是否包含键 key
	_, exists := s.data[key]
	return exists
}

func (s *MyHashSet[K]) Size() int {
	return len(s.data)
}