目录
1 引言
在现代计算机系统中,哈希表(Hash Table)因其高效的查找、插入和删除性能而被广泛应用于数据库索引、缓存系统、编译器符号表等场景。然而,由于哈希函数将无限多的输入映射到有限的地址空间,不可避免地会出现哈希冲突(Hash Collision)——即不同的键映射到了相同的位置。如何优雅、高效地解决这些冲突,是哈希表设计中的关键问题。本文将详细介绍解决哈希冲突的四种经典方法:开放定址法、链地址法、再哈希法和建立公共溢出区,分析它们的原理、优缺点及适用场景,并给出对比总结,帮助读者深入理解哈希表冲突处理机制。
2 开放定址法
开放定址法(Open Addressing)是一种解决哈希冲突的策略,其核心思想是在哈希表中继续“探查”其他空位以安置冲突元素。当插入的元素与已有元素发生哈希地址冲突时,不是另开空间,而是在哈希表内部按照一定的探查序列(如线性探测、二次探测或双重哈希)寻找下一个可用槽位。插入、查找和删除操作都基于相同的探查逻辑,以保持一致性。这种方法避免了额外的链表结构,占用空间较少,但在负载因子较高时容易出现“聚集”现象,影响查找效率。
2.1 线性探测法
线性探测法(Linear Probing)是开放定址法中最简单的一种形式,其原理是在哈希冲突发生时,按照固定的步长(通常为1)依次向后探查下一个位置,直到找到一个空槽为止。即如果元素 key
的初始哈希地址为 h(key)
,发生冲突时就尝试 h(key)+1
、h(key)+2
、……,直到遇到空位为止。查找和删除时也采用相同的线性序列进行探测,确保操作的一致性。该方法实现简单,但容易形成“聚集”现象(即冲突元素聚集在一起),影响性能。
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
#define EMPTY -1
// 哈希表结构
int hashTable[TABLE_SIZE];
// 初始化哈希表
void initHashTable()
{
for (int i = 0; i < TABLE_SIZE; i++)
{
hashTable[i] = EMPTY;
}
}
// 哈希函数
int hash(int key)
{
return key % TABLE_SIZE;
}
// 插入函数(线性探测法)
void insert(int key)
{
int index = hash(key);
int originalIndex = index;
int i = 0;
while (hashTable[index] != EMPTY)
{
i++;
index = (originalIndex + i) % TABLE_SIZE;
if (index == originalIndex)
{
printf("Hash table is full, cannot insert %d\n", key);
return;
}
}
hashTable[index] = key;
printf("Inserted %d at index %d\n", key, index);
}
// 查找函数(线性探测)
int search(int key)
{
int index = hash(key);
int originalIndex = index;
int i = 0;
while (hashTable[index] != EMPTY)
{
if (hashTable[index] == key)
{
return index;
}
i++;
index = (originalIndex + i) % TABLE_SIZE;
if (index == originalIndex) break;
}
return -1; // 未找到
}
// 打印哈希表
void printHashTable()
{
printf("Hash Table:\n");
for (int i = 0; i < TABLE_SIZE; i++)
{
if (hashTable[i] != EMPTY)
printf("[%d]: %d\n", i, hashTable[i]);
else
printf("[%d]: (empty)\n", i);
}
}
int main()
{
initHashTable();
insert(12);
insert(22);
insert(32);
insert(42);
insert(52);
printHashTable();
int key = 32;
int result = search(key);
if (result != -1)
printf("Found %d at index %d\n", key, result);
else
printf("%d not found in hash table\n", key);
return 0;
}
2.2 二次探测法
二次探测法(Quadratic Probing)是开放定址法的一种改进策略,用于解决哈希冲突。当发生冲突时,不是简单地顺序往后查找(如线性探测),而是按照二次函数间隔探测下一个位置,避免“主聚集”现象。
公式:
hi = (h(key) + c1*i + c2*i^2) % m
其中:
-
h(key)
是基本哈希函数 -
i
是探测次数(从 0 开始) -
c1
和c2
是常数,一般可取c1 = c2 = 1
-
m
是哈希表大小
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
#define EMPTY -1
int hashTable[TABLE_SIZE];
// 初始化哈希表
void initHashTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
hashTable[i] = EMPTY;
}
}
// 哈希函数
int hash(int key) {
return key % TABLE_SIZE;
}
// 插入函数(二次探测)
void insert(int key) {
int index = hash(key);
int i = 0;
while (hashTable[(index + i * i) % TABLE_SIZE] != EMPTY) {
i++;
if (i == TABLE_SIZE) {
printf("Hash table is full, cannot insert %d\n", key);
return;
}
}
int finalIndex = (index + i * i) % TABLE_SIZE;
hashTable[finalIndex] = key;
printf("Inserted %d at index %d\n", key, finalIndex);
}
// 查找函数(二次探测)
int search(int key) {
int index = hash(key);
int i = 0;
while (hashTable[(index + i * i) % TABLE_SIZE] != EMPTY) {
int curIndex = (index + i * i) % TABLE_SIZE;
if (hashTable[curIndex] == key) {
return curIndex;
}
i++;
if (i == TABLE_SIZE) break;
}
return -1; // 未找到
}
// 打印哈希表
void printHashTable() {
printf("Hash Table:\n");
for (int i = 0; i < TABLE_SIZE; i++) {
if (hashTable[i] != EMPTY)
printf("[%d]: %d\n", i, hashTable[i]);
else
printf("[%d]: (empty)\n", i);
}
}
int main() {
initHashTable();
insert(10);
insert(20);
insert(30);
insert(25);
insert(35);
insert(15);
printHashTable();
int key = 25;
int result = search(key);
if (result != -1)
printf("Found %d at index %d\n", key, result);
else
printf("%d not found in hash table\n", key);
return 0;
}
2.3 双重哈希法
双重哈希法(Double Hashing)是一种开放定址法的高级策略,利用两个不同的哈希函数来解决冲突。发生冲突时,用第二个哈希函数生成探测步长,按该步长跳跃式地寻找下一个可用槽位。其探测公式如下:
hi = (h1(key) + i * h2(key)) % m
其中 h1(key)
是主哈希函数,h2(key)
是第二哈希函数,i
是第 i
次冲突。与线性探测和二次探测相比,双重哈希更能有效避免聚集问题,具有更好的查找性能。
// 双重哈希法
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 11
#define EMPTY -1
int hashTable[TABLE_SIZE];
// 主哈希函数
int h1(int key) {
return key % TABLE_SIZE;
}
// 第二哈希函数(确保返回值不为0)
int h2(int key) {
return 7 - (key % 7); // 7 应小于 TABLE_SIZE 且为素数
}
// 初始化哈希表
void initHashTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
hashTable[i] = EMPTY;
}
}
// 插入函数(双重哈希)
void insert(int key) {
int index = h1(key);
int step = h2(key);
int i = 0;
while (hashTable[(index + i * step) % TABLE_SIZE] != EMPTY) {
i++;
if (i == TABLE_SIZE) {
printf("Hash table is full, cannot insert %d\n", key);
return;
}
}
int finalIndex = (index + i * step) % TABLE_SIZE;
hashTable[finalIndex] = key;
printf("Inserted %d at index %d\n", key, finalIndex);
}
// 查找函数(双重哈希)
int search(int key) {
int index = h1(key);
int step = h2(key);
int i = 0;
while (hashTable[(index + i * step) % TABLE_SIZE] != EMPTY) {
int curIndex = (index + i * step) % TABLE_SIZE;
if (hashTable[curIndex] == key) {
return curIndex;
}
i++;
if (i == TABLE_SIZE) break;
}
return -1;
}
// 打印哈希表
void printHashTable() {
printf("Hash Table:\n");
for (int i = 0; i < TABLE_SIZE; i++) {
if (hashTable[i] != EMPTY)
printf("[%d]: %d\n", i, hashTable[i]);
else
printf("[%d]: (empty)\n", i);
}
}
int main() {
initHashTable();
insert(10);
insert(22);
insert(31);
insert(4);
insert(15);
insert(28);
insert(17);
insert(88);
insert(59);
printHashTable();
int key = 28;
int result = search(key);
if (result != -1)
printf("Found %d at index %d\n", key, result);
else
printf("%d not found in hash table\n", key);
return 0;
}
3 链地址法
链地址法(Separate Chaining)是一种解决哈希冲突的经典方法。其核心思想是:将哈希表的每个槽位扩展为一个链表(或其他结构),当多个键哈希到同一个槽位时,依次插入到该槽位的链表中。这种方式允许多个元素共享同一哈希地址,插入和删除操作较为灵活,且不受负载因子的限制。尽管查找效率在冲突较多时会下降,但链地址法结构简单、易于实现,是实际应用中非常常见的哈希冲突解决方案。
// 链地址法
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
// 链表节点
typedef struct Node {
int key;
struct Node* next;
} Node;
// 哈希表数组(每个元素是链表头指针)
Node* hashTable[TABLE_SIZE];
// 哈希函数
int hashFunction(int key) {
return key % TABLE_SIZE;
}
// 插入函数
void insert(int key) {
int index = hashFunction(key);
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->key = key;
newNode->next = hashTable[index]; // 插入到链表头部
hashTable[index] = newNode;
printf("Inserted %d at index %d\n", key, index);
}
// 查找函数
int search(int key) {
int index = hashFunction(key);
Node* current = hashTable[index];
while (current != NULL) {
if (current->key == key) return 1;
current = current->next;
}
return 0;
}
// 打印哈希表
void printHashTable() {
printf("Hash Table:\n");
for (int i = 0; i < TABLE_SIZE; i++) {
printf("[%d]:", i);
Node* current = hashTable[i];
while (current != NULL) {
printf(" -> %d", current->key);
current = current->next;
}
printf(" -> NULL\n");
}
}
// 释放内存
void freeHashTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
Node* current = hashTable[i];
while (current != NULL) {
Node* temp = current;
current = current->next;
free(temp);
}
hashTable[i] = NULL;
}
}
int main() {
// 初始化哈希表
for (int i = 0; i < TABLE_SIZE; i++) {
hashTable[i] = NULL;
}
// 插入元素
insert(10);
insert(20);
insert(30);
insert(15);
insert(25);
printHashTable();
// 查找元素
int key = 15;
if (search(key)) {
printf("%d found in hash table.\n", key);
} else {
printf("%d not found in hash table.\n", key);
}
// 释放内存
freeHashTable();
return 0;
}
4 再哈希法
再哈希法(Rehashing)是一种通过使用多个不同的哈希函数来解决哈希冲突的方法。当一个元素在主哈希函数(如 h1(key)
)位置发生冲突后,系统会尝试依次使用其他哈希函数(如 h2(key)
、h3(key)
)重新计算位置,直到找到一个空槽。相比开放定址法中单一线性或二次探测的方式,再哈希法通过“多函数多路径”减少聚集问题,冲突分布更均匀,从而提高哈希表性能。不过,再哈希法实现上要求设计多个良好的哈希函数,增加了编程复杂度。
这里我们采用 双重哈希作为再哈希的代表:
-
h1(key) = key % TABLE_SIZE
-
h2(key) = PRIME - (key % PRIME)
(PRIME 为小于 TABLE_SIZE 的素数)
/*
再哈希法
双重哈希
*/
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 11
#define PRIME 7 // 用于第二哈希函数
int hashTable[TABLE_SIZE];
// 初始化哈希表
void initHashTable() {
for (int i = 0; i < TABLE_SIZE; i++)
hashTable[i] = -1;
}
// 第一个哈希函数
int hash1(int key) {
return key % TABLE_SIZE;
}
// 第二个哈希函数
int hash2(int key) {
return PRIME - (key % PRIME);
}
// 插入函数
void insert(int key) {
int index1 = hash1(key);
int index2 = hash2(key);
int i = 0;
while (i < TABLE_SIZE) {
int newIndex = (index1 + i * index2) % TABLE_SIZE;
if (hashTable[newIndex] == -1) {
hashTable[newIndex] = key;
printf("Inserted %d at index %d\n", key, newIndex);
return;
}
i++;
}
printf("Failed to insert %d: Table is full.\n", key);
}
// 查找函数
int search(int key) {
int index1 = hash1(key);
int index2 = hash2(key);
int i = 0;
while (i < TABLE_SIZE) {
int newIndex = (index1 + i * index2) % TABLE_SIZE;
if (hashTable[newIndex] == key)
return newIndex;
if (hashTable[newIndex] == -1)
return -1; // 说明不存在
i++;
}
return -1;
}
// 打印哈希表
void printHashTable() {
printf("Hash Table:\n");
for (int i = 0; i < TABLE_SIZE; i++) {
if (hashTable[i] != -1)
printf("[%d]: %d\n", i, hashTable[i]);
else
printf("[%d]: NULL\n", i);
}
}
int main() {
initHashTable();
insert(10);
insert(22);
insert(31);
insert(4);
insert(15);
insert(28);
insert(17);
insert(88);
insert(59);
printHashTable();
int key = 31;
int pos = search(key);
if (pos != -1)
printf("%d found at index %d\n", key, pos);
else
printf("%d not found in table.\n", key);
return 0;
}
5 建立公共溢出区
建立公共溢出区法是一种处理哈希冲突的策略,它将哈希表划分为两个部分:主表(Main Area)和溢出区(Overflow Area)。当哈希函数定位的主表槽位已被占用时,冲突的元素不在主表中探测或扩展,而是统一存入溢出区。查找时,若主表不匹配,就扫描溢出区。该方法结构清晰、插入简单,尤其适用于冲突频繁但查找频率较低的应用场景,但溢出区可能退化为线性搜索,影响查找性能。
// 建立公共溢出区法
#include <stdio.h>
#include <stdlib.h>
#define MAIN_TABLE_SIZE 7 // 主表大小
#define OVERFLOW_SIZE 10 // 溢出区大小
typedef struct {
int key;
int isOccupied;
} HashNode;
HashNode mainTable[MAIN_TABLE_SIZE];
HashNode overflowArea[OVERFLOW_SIZE];
// 初始化哈希表和溢出区
void initHashTable() {
for (int i = 0; i < MAIN_TABLE_SIZE; i++) {
mainTable[i].isOccupied = 0;
}
for (int i = 0; i < OVERFLOW_SIZE; i++) {
overflowArea[i].isOccupied = 0;
}
}
// 哈希函数
int hashFunction(int key) {
return key % MAIN_TABLE_SIZE;
}
// 插入函数
void insert(int key) {
int index = hashFunction(key);
if (!mainTable[index].isOccupied) {
mainTable[index].key = key;
mainTable[index].isOccupied = 1;
printf("Inserted %d in mainTable[%d]\n", key, index);
return;
}
// 插入溢出区
for (int i = 0; i < OVERFLOW_SIZE; i++) {
if (!overflowArea[i].isOccupied) {
overflowArea[i].key = key;
overflowArea[i].isOccupied = 1;
printf("Inserted %d in overflowArea[%d]\n", key, i);
return;
}
}
printf("Failed to insert %d: overflow area is full.\n", key);
}
// 查找函数
int search(int key) {
int index = hashFunction(key);
if (mainTable[index].isOccupied && mainTable[index].key == key) {
return index;
}
for (int i = 0; i < OVERFLOW_SIZE; i++) {
if (overflowArea[i].isOccupied && overflowArea[i].key == key) {
return MAIN_TABLE_SIZE + i;
}
}
return -1;
}
// 打印函数
void printHashTable() {
printf("\nMain Table:\n");
for (int i = 0; i < MAIN_TABLE_SIZE; i++) {
if (mainTable[i].isOccupied)
printf("[%d]: %d\n", i, mainTable[i].key);
else
printf("[%d]: NULL\n", i);
}
printf("\nOverflow Area:\n");
for (int i = 0; i < OVERFLOW_SIZE; i++) {
if (overflowArea[i].isOccupied)
printf("[%d]: %d\n", i, overflowArea[i].key);
else
printf("[%d]: NULL\n", i);
}
}
int main() {
initHashTable();
// 插入测试数据
int keys[] = {10, 17, 24, 3, 31, 45, 38, 52, 59, 66};
int n = sizeof(keys) / sizeof(keys[0]);
for (int i = 0; i < n; i++) {
insert(keys[i]);
}
printHashTable();
// 查找示例
int key = 59;
int pos = search(key);
if (pos != -1) {
if (pos < MAIN_TABLE_SIZE)
printf("\nKey %d found in mainTable[%d]\n", key, pos);
else
printf("\nKey %d found in overflowArea[%d]\n", key, pos - MAIN_TABLE_SIZE);
} else {
printf("\nKey %d not found\n", key);
}
return 0;
}
6 总结
哈希表作为高效的数据结构,其核心挑战在于处理哈希冲突。本文详细介绍了四种经典方法:开放定址法(包括线性探测、二次探测和双重哈希)通过表内探查解决冲突,节省空间但易受聚集现象影响;链地址法将冲突元素组织成链表,结构简单但需额外内存;再哈希法利用多哈希函数减少聚集,但实现复杂;公共溢出区法隔离冲突元素,适合低频查找场景。每种方法各有优劣,实际应用中需根据数据特征、负载因子和性能需求选择合适策略。理解这些冲突解决机制,有助于优化哈希表设计,提升系统性能。