1. 背景
哈希作为一种常用的数据结构,key-value 的读写都很有着很高的性能,时间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。但也存在着一些限制,比如:
- 关于哈希的遍历都是无序的,所以每次遍历的结果都不一样。
- 怎么做到随机返回哈希中的一对 key-value,要求时间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),而且是均匀的。
现在我们就针对这 2 个问题讨论下怎么设计。
2. 链表加强哈希,实现遍历有序
思路:
- 继续使用哈希数据结构存储 key-value,毕竟这是最高效的
- 如何保证有序:使用双向链表
- 添加 key-value 时,将 key-value 添加在链表的头部,当然也可能是移动到链表头部。
- 删除 key-value 时,将 key-value 从链表中删除。
- 遍历时,从链表的头部开始直到链表的尾部,因为链表的遍历是有序的,所以整个遍历结果也是有序的。
Golang
type Node[K comparable, T any] struct {
key K
value T
next *Node[K, T]
prev *Node[K, T]
}
type LinkedList[K comparable, T any] interface {
MoveToHead(node Node[K, T])
AddToHead(node Node[K, T])
DeleteNode(node Node[K, T])
Next() Node[K, T]
HasNext() bool
}
type HashSet[K comparable, T any] struct {
linkedList LinkedList[K, T]
keyNode map[K]*Node[K, T]
}
func (h *HashSet[K, T]) Get(key K) (T, error) {
var value T
node, ok := h.keyNode[key]
if !ok {
return value, errors.New("key not found")
}
return node.value, nil
}
func (h *HashSet[K, T]) Set(key K, value T) error {
node, ok := h.keyNode[key]
if ok {
node.value = value
h.linkedList.MoveToHead(*node)
return nil
}
node = &Node[K, T]{key: key, value: value, prev: nil, next: nil}
h.keyNode[key] = node
h.linkedList.AddToHead(*node)
return nil
}
func (h *HashSet[K, T]) Delete(key K) error {
node, ok := h.keyNode[key]
if !ok {
return errors.New("key not found")
}
delete(h.keyNode, key)
h.linkedList.DeleteNode(*node)
return nil
}
type procFunc[K comparable, T any] func(key K, value T)
func (h *HashSet[K, T]) iterator(proc procFunc[K, T]) {
for h.linkedList.HasNext() {
node := h.linkedList.Next()
proc(node.key, node.value)
}
}
时间复杂度并没有增加,读写依旧是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),空间复杂度比普通哈希数据结构翻倍。
3. 数组加强哈希,实现 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 复杂度随机返回一对 key-value
思路:
-
标准数组可以实现 O(1) 复杂度在随机生成一个 [0, size) 的索引。
-
基于拉链法实现的哈希,虽然本质使用了一个数组,但每一个数组都都是一个指向头节点的链表。不知道数组的长度,也不知道每一个链表的长度。必然需要先遍历才能实现返回一对随机的 key-value,时间复杂度不满足。
-
基于线性探测法,可能存在空洞,每一对 key-value 返回的概率不等。
- 向左或向右查找,找到第一个非空元素。时间复杂度不满足,不是均匀随机的,沿固定方向查找会导致某一侧元素被选中的概率增大。
- 多次随机------遇到空洞就再随机生成一个索引,这样做是均匀的,但是时间复杂度不满足。
-
基于标准哈希 + 数组实现
- 哈希的 value 是数组的索引。
- 添加一对 key-value 就在数组后面添加一个元素。
- 删除一对 key-value 需要在数据中删除对应的元素,但我们可以简化操作,将最后一个元素替换到删除的索引,然后将数据的长度剪 1。
goland
type array[T any] []T
func (a array[T]) pop() {
size := len(a)
if size == 0 {
return
}
newArray := make([]T, size-1)
copy(newArray, a[0:size-1])
a = newArray
}
type HashSet[K comparable, T any] struct {
values array[T]
indexes map[K]int
}
func (h *HashSet[K, T]) Get(key K) (T, error) {
var value T
index, ok := h.indexes[key]
if !ok {
return value, errors.New("key not found")
}
value = h.values[index]
return value, nil
}
func (h *HashSet[K, T]) Set(key K, value T) {
index, ok := h.indexes[key]
if ok {
h.values[index] = value
return
}
index = len(h.values)
h.values = append(h.values, value)
h.indexes[key] = index
}
func (h *HashSet[K, T]) Delete(key K) error {
index, ok := h.indexes[key]
if !ok {
return errors.New("key not found")
}
lastIndex := len(h.indexes) - 1
// 交换 value
h.values[index], h.values[lastIndex] = h.values[lastIndex], h.values[index]
// 删除最后一个元素。可优化,删除大量元素后,再缩容
h.values.pop()
// 删除 key
delete(h.indexes, key)
return nil
}
func (h *HashSet[K, T]) Random() T {
size := len(h.indexes)
index := rand.Intn(size)
return h.values[index]
}