3-6 开放哈希表(闭地址法)
开放哈希表(闭地址法 / Separate Chaining)
开放哈希表(Open Hashing),又称闭地址法(Closed Addressing)或分离链接法(Separate Chaining),是一种处理哈希冲突(Hash Collision)的经典方法。其核心思想是:哈希表(Hash Table)的每个桶(Bucket)是一个链表(Linked List)的头指针。当多个键(Key)被哈希函数映射到同一个桶时,它们以链表的形式串联在该桶中,而不是去寻找其他桶。因此被称为"闭地址"——每个元素始终留在其哈希值对应的桶里。
核心概念
- 哈希函数(Hash Function):将键映射到数组下标的函数,例如
hash(key) = key % TABLE_SIZE。 - 哈希冲突(Collision):两个不同的键经过哈希函数计算后得到相同的下标。冲突是不可避免的,因为键空间远大于桶的数量。
- 装载因子(Load Factor):记作
α = n / m,其中n是已存储的元素数量,m是桶的数量。装载因子越大,冲突越频繁,链表越长,查找越慢。通常当α超过某个阈值(如 0.75)时需要进行扩容(Rehashing)。
下面是一个 TABLE_SIZE = 10 的开放哈希表示意图:
Index Chain
[0] -> NULL
[1] -> (1,10) -> (11,110) -> NULL
[2] -> (2,20) -> (12,120) -> (22,220) -> NULL
[3] -> NULL
[4] -> NULL
[5] -> NULL
[6] -> NULL
[7] -> NULL
[8] -> NULL
[9] -> NULL
在这个例子中,键 1、11 都被哈希到桶 1,键 2、12、22 都被哈希到桶 2,它们通过链表串联在各自的桶中。
哈希函数
哈希函数(Hash Function)是哈希表的核心,它决定了键被映射到哪个桶。最简单的哈希函数是取模运算(Modulo Operation):
hash(key) = key % TABLE_SIZE
以 TABLE_SIZE = 10 为例:
| key | hash(key) | 桶下标 |
|---|---|---|
| 1 | 1 % 10 = 1 | 1 |
| 2 | 2 % 10 = 2 | 2 |
| 11 | 11 % 10 = 1 | 1 |
| 12 | 12 % 10 = 2 | 2 |
| 22 | 22 % 10 = 2 | 2 |
可以看到,键 1 和 11 都映射到桶 1,键 2、12、22 都映射到桶 2——这就是哈希冲突。在闭地址法中,冲突的元素被链接在同一个桶的链表中。
取模运算的 TABLE_SIZE 通常选择质数(Prime Number),这样可以减少因键分布不均匀导致的冲突。为了教学简洁,本文使用 TABLE_SIZE = 10。
节点与哈希表定义
开放哈希表的每个桶指向一个链表,因此首先需要定义链表节点(Node)。每个节点存储键(Key)、值(Value)和指向下一个节点的指针。哈希表本身是一个指针数组,数组的每个元素指向对应桶的链表头。
// C++ Node and HashTable definitions
const int TABLE_SIZE = 10;
struct Node {
int key;
int value;
Node* next;
// Constructor: create a node with given key-value pair
Node(int k, int v) : key(k), value(v), next(nullptr) {}
};
class HashTable {
private:
Node** table; // Array of pointers to linked list heads
int size; // Number of buckets
public:
HashTable(int s = TABLE_SIZE);
~HashTable();
void insert(int key, int value);
int* search(int key);
void remove(int key);
void display();
};
// C Node and HashTable definitions
#define TABLE_SIZE 10
typedef struct Node {
int key;
int value;
struct Node* next;
} Node;
typedef struct HashTable {
Node** table; // Array of pointers to linked list heads
int size; // Number of buckets
} HashTable;
# Python Node and HashTable definitions
TABLE_SIZE = 10
class Node:
"""Linked list node for separate chaining."""
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None
class HashTable:
"""Hash table using separate chaining."""
def __init__(self, size=TABLE_SIZE):
self.size = size
self.table = [None] * size # Array of linked list heads
// Go Node and HashTable definitions
package main
const TABLE_SIZE = 10
// Node 链表节点,存储键值对和指向下一个节点的指针
type Node struct {
key int
value int
next *Node
}
// HashTable 开放哈希表,使用分离链接法处理冲突
type HashTable struct {
size int
table []*Node // 桶数组,每个元素指向链表头
}
C++ 使用 Node** table 表示一个指针数组——table[i] 是第 i 个桶的链表头指针。C 语言用 typedef 定义结构体,并提供手动内存管理。Python 用列表存储每个桶的链表头,初始值为 None。Go 使用 []*Node 切片存储桶数组,每个元素是指向链表头节点的指针,初始值为 nil。
创建哈希表
创建哈希表需要分配桶数组,并将每个桶初始化为空(NULL),表示所有链表当前为空。
// C++ Constructor: allocate and initialize the bucket array
HashTable::HashTable(int s) : size(s) {
table = new Node*[size];
for (int i = 0; i < size; i++) {
table[i] = nullptr; // Each bucket starts empty
}
}
// C++ Destructor: free all nodes and the bucket array
HashTable::~HashTable() {
for (int i = 0; i < size; i++) {
Node* current = table[i];
while (current != nullptr) {
Node* temp = current;
current = current->next;
delete temp;
}
}
delete[] table;
}
// C: create and destroy hash table
HashTable* createHashTable(int size) {
HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
if (!ht) { exit(1); }
ht->size = size;
ht->table = (Node**)calloc(size, sizeof(Node*)); // calloc initializes to NULL
if (!ht->table) { exit(1); }
return ht;
}
void destroyHashTable(HashTable* ht) {
for (int i = 0; i < ht->size; i++) {
Node* current = ht->table[i];
while (current != NULL) {
Node* temp = current;
current = current->next;
free(temp);
}
}
free(ht->table);
free(ht);
}
# Python HashTable __init__ already creates the empty table
# No explicit destructor needed (garbage collected)
// Go: create and initialize hash table
package main
import "fmt"
// newHashTable 创建并初始化哈希表
func newHashTable(size int) *HashTable {
return &HashTable{
size: size,
table: make([]*Node, size), // make 初始化切片,所有元素为 nil
}
}
func main() {
ht := newHashTable(TABLE_SIZE)
fmt.Printf("Created hash table with %d buckets\n", ht.size)
}
C++ 的构造函数使用 new Node*[size] 分配指针数组,析构函数逐桶释放链表节点再释放数组。C 语言使用 calloc 分配并自动初始化为零(NULL)。Python 在 __init__ 中直接用 [None] * size 创建空列表,无需手动释放内存。Go 使用 make([]*Node, size) 创建切片,所有元素自动初始化为 nil,无需手动释放内存(垃圾回收自动处理)。
插入操作
插入(Insertion)的步骤如下:
- 用哈希函数计算键对应的桶下标
index = key % size。 - 遍历该桶的链表,检查键是否已存在。
- 如果键已存在,更新其值。
- 如果键不存在,创建新节点并插入到链表头部(头部插入效率最高,时间复杂度 O(1))。
我们以插入 {1:10, 2:20, 12:120, 22:220} 为例,TABLE_SIZE = 10:
hash(1) = 1:桶1为空,插入(1,10)。hash(2) = 2:桶2为空,插入(2,20)。hash(12) = 2:桶2已有(2,20),键12不存在,插入(12,120)到链表头部。hash(22) = 2:桶2已有(12,120) -> (2,20),键22不存在,插入(22,220)到链表头部。
插入后桶 2 的链表为:(22,220) -> (12,120) -> (2,20)。
// C++ Insert: add or update a key-value pair
void HashTable::insert(int key, int value) {
int index = key % size;
// Traverse the chain to check if key already exists
Node* current = table[index];
while (current != nullptr) {
if (current->key == key) {
current->value = value; // Update existing key
return;
}
current = current->next;
}
// Key not found, insert new node at the head of the chain
Node* newNode = new Node(key, value);
newNode->next = table[index];
table[index] = newNode;
}
int main() {
HashTable ht;
ht.insert(1, 10);
ht.insert(2, 20);
ht.insert(12, 120);
ht.insert(22, 220);
ht.display();
return 0;
}
// C Insert: add or update a key-value pair
void insert(HashTable* ht, int key, int value) {
int index = key % ht->size;
// Traverse the chain to check if key already exists
Node* current = ht->table[index];
while (current != NULL) {
if (current->key == key) {
current->value = value; // Update existing key
return;
}
current = current->next;
}
// Key not found, insert new node at the head of the chain
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode) { exit(1); }
newNode->key = key;
newNode->value = value;
newNode->next = ht->table[index];
ht->table[index] = newNode;
}
int main() {
HashTable* ht = createHashTable(TABLE_SIZE);
insert(ht, 1, 10);
insert(ht, 2, 20);
insert(ht, 12, 120);
insert(ht, 22, 220);
display(ht);
destroyHashTable(ht);
return 0;
}
# Python Insert: add or update a key-value pair
def insert(self, key, value):
index = key % self.size
# Traverse the chain to check if key already exists
current = self.table[index]
while current is not None:
if current.key == key:
current.value = value # Update existing key
return
current = current.next
# Key not found, insert new node at the head of the chain
new_node = Node(key, value)
new_node.next = self.table[index]
self.table[index] = new_node
if __name__ == "__main__":
ht = HashTable()
ht.insert(1, 10)
ht.insert(2, 20)
ht.insert(12, 120)
ht.insert(22, 220)
ht.display()
插入操作首先计算桶下标,然后遍历该桶的链表。如果找到相同的键就更新值,否则在链表头部插入新节点。选择头部插入而非尾部插入是因为头部插入不需要遍历到链表末尾,始终是 O(1) 操作。
运行该程序将输出:
Bucket 0: empty
Bucket 1: (1,10)
Bucket 2: (22,220) -> (12,120) -> (2,20)
Bucket 3: empty
Bucket 4: empty
Bucket 5: empty
Bucket 6: empty
Bucket 7: empty
Bucket 8: empty
Bucket 9: empty
// Go Insert: add or update a key-value pair
package main
import "fmt"
func (ht *HashTable) insert(key, value int) {
idx := key % ht.size
// 遍历链表检查键是否已存在
for curr := ht.table[idx]; curr != nil; curr = curr.next {
if curr.key == key {
curr.value = value // 更新已有键
return
}
}
// 键不存在,在链表头部插入新节点
newNode := &Node{key: key, value: value, next: ht.table[idx]}
ht.table[idx] = newNode
}
func main() {
ht := newHashTable(TABLE_SIZE)
ht.insert(1, 10)
ht.insert(2, 20)
ht.insert(12, 120)
ht.insert(22, 220)
ht.display()
}
插入操作首先计算桶下标,然后遍历该桶的链表。如果找到相同的键就更新值,否则在链表头部插入新节点。选择头部插入而非尾部插入是因为头部插入不需要遍历到链表末尾,始终是 O(1) 操作。
搜索操作
搜索(Search)的步骤:
- 计算键的桶下标
index = key % size。 - 遍历该桶的链表,逐个比较键。
- 找到则返回值的指针(或引用),未找到则返回空(NULL / None)。
// C++ Search: find value by key, return pointer to value or nullptr
int* HashTable::search(int key) {
int index = key % size;
Node* current = table[index];
while (current != nullptr) {
if (current->key == key) {
return &(current->value); // Found, return pointer to value
}
current = current->next;
}
return nullptr; // Not found
}
// C Search: find value by key, return pointer to value or NULL
int* search(HashTable* ht, int key) {
int index = key % ht->size;
Node* current = ht->table[index];
while (current != NULL) {
if (current->key == key) {
return &(current->value);
}
current = current->next;
}
return NULL;
}
# Python Search: find value by key, return value or None
def search(self, key):
index = key % self.size
current = self.table[index]
while current is not None:
if current.key == key:
return current.value # Found
current = current.next
return None # Not found
// Go Search: find value by key, return (value, true) or (0, false)
package main
import "fmt"
func (ht *HashTable) search(key int) (int, bool) {
idx := key % ht.size
for curr := ht.table[idx]; curr != nil; curr = curr.next {
if curr.key == key {
return curr.value, true // Found
}
}
return 0, false // Not found
}
func main() {
ht := newHashTable(TABLE_SIZE)
ht.insert(1, 10)
ht.insert(2, 20)
ht.insert(12, 120)
targets := []int{12, 7}
for _, t := range targets {
if val, ok := ht.search(t); ok {
fmt.Printf("Found key %d -> value %d\n", t, val)
} else {
fmt.Printf("Key %d not found\n", t)
}
}
}
搜索只在与键对应的桶的链表中查找,不需要遍历整个哈希表。在没有冲突的理想情况下,每个桶最多只有一个节点,搜索时间为 O(1)。最坏情况下(所有键都映射到同一个桶),搜索退化为链表遍历,时间复杂度为 O(n)。Go 使用多返回值 (int, bool) 替代 C++ 的指针返回和 Python 的 None,使得搜索接口更加清晰。
删除操作
删除(Deletion)的步骤:
- 计算键的桶下标
index = key % size。 - 遍历该桶的链表,同时记录前驱节点(Previous Node)。
- 找到目标节点后,将前驱节点的
next指针指向目标节点的下一个节点,然后释放目标节点。 - 如果目标节点是链表头节点,则直接更新桶指针。
// C++ Remove: delete a key-value pair from the hash table
void HashTable::remove(int key) {
int index = key % size;
Node* current = table[index];
Node* prev = nullptr;
while (current != nullptr) {
if (current->key == key) {
// Found the node to delete
if (prev == nullptr) {
// Node is the head of the chain
table[index] = current->next;
} else {
// Node is in the middle or end of the chain
prev->next = current->next;
}
delete current;
return;
}
prev = current;
current = current->next;
}
}
// C Remove: delete a key-value pair from the hash table
void removeKey(HashTable* ht, int key) {
int index = key % ht->size;
Node* current = ht->table[index];
Node* prev = NULL;
while (current != NULL) {
if (current->key == key) {
if (prev == NULL) {
ht->table[index] = current->next;
} else {
prev->next = current->next;
}
free(current);
return;
}
prev = current;
current = current->next;
}
}
# Python Remove: delete a key-value pair from the hash table
def remove(self, key):
index = key % self.size
current = self.table[index]
prev = None
while current is not None:
if current.key == key:
if prev is None:
# Node is the head of the chain
self.table[index] = current.next
else:
# Node is in the middle or end of the chain
prev.next = current.next
return
prev = current
current = current.next
// Go Remove: delete a key-value pair from the hash table
package main
import "fmt"
func (ht *HashTable) remove(key int) bool {
idx := key % ht.size
var prev *Node
curr := ht.table[idx]
for curr != nil {
if curr.key == key {
if prev == nil {
// 被删除的是链表头节点
ht.table[idx] = curr.next
} else {
prev.next = curr.next
}
return true // 删除成功
}
prev = curr
curr = curr.next
}
return false // 未找到
}
func main() {
ht := newHashTable(TABLE_SIZE)
ht.insert(1, 10)
ht.insert(2, 20)
ht.insert(12, 120)
ht.insert(22, 220)
ht.display()
fmt.Println("---")
if ht.remove(12) {
fmt.Println("Deleted key 12")
} else {
fmt.Println("Key 12 not found")
}
if ht.remove(7) {
fmt.Println("Deleted key 7")
} else {
fmt.Println("Key 7 not found")
}
ht.display()
}
删除操作的关键在于维护前驱指针 prev。如果被删除的是链表头节点(prev 为 NULL / nullptr / None / nil),需要更新桶指针指向下一个节点;否则将前驱节点的 next 跳过被删除节点即可。Go 不需要手动释放内存(垃圾回收自动处理),因此删除操作只需调整指针即可。
显示哈希表
显示(Display)操作遍历所有桶,打印每个桶的链表内容,方便观察哈希表的内部结构和冲突分布情况。
// C++ Display: print all buckets and their chains
void HashTable::display() {
for (int i = 0; i < size; i++) {
cout << "Bucket " << i << ": ";
if (table[i] == nullptr) {
cout << "empty";
} else {
Node* current = table[i];
while (current != nullptr) {
cout << "(" << current->key << "," << current->value << ")";
if (current->next != nullptr) {
cout << " -> ";
}
current = current->next;
}
}
cout << endl;
}
}
// C Display: print all buckets and their chains
void display(HashTable* ht) {
for (int i = 0; i < ht->size; i++) {
printf("Bucket %d: ", i);
if (ht->table[i] == NULL) {
printf("empty");
} else {
Node* current = ht->table[i];
while (current != NULL) {
printf("(%d,%d)", current->key, current->value);
if (current->next != NULL) {
printf(" -> ");
}
current = current->next;
}
}
printf("\n");
}
}
# Python Display: print all buckets and their chains
def display(self):
for i in range(self.size):
print(f"Bucket {i}: ", end="")
if self.table[i] is None:
print("empty")
else:
current = self.table[i]
while current is not None:
print(f"({current.key},{current.value})", end="")
if current.next is not None:
print(" -> ", end="")
current = current.next
print()
// Go Display: print all buckets and their chains
package main
import "fmt"
func (ht *HashTable) display() {
for i := 0; i < ht.size; i++ {
fmt.Printf("Bucket %d: ", i)
if ht.table[i] == nil {
fmt.Print("empty")
} else {
for curr := ht.table[i]; curr != nil; curr = curr.next {
fmt.Printf("(%d,%d)", curr.key, curr.value)
if curr.next != nil {
fmt.Print(" -> ")
}
}
}
fmt.Println()
}
}
func main() {
ht := newHashTable(TABLE_SIZE)
ht.insert(1, 10)
ht.insert(2, 20)
ht.insert(12, 120)
ht.insert(22, 220)
ht.display()
}
显示操作从桶 0 遍历到桶 size - 1,对每个桶沿链表依次输出 (key,value) 对,用 -> 连接。空桶输出 empty。通过显示操作可以直观地观察哈希冲突的分布——哪些桶有长链,哪些桶为空。
完整实现
下面提供完整的开放哈希表实现,包含插入、搜索、删除和显示操作,整合为可独立运行的程序。
#include <iostream>
using namespace std;
const int TABLE_SIZE = 10;
struct Node {
int key;
int value;
Node* next;
Node(int k, int v) : key(k), value(v), next(nullptr) {}
};
class HashTable {
private:
Node** table;
int size;
int hashFunction(int key) {
return key % size;
}
public:
HashTable(int s = TABLE_SIZE) : size(s) {
table = new Node*[size];
for (int i = 0; i < size; i++) {
table[i] = nullptr;
}
}
~HashTable() {
for (int i = 0; i < size; i++) {
Node* current = table[i];
while (current != nullptr) {
Node* temp = current;
current = current->next;
delete temp;
}
}
delete[] table;
}
// Insert or update a key-value pair
void insert(int key, int value) {
int index = hashFunction(key);
Node* current = table[index];
while (current != nullptr) {
if (current->key == key) {
current->value = value;
return;
}
current = current->next;
}
Node* newNode = new Node(key, value);
newNode->next = table[index];
table[index] = newNode;
}
// Search for a key, return pointer to value or nullptr
int* search(int key) {
int index = hashFunction(key);
Node* current = table[index];
while (current != nullptr) {
if (current->key == key) {
return &(current->value);
}
current = current->next;
}
return nullptr;
}
// Remove a key-value pair
void remove(int key) {
int index = hashFunction(key);
Node* current = table[index];
Node* prev = nullptr;
while (current != nullptr) {
if (current->key == key) {
if (prev == nullptr) {
table[index] = current->next;
} else {
prev->next = current->next;
}
delete current;
cout << "Deleted key " << key << endl;
return;
}
prev = current;
current = current->next;
}
cout << "Key " << key << " not found" << endl;
}
// Display all buckets and their chains
void display() {
for (int i = 0; i < size; i++) {
cout << "Bucket " << i << ": ";
if (table[i] == nullptr) {
cout << "empty";
} else {
Node* current = table[i];
while (current != nullptr) {
cout << "(" << current->key << "," << current->value << ")";
if (current->next != nullptr) {
cout << " -> ";
}
current = current->next;
}
}
cout << endl;
}
}
};
int main() {
HashTable ht;
// Insert key-value pairs
cout << "=== Inserting ===" << endl;
int keys[] = {1, 2, 12, 22, 5, 15};
int values[] = {10, 20, 120, 220, 50, 150};
for (int i = 0; i < 6; i++) {
ht.insert(keys[i], values[i]);
cout << "Inserted (" << keys[i] << "," << values[i] << ")" << endl;
}
// Display the hash table
cout << "\n=== Hash Table ===" << endl;
ht.display();
// Search for keys
cout << "\n=== Search ===" << endl;
int targets[] = {12, 7, 22};
for (int i = 0; i < 3; i++) {
int* result = ht.search(targets[i]);
if (result != nullptr) {
cout << "Found key " << targets[i] << " -> value " << *result << endl;
} else {
cout << "Key " << targets[i] << " not found" << endl;
}
}
// Delete keys
cout << "\n=== Delete ===" << endl;
ht.remove(12);
ht.remove(7); // Key not exists
// Display after deletion
cout << "\n=== After Deletion ===" << endl;
ht.display();
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
typedef struct Node {
int key;
int value;
struct Node* next;
} Node;
typedef struct HashTable {
Node** table;
int size;
} HashTable;
int hashFunction(int key, int size) {
return key % size;
}
HashTable* createHashTable(int size) {
HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
if (!ht) { exit(1); }
ht->size = size;
ht->table = (Node**)calloc(size, sizeof(Node*));
if (!ht->table) { exit(1); }
return ht;
}
void destroyHashTable(HashTable* ht) {
for (int i = 0; i < ht->size; i++) {
Node* current = ht->table[i];
while (current != NULL) {
Node* temp = current;
current = current->next;
free(temp);
}
}
free(ht->table);
free(ht);
}
void insert(HashTable* ht, int key, int value) {
int index = hashFunction(key, ht->size);
Node* current = ht->table[index];
while (current != NULL) {
if (current->key == key) {
current->value = value;
return;
}
current = current->next;
}
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode) { exit(1); }
newNode->key = key;
newNode->value = value;
newNode->next = ht->table[index];
ht->table[index] = newNode;
}
int* search(HashTable* ht, int key) {
int index = hashFunction(key, ht->size);
Node* current = ht->table[index];
while (current != NULL) {
if (current->key == key) {
return &(current->value);
}
current = current->next;
}
return NULL;
}
void removeKey(HashTable* ht, int key) {
int index = hashFunction(key, ht->size);
Node* current = ht->table[index];
Node* prev = NULL;
while (current != NULL) {
if (current->key == key) {
if (prev == NULL) {
ht->table[index] = current->next;
} else {
prev->next = current->next;
}
free(current);
printf("Deleted key %d\n", key);
return;
}
prev = current;
current = current->next;
}
printf("Key %d not found\n", key);
}
void display(HashTable* ht) {
for (int i = 0; i < ht->size; i++) {
printf("Bucket %d: ", i);
if (ht->table[i] == NULL) {
printf("empty");
} else {
Node* current = ht->table[i];
while (current != NULL) {
printf("(%d,%d)", current->key, current->value);
if (current->next != NULL) {
printf(" -> ");
}
current = current->next;
}
}
printf("\n");
}
}
int main() {
HashTable* ht = createHashTable(TABLE_SIZE);
printf("=== Inserting ===\n");
int keys[] = {1, 2, 12, 22, 5, 15};
int values[] = {10, 20, 120, 220, 50, 150};
for (int i = 0; i < 6; i++) {
insert(ht, keys[i], values[i]);
printf("Inserted (%d,%d)\n", keys[i], values[i]);
}
printf("\n=== Hash Table ===\n");
display(ht);
printf("\n=== Search ===\n");
int targets[] = {12, 7, 22};
for (int i = 0; i < 3; i++) {
int* result = search(ht, targets[i]);
if (result != NULL) {
printf("Found key %d -> value %d\n", targets[i], *result);
} else {
printf("Key %d not found\n", targets[i]);
}
}
printf("\n=== Delete ===\n");
removeKey(ht, 12);
removeKey(ht, 7);
printf("\n=== After Deletion ===\n");
display(ht);
destroyHashTable(ht);
return 0;
}
TABLE_SIZE = 10
class Node:
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None
class HashTable:
def __init__(self, size=TABLE_SIZE):
self.size = size
self.table = [None] * size
def _hash(self, key):
return key % self.size
def insert(self, key, value):
index = self._hash(key)
current = self.table[index]
while current is not None:
if current.key == key:
current.value = value
return
current = current.next
new_node = Node(key, value)
new_node.next = self.table[index]
self.table[index] = new_node
def search(self, key):
index = self._hash(key)
current = self.table[index]
while current is not None:
if current.key == key:
return current.value
current = current.next
return None
def remove(self, key):
index = self._hash(key)
current = self.table[index]
prev = None
while current is not None:
if current.key == key:
if prev is None:
self.table[index] = current.next
else:
prev.next = current.next
print(f"Deleted key {key}")
return
prev = current
current = current.next
print(f"Key {key} not found")
def display(self):
for i in range(self.size):
print(f"Bucket {i}: ", end="")
if self.table[i] is None:
print("empty")
else:
current = self.table[i]
while current is not None:
print(f"({current.key},{current.value})", end="")
if current.next is not None:
print(" -> ", end="")
current = current.next
print()
if __name__ == "__main__":
ht = HashTable()
print("=== Inserting ===")
pairs = [(1, 10), (2, 20), (12, 120), (22, 220), (5, 50), (15, 150)]
for k, v in pairs:
ht.insert(k, v)
print(f"Inserted ({k},{v})")
print("\n=== Hash Table ===")
ht.display()
print("\n=== Search ===")
for t in [12, 7, 22]:
result = ht.search(t)
if result is not None:
print(f"Found key {t} -> value {result}")
else:
print(f"Key {t} not found")
print("\n=== Delete ===")
ht.remove(12)
ht.remove(7)
print("\n=== After Deletion ===")
ht.display()
Go 语言使用结构体(struct)表示链表节点,用切片(slice)存储桶的头指针。与 C/C++ 类似,Go 需要显式管理指针,但拥有自动垃圾回收(Garbage Collection),无需手动释放内存。
package main
import "fmt"
const TABLE_SIZE = 10
// Node 链表节点,存储键值对和指向下一个节点的指针
type Node struct {
key int
value int
next *Node
}
// HashTable 开放哈希表,使用分离链接法处理冲突
type HashTable struct {
size int
table []*Node // 桶数组,每个元素指向链表头
}
func newHashTable(size int) *HashTable {
return &HashTable{
size: size,
table: make([]*Node, size),
}
}
func (ht *HashTable) hash(key int) int {
return key % ht.size
}
// Insert 插入或更新键值对,如果键已存在则更新值,否则插入到链表头部
func (ht *HashTable) insert(key, value int) {
idx := ht.hash(key)
// 遍历链表检查键是否已存在
for curr := ht.table[idx]; curr != nil; curr = curr.next {
if curr.key == key {
curr.value = value // 更新已有键
return
}
}
// 键不存在,在链表头部插入新节点
newNode := &Node{key: key, value: value, next: ht.table[idx]}
ht.table[idx] = newNode
}
// Search 查找键,返回值和是否找到的标志
func (ht *HashTable) search(key int) (int, bool) {
idx := ht.hash(key)
for curr := ht.table[idx]; curr != nil; curr = curr.next {
if curr.key == key {
return curr.value, true
}
}
return 0, false
}
// Remove 删除指定键的键值对
func (ht *HashTable) remove(key int) {
idx := ht.hash(key)
var prev *Node
curr := ht.table[idx]
for curr != nil {
if curr.key == key {
if prev == nil {
// 被删除的是链表头节点
ht.table[idx] = curr.next
} else {
prev.next = curr.next
}
fmt.Printf("Deleted key %d\n", key)
return
}
prev = curr
curr = curr.next
}
fmt.Printf("Key %d not found\n", key)
}
// Display 打印所有桶及其链表内容
func (ht *HashTable) display() {
for i := 0; i < ht.size; i++ {
fmt.Printf("Bucket %d: ", i)
if ht.table[i] == nil {
fmt.Print("empty")
} else {
for curr := ht.table[i]; curr != nil; curr = curr.next {
fmt.Printf("(%d,%d)", curr.key, curr.value)
if curr.next != nil {
fmt.Print(" -> ")
}
}
}
fmt.Println()
}
}
func main() {
ht := newHashTable(TABLE_SIZE)
// 插入键值对
fmt.Println("=== Inserting ===")
pairs := [][2]int{{1, 10}, {2, 20}, {12, 120}, {22, 220}, {5, 50}, {15, 150}}
for _, p := range pairs {
ht.insert(p[0], p[1])
fmt.Printf("Inserted (%d,%d)\n", p[0], p[1])
}
// 显示哈希表
fmt.Println("\n=== Hash Table ===")
ht.display()
// 搜索键
fmt.Println("\n=== Search ===")
targets := []int{12, 7, 22}
for _, t := range targets {
if val, ok := ht.search(t); ok {
fmt.Printf("Found key %d -> value %d\n", t, val)
} else {
fmt.Printf("Key %d not found\n", t)
}
}
// 删除键
fmt.Println("\n=== Delete ===")
ht.remove(12)
ht.remove(7)
// 删除后显示
fmt.Println("\n=== After Deletion ===")
ht.display()
}
Go 使用 *Node 指针模拟链表的 next 引用,[]*Node 切片存储桶数组。与 Python 不同,Go 需要显式的指针操作(prev.next = curr.next),但不需要手动释放内存(垃圾回收自动处理)。Go 的多返回值 (int, bool) 替代了 C++ 的指针返回和 Python 的 None,使得搜索接口更加清晰。
运行该程序将输出:
=== Inserting ===
Inserted (1,10)
Inserted (2,20)
Inserted (12,120)
Inserted (22,220)
Inserted (5,50)
Inserted (15,150)
=== Hash Table ===
Bucket 0: empty
Bucket 1: (1,10)
Bucket 2: (22,220) -> (12,120) -> (2,20)
Bucket 3: empty
Bucket 4: empty
Bucket 5: (15,150) -> (5,50)
Bucket 6: empty
Bucket 7: empty
Bucket 8: empty
Bucket 9: empty
=== Search ===
Found key 12 -> value 120
Key 7 not found
Found key 22 -> value 220
=== Delete ===
Deleted key 12
Key 7 not found
=== After Deletion ===
Bucket 0: empty
Bucket 1: (1,10)
Bucket 2: (22,220) -> (2,20)
Bucket 3: empty
Bucket 4: empty
Bucket 5: (15,150) -> (5,50)
Bucket 6: empty
Bucket 7: empty
Bucket 8: empty
Bucket 9: empty
可以看到:
- 插入
{1:10, 2:20, 12:120, 22:220, 5:50, 15:150}后,桶2有三条记录(22、12、2都哈希到桶2),桶5有两条记录(15、5都哈希到桶5)。 - 搜索
12在桶2的链表中找到,搜索7在桶7中未找到,搜索22在桶2的链表中找到。 - 删除
12后,桶2的链表从(22,220) -> (12,120) -> (2,20)变为(22,220) -> (2,20)。
性能分析
下表总结了开放哈希表(闭地址法)各操作的时间复杂度:
| 操作 | 平均情况(Average) | 最坏情况(Worst) |
|---|---|---|
| 插入(Insert) | O(1) | O(n) |
| 搜索(Search) | O(1 + α) | O(n) |
| 删除(Delete) | O(1 + α) | O(n) |
| 显示(Display) | O(n + m) | O(n + m) |
其中 α 是装载因子(Load Factor),α = n / m,n 为元素总数,m 为桶的数量。
关键说明:
- 平均情况 O(1):当哈希函数将元素均匀分布到各个桶时,每个桶的链表长度约为
α。如果装载因子保持较小(例如α < 1),则每次操作只需常数时间加上遍历很短的链表。 - 最坏情况 O(n):当所有键都哈希到同一个桶时,哈希表退化为一个单链表,所有操作都需要遍历整个链表。
- 装载因子的选择:实践中通常将装载因子控制在
0.75以下。当α超过阈值时,进行扩容(Rehashing)——创建一个更大的桶数组(通常是原来的两倍),然后将所有元素重新哈希到新数组中。扩容的时间复杂度为 O(n + m),但均摊(Amortized)后每次插入仍为 O(1)。 - 空间复杂度:O(n + m),其中 m 是桶数组的开销,n 是链表节点的开销。
与其他冲突解决方法的比较:
| 特性 | 开放哈希表(Separate Chaining) | 开放寻址法(Open Addressing) |
|---|---|---|
| 冲突处理 | 链表存储冲突元素 | 在数组内探测下一个空位 |
| 装载因子 | 可超过 1.0 | 必须小于 1.0 |
| 删除操作 | 直接删除链表节点 | 需要标记为"已删除"(Lazy Deletion) |
| 缓存性能 | 较差(链表节点不连续) | 较好(数据在连续数组中) |
| 内存开销 | 每个节点额外存储指针 | 无额外指针开销 |
| 适用场景 | 元素数量不确定、频繁删除 | 元素大小固定、内存受限 |
开放哈希表的优点是实现简单、删除方便、对装载因子不敏感。缺点是链表节点的内存不连续,缓存不友好。在实际应用中,Java 的 HashMap 和 C++ 的 std::unordered_map 都使用分离链接法作为默认的冲突解决策略(Java 8 在链表过长时会转为红黑树以进一步优化性能)。

浙公网安备 33010602011771号