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

在这个例子中,键 111 都被哈希到桶 1,键 21222 都被哈希到桶 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

可以看到,键 111 都映射到桶 1,键 21222 都映射到桶 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)的步骤如下:

  1. 用哈希函数计算键对应的桶下标 index = key % size
  2. 遍历该桶的链表,检查键是否已存在。
  3. 如果键已存在,更新其值。
  4. 如果键不存在,创建新节点并插入到链表头部(头部插入效率最高,时间复杂度 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)的步骤:

  1. 计算键的桶下标 index = key % size
  2. 遍历该桶的链表,逐个比较键。
  3. 找到则返回值的指针(或引用),未找到则返回空(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)的步骤:

  1. 计算键的桶下标 index = key % size
  2. 遍历该桶的链表,同时记录前驱节点(Previous Node)。
  3. 找到目标节点后,将前驱节点的 next 指针指向目标节点的下一个节点,然后释放目标节点。
  4. 如果目标节点是链表头节点,则直接更新桶指针。
// 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 有三条记录(22122 都哈希到桶 2),桶 5 有两条记录(155 都哈希到桶 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 / mn 为元素总数,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 在链表过长时会转为红黑树以进一步优化性能)。

posted @ 2026-04-16 17:50  游翔  阅读(6)  评论(0)    收藏  举报