线性探查法的两种代码实现

前文 哈希表核心原理 中我介绍了哈希表的核心原理和几个关键概念, 拉链法原理和实现 中介绍了拉链法的实现, 线性探查法的两个难点 介绍了线性探查法实现哈希表的难点所在,并给出了两种方法解决删除元素时的空洞问题,本文会同时给出这两种方法的参考代码实现。

本文会先结合可视化面板给出简化的实现,方便大家理解增删查改的过程,最后给完整实现。

简化实现中,具体简化的地方如下:

1、我们实现的哈希表只支持 key 类型为 intvalue 类型为 int 的情况,如果 key 不存在,就返回 -1。

2、我们实现的 hash 函数就是简单地取模,即 hash(key) = key % table.length。这样也方便模拟出哈希冲突的情况,比如当 table.length = 10 时,hash(1)hash(11) 的值都是 1。

3、底层的 table 数组的大小在创建哈希表时就固定,假设 table 数组不会被装满,不考虑负载因子和动态扩缩容的问题。

简化版代码

搬移数据的线性探查法

这种方法在 remove 操作时,会将删除的元素后面的元素重新插入哈希表,以此保持元素的连续性。

这里直接给出简化版的代码实现,你可以先看看,后面将通过可视化面板展示增删查改的过程:

import "fmt"

// 定义节点结构体
type Node struct {
	key int
	val int
}

// 定义哈希表结构体
type ExampleLinearProbingHashMap1 struct {
	table []*Node
}

// 初始化哈希表
func NewExampleLinearProbingHashMap1(cap int) *ExampleLinearProbingHashMap1 {
	table := make([]*Node, cap)

	return &ExampleLinearProbingHashMap1{
		table: table,
	}
}

// 增/改
func (e *ExampleLinearProbingHashMap1) Put(key int, value int) {
	index := e.findKeyIndex(key)
	e.table[index] = &Node{
		key: key,
		val: value,
	}
}

// 查,找不到就返回 -1
func (e *ExampleLinearProbingHashMap1) Get(key int) int {
	index := e.findKeyIndex(key)
	if e.table[index] == nil {
		return -1
	}
	return e.table[index].val
}

// 删
func (e *ExampleLinearProbingHashMap1) Remove(key int) {
	index := e.findKeyIndex(key)
	if e.table[index] == nil {
		return
	}

	e.table[index] = nil
	index = (index + 1) % len(e.table)

	for e.table[index] != nil {
		tmp := e.table[index]
		e.table[index] = nil
		// 这个操作是关键,利用 put 方法,将键值对重新插入
		// 这样就能把它们移动到正确的 table 索引位置
		e.Put(tmp.key, tmp.val)
		index = (index + 1) % len(e.table)
	}
}

// 线性探测法查找 key 在 table 中的索引
// 如果找不到,返回的就是下一个为 null 的索引,可用于插入
func (e *ExampleLinearProbingHashMap1) findKeyIndex(key int) int {
	index := e.hash(key)
	for e.table[index] != nil {
		if e.table[index].key == key {
			return index
		}
		index = (index + 1) % len(e.table)
	}
	return index
}

// 散列函数
func (e *ExampleLinearProbingHashMap1) hash(key int) int {
	return key % len(e.table)
}

func main() {
	map1 := NewExampleLinearProbingHashMap1(10)
	map1.Put(1, 1)
	map1.Put(2, 2)
	map1.Put(10, 10)
	map1.Put(20, 20)
	map1.Put(30, 30)
	map1.Put(3, 3)
	fmt.Println(map1.Get(1)) // 1
	fmt.Println(map1.Get(2)) // 2
	fmt.Println(map1.Get(20)) // 20

	map1.Put(1, 100)
	fmt.Println(map1.Get(1)) // 100

	map1.Remove(20)
	fmt.Println(map1.Get(20)) // -1
	fmt.Println(map1.Get(30)) // 30
}

特殊占位符的线性探查法

这种方法通过一个 DELETED 特殊值作为占位符,标记被删除的元素。

这个方法与上面的方法最大的区别在于 findKeyIndex 方法的实现,同时需要对 DELETED 特殊处理。直接看代码吧,后面会通过可视化面板展示增删查改的过程:

// 用线性探查法解决哈希冲突的简化实现(特殊占位符版)

struct KVNode {
    key int
    val int
}

type ExampleLinearProbingHashMap2 struct {
    // 真正存储键值对的数组
    table []*KVNode
    // 用于标记被删元素的占位符
    DELETED *KVNode
}

// 哈希函数,将键映射到 table 的索引
func (e *ExampleLinearProbingHashMap2) hash(key int) int {
    return key % len(e.table)
}

// 线性探测法查找 key 在 table 中的索引,如果找不到,返回 -1
func (e *ExampleLinearProbingHashMap2) findKeyIndex(key int) int {
    // 因为删除元素时只是标记为 DELETED,并不是真的删除,所以 table 可能会被填满,导致死循环
    // step 用来记录查找的步数,防止死循环
    for i, step := e.hash(key), 0; e.table[i] != nil; i = (i+1) % len(e.table) {
        if step++; step > len(e.table) {
            return -1
        }
        // 遇到占位符直接跳过
        if e.table[i] == e.DELETED {
            continue
        }
        if e.table[i].key == key {
            return i
        }
    }

    return -1
}

// 增/改
func (e *ExampleLinearProbingHashMap2) put(key int, val int) {
    index := e.findKeyIndex(key)
    // key 已存在,修改对应的 val,如果 key 不存在,新建节点并插入表中
    if index != -1 && e.table[index] != nil {
        e.table[index].val = val
        return
    }

    node := &KVNode{key, val}
    index = e.hash(key)
    for e.table[index] != nil && e.table[index] != e.DELETED {
        index = (index+1) % len(e.table)
    }
    e.table[index] = node
}

// 删
func (e *ExampleLinearProbingHashMap2) remove(key int) {
    index := e.findKeyIndex(key)
    // key 不存在,不需要 remove
    if index == -1 {
        return
    }
    // 直接用占位符表示删除
    e.table[index] = e.DELETED
}

// 查,返回 key 对应的 val,如果 key 不存在,则返回 -1
func (e *ExampleLinearProbingHashMap2) get(key int) int {
    index := e.findKeyIndex(key)
    if index == -1 {
        return -1
    }
    return e.table[index].val
}

func main() {
    map := &ExampleLinearProbingHashMap2{
        table: make([]*KVNode, 10),
        DELETED: &KVNode{-2, -2},
    }
    map.put(1, 1)
    map.put(2, 2)
    map.put(10, 10)
    map.put(20, 20)
    map.put(30, 30)
    map.put(3, 3)
    fmt.Println(map.get(1)) // 1
    fmt.Println(map.get(2)) // 2
    fmt.Println(map.get(20)) // 20

    map.put(1, 100)
    fmt.Println(map.get(1)) // 100

    map.remove(20)
    fmt.Println(map.get(20)) // -1
    fmt.Println(map.get(30)) // 30
}

特殊值标记版本

package main

import (
	"errors"
	"fmt"
)

// KVNode 表示键值对的节点
type KVNode struct {
	key interface{}
	val interface{}
}

// MyLinearProbingHashMap2 是线性探查哈希表的结构体
type MyLinearProbingHashMap2 struct {
	table []*KVNode
	size  int
}

// 被删除的 KVNode 的占位符
var dummy = &KVNode{nil, nil}

// 默认的初始化容量
const initCap = 4

// 构造函数,初始化容量
func NewMyLinearProbingHashMap2(cap int) *MyLinearProbingHashMap2 {
	if cap <= 0 {
		cap = initCap
	}
	return &MyLinearProbingHashMap2{
		table: make([]*KVNode, cap),
		size:  0,
	}
}

// **** 增/改 ****

// 添加 key -> val 键值对
// 如果键 key 已存在,则将值修改为 val
func (m *MyLinearProbingHashMap2) Put(key, val interface{}) error {
	if key == nil {
		return errors.New("key is null")
	}

	// 负载因子默认设为 0.75,超过则扩容
	if m.size >= len(m.table)*3/4 {
		m.resize(len(m.table) * 2)
	}

	index := m.getKeyIndex(key)
	if index != -1 {
		// key 已存在,修改对应的 val
		m.table[index].val = val
		return nil
	}

	// key 不存在
	x := &KVNode{key, val}
	// 在 table 中找一个空位或者占位符,插入
	index = m.hash(key)
	for m.table[index] != nil && m.table[index] != dummy {
		index = (index + 1) % len(m.table)
	}
	m.table[index] = x
	m.size++
	return nil
}

// **** 删 ****

// 删除 key 和对应的 val,并返回 val
// 若 key 不存在,则返回 nil
func (m *MyLinearProbingHashMap2) Remove(key interface{}) error {
	if key == nil {
		return errors.New("key is null")
	}

	// 缩容
	if m.size < len(m.table)/8 {
		m.resize(len(m.table) / 2)
	}

	index := m.getKeyIndex(key)
	if index == -1 {
		// key 不存在,不需要 remove
		return nil
	}

	// 开始 remove
	// 直接用占位符表示删除
	m.table[index] = dummy
	m.size--
	return nil
}

// **** 查 ****

// 返回 key 对应的 val
// 如果 key 不存在,则返回 nil
func (m *MyLinearProbingHashMap2) Get(key interface{}) interface{} {
	if key == nil {
		return nil
	}

	index := m.getKeyIndex(key)
	if index == -1 {
		return nil
	}

	return m.table[index].val
}

// 检查是否包含指定的 key
func (m *MyLinearProbingHashMap2) ContainsKey(key interface{}) bool {
	return m.getKeyIndex(key) != -1
}

// 返回所有键的列表
func (m *MyLinearProbingHashMap2) Keys() []interface{} {
	keys := []interface{}{}
	for _, entry := range m.table {
		if entry != nil && entry != dummy {
			keys = append(keys, entry.key)
		}
	}
	return keys
}

// 返回哈希表中键值对的数量
func (m *MyLinearProbingHashMap2) Size() int {
	return m.size
}

// 对 key 进行线性探查,返回一个索引
// 根据 table[i] 是否为 nil 判断是否找到对应的 key
func (m *MyLinearProbingHashMap2) getKeyIndex(key interface{}) int {
	step := 0
	for i := m.hash(key); m.table[i] != nil; i = (i + 1) % len(m.table) {
		step++
		// 防止死循环
		if step > len(m.table) {
			// 这里可以触发一次 resize,把标记为删除的占位符清理掉
			m.resize(len(m.table))
			return -1
		}
		entry := m.table[i]
		// 遇到占位符直接跳过
		if entry == dummy {
			continue
		}
		if entry.key == key {
			return i
		}
	}
	return -1
}

// 哈希函数,将键映射到 table 的索引
// [0, len(table) - 1]
func (m *MyLinearProbingHashMap2) hash(key interface{}) int {
	return int(fmt.Sprintf("%v", key)[0]) % len(m.table)
}

// 扩容或缩容函数
func (m *MyLinearProbingHashMap2) resize(cap int) {
	newMap := NewMyLinearProbingHashMap2(cap)
	for _, entry := range m.table {
		if entry != nil && entry != dummy {
			newMap.Put(entry.key, entry.val)
		}
	}
	m.table = newMap.table
}

func main() {
	mapInstance := NewMyLinearProbingHashMap2(4)
	mapInstance.Put(1, 1)
	mapInstance.Put(2, 2)
	mapInstance.Put(10, 10)
	mapInstance.Put(20, 20)
	mapInstance.Put(30, 30)
	mapInstance.Put(3, 3)

	fmt.Println(mapInstance.Get(1))  // 1
	fmt.Println(mapInstance.Get(2))  // 2
	fmt.Println(mapInstance.Get(20)) // 20

	mapInstance.Put(1, 100)
	fmt.Println(mapInstance.Get(1)) // 100

	mapInstance.Remove(20)
	fmt.Println(mapInstance.Get(20)) // nil
	fmt.Println(mapInstance.Get(30)) // 30
}

完整版代码

有了上面的铺垫,我们现在来看比较完善的 Java 代码实现,主要新增了以下几个功能:

1、使用了泛型,可以存储任意类型的 keyvalue

2、底层的 table 数组会根据负载因子动态扩缩容。

3、使用了 哈希表基础 中提到的 hash 函数,利用 keyhashCode() 方法和 table.length 来计算哈希值。

4、实现了 keys() 方法,可以返回哈希表中所有的 key

下面我会分别给出 rehash 版本和特殊值标记版本的实现,具体细节可以参考代码注释。

rehash 版本

package main

import (
	"fmt"
	"hash/fnv"
)

type KVNode[K comparable, V any] struct {
	key K
	val V
}

// 线性探查哈希表
type MyLinearProbingHashMap1[K comparable, V any] struct {
	table []*KVNode[K, V]
	size  int
}

// 默认的初始化容量
const INIT_CAP = 4

func NewMyLinearProbingHashMap1[K comparable, V any]() *MyLinearProbingHashMap1[K, V] {
	return NewMyLinearProbingHashMap1WithCapacity[K, V](INIT_CAP)
}

func NewMyLinearProbingHashMap1WithCapacity[K comparable, V any](initCapacity int) *MyLinearProbingHashMap1[K, V] {
	return &MyLinearProbingHashMap1[K, V]{
		table: make([]*KVNode[K, V], initCapacity),
	}
}

// **** 增/改 ****
func (m *MyLinearProbingHashMap1[K, V]) Put(key K, val V) error {
	// 负载因子默认设为 0.75,超过则扩容
	if m.size >= len(m.table)*3/4 {
		m.resize(len(m.table) * 2)
	}

	index := m.getKeyIndex(key)
	// key 已存在,修改对应的 val
	if m.table[index] != nil && m.table[index].key == key {
		m.table[index].val = val
		return nil
	}

	// key 不存在,在空位插入
	m.table[index] = &KVNode[K, V]{key, val}
	m.size++
	return nil
}

// **** 删 ****
func (m *MyLinearProbingHashMap1[K, V]) Remove(key K) error {
	// 缩容,当负载因子小于 0.125 时,缩容
	if m.size <= len(m.table)/8 {
		m.resize(len(m.table) / 2)
	}

	index := m.getKeyIndex(key)

	if m.table[index] == nil {
		// key 不存在,不需要 remove
		return nil
	}

	// 开始 remove
	m.table[index] = nil
	m.size--
	// 保持元素连续性,进行 rehash
	index = (index + 1) % len(m.table)
	for m.table[index] != nil {
		entry := m.table[index]
		m.table[index] = nil
		m.size--
		m.Put(entry.key, entry.val)
		index = (index + 1) % len(m.table)
	}
	return nil
}

// **** 查 ****
func (m *MyLinearProbingHashMap1[K, V]) Get(key K) (V, error) {
	index := m.getKeyIndex(key)
	if m.table[index] == nil {
		return *new(V), nil
	}
	return m.table[index].val, nil
}

func (m *MyLinearProbingHashMap1[K, V]) ContainsKey(key K) bool {
	index := m.getKeyIndex(key)
	return m.table[index] != nil
}

// 返回所有 key(顺序不固定)
func (m *MyLinearProbingHashMap1[K, V]) Keys() []K {
	keys := []K{}
	for _, entry := range m.table {
		if entry != nil {
			keys = append(keys, entry.key)
		}
	}
	return keys
}

// **** 其他工具函数 ****
func (m *MyLinearProbingHashMap1[K, V]) Size() int {
	return m.size
}

// 哈希函数,将键映射到 table 的索引
func (m *MyLinearProbingHashMap1[K, V]) hash(key K) int {
	h := fnv.New32a()
	h.Write([]byte(fmt.Sprintf("%v", key)))
	return int(h.Sum32()) & 0x7fffffff % len(m.table)
}

// 对 key 进行线性探查,返回一个索引
// 如果 key 不存在,返回的就是下一个为 null 的索引,可用于插入
func (m *MyLinearProbingHashMap1[K, V]) getKeyIndex(key K) int {
	index := m.hash(key)
	for m.table[index] != nil {
		if m.table[index].key == key {
			return index
		}
		index = (index + 1) % len(m.table)
	}
	return index
}

func (m *MyLinearProbingHashMap1[K, V]) resize(newCap int) {
	newMap := NewMyLinearProbingHashMap1WithCapacity[K, V](newCap)
	for _, entry := range m.table {
		if entry != nil {
			newMap.Put(entry.key, entry.val)
		}
	}
	m.table = newMap.table
}

func main() {
	mapInstance := NewMyLinearProbingHashMap1[int, int]()
	mapInstance.Put(1, 1)
	mapInstance.Put(2, 2)
	mapInstance.Put(10, 10)
	mapInstance.Put(20, 20)
	mapInstance.Put(30, 30)
	mapInstance.Put(3, 3)

	val, _ := mapInstance.Get(1)
	fmt.Println(val) // 1
	val, _ = mapInstance.Get(2)
	fmt.Println(val) // 2
	val, _ = mapInstance.Get(20)
	fmt.Println(val) // 20

	mapInstance.Put(1, 100)
	val, _ = mapInstance.Get(1)
	fmt.Println(val) // 100

	mapInstance.Remove(20)
	fmt.Println(mapInstance.ContainsKey(20)) // false
	val, _ = mapInstance.Get(30)
	fmt.Println(val) // 30
}