一、哈希函数:
哈希函数是将键映射到哈希表索引的函数。它应该尽可能地将键均匀地分布在哈希表的所有位置上,以减少冲突(多个键映射到同一索引)的可能性。
以下是常见的哈希函数:
1.直接定址法(Direct Addressing)
直接定址法适用于键的取值范围较小且连续的情况。它的基本思想是将每个键直接映射到哈希表中的一个具体位置,这个位置通常就是键的值。
在直接定址法中,哈希函数的形式为:
h(k) = k
其中 k 是键的值。这意味着每个键的值就是其在哈希表中的位置,因此需要一个大小足够的数组来存储所有可能的键值。
直接定址法的优点是简单快速,哈希值的计算时间是常数级别的。但是它的缺点是需要一个足够大的数组来存储所有可能的键值,因此适用于键值范围较小且连续的情况。如果键值范围过大或者不连续,就会造成空间的浪费。
以下是一个简单的示例,演示了如何使用直接定址法来实现哈希表:
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 1000
// 定义哈希表结构
typedef struct {
int keys[TABLE_SIZE];
int values[TABLE_SIZE];
} HashTable;
// 创建哈希表
HashTable* createHashTable() {
HashTable* table = (HashTable*)malloc(sizeof(HashTable));
return table;
}
// 插入键值对到哈希表中
void insert(HashTable* table, int key, int value) {
table->keys[key] = key;
table->values[key] = value;
}
// 查找键对应的值
int find(HashTable* table, int key) {
return table->values[key];
}
// 主函数
int main() {
HashTable* table = createHashTable();
// 插入键值对
insert(table, 10, 100);
insert(table, 20, 200);
insert(table, 30, 300);
// 查找键对应的值
printf("Value for key 10: %d\n", find(table, 10));
printf("Value for key 20: %d\n", find(table, 20));
printf("Value for key 30: %d\n", find(table, 30));
return 0;
}
2.除留取余法(Division Method)
除留取余法使用键值除以哈希表的大小,然后取余数作为哈希值。它的形式为:
h(k) = k mod m
其中,k 是键的值,m 是哈希表的大小。
除留取余法的实现非常简单,适用于大多数哈希表的场景。它的优点是简单易实现,并且在键值分布均匀的情况下可以得到良好的哈希值。但是,如果键的值分布不均匀,可能会导致哈希冲突较多。
以下是一个简单的示例,演示了如何使用除留取余法来实现哈希函数:
#include <stdio.h>
#define TABLE_SIZE 10
// 哈希函数:除留取余法
int hash(int key) {
return key % TABLE_SIZE;
}
// 主函数
int main() {
int keys[] = {12, 25, 6, 8, 17, 35, 14, 23, 11, 9};
int n = sizeof(keys) / sizeof(keys[0]);
// 计算哈希值并输出
printf("Keys and their hash values:\n");
for (int i = 0; i < n; i++) {
int h = hash(keys[i]);
printf("Key: %d, Hash: %d\n", keys[i], h);
}
return 0;
}
3.平方取中法(Mid Square Method)
平方取中法将键值的平方作为中间结果,然后取中间结果的某一部分作为哈希值。具体步骤如下:
- 将键值进行平方运算。
- 取平方结果的中间一部分作为哈希值。
平方取中法的形式为:
h(k) = mid ( (k2) ,d,w)
其中,k 是键的值,d 是哈希值的位数,w 是键的位数。函数mid(x,d,w) 表示取 x 的中间 d 位作为哈希值。
以下是一个示例,演示如何使用平方取中法来实现哈希函数:
#include <stdio.h>
#include <math.h>
// 计算 x 的中间 d 位数作为哈希值
int mid(int x, int d, int w) {
int mask = (int)pow(10, w - d) - 1; // 用于取中间 d 位数的掩码
return (x / (int)pow(10, (w - d) / 2)) % (int)pow(10, d); // 取中间 d 位数
}
// 平方取中法哈希函数
int hash(int key, int d, int w) {
return mid(key * key, d, w); // 对键的平方取中间部分作为哈希值
}
// 主函数
int main() {
int keys[] = {12, 25, 6, 8, 17, 35, 14, 23, 11, 9};
int n = sizeof(keys) / sizeof(keys[0]);
// 计算哈希值并输出
printf("Keys and their hash values:\n");
for (int i = 0; i < n; i++) {
int h = hash(keys[i], 2, 4); // 设置哈希值的位数为 2,键的位数为 4
printf("Key: %d, Hash: %d\n", keys[i], h);
}
return 0;
}
二、哈希冲突:
当两个或多个键通过哈希函数映射到哈希表的同一个索引位置时,就发生了哈希冲突。处理冲突的方法有很多种,包括开放寻址法和链表法等。
三、解决哈希冲突的方法:
1.开放寻址法(Open Addressing)
线性探测(Linear Probing):
线性探测是一种开放寻址法的处理哈希冲突的方法。当发生哈希冲突时,线性探测会依次检查哈希表中的下一个位置,直到找到一个空闲位置为止。具体步骤如下:
- 当发生哈希冲突时,计算出下一个探测位置。
- 检查该位置是否为空,如果为空,则将键值对存储在该位置;如果不为空,则继续探测下一个位置。
- 重复以上步骤,直到找到一个空闲位置为止,或者遍历了整个哈希表。
线性探测的探测步长是固定的,通常为 1。也就是说,每次探测都会顺序地检查下一个位置。
下面是一个简单的示例,演示了如何使用线性探测来处理哈希冲突:
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
// 哈希表结构
typedef struct {
int keys[TABLE_SIZE];
int values[TABLE_SIZE];
} HashTable;
// 创建哈希表
HashTable* createHashTable() {
HashTable* table = (HashTable*)malloc(sizeof(HashTable));
for (int i = 0; i < TABLE_SIZE; ++i) {
table->keys[i] = -1; // 初始化所有键为 -1,表示空闲位置
}
return table;
}
// 哈希函数:除留余数法
int hash(int key) {
return key % TABLE_SIZE;
}
// 插入键值对到哈希表中(使用线性探测解决冲突)
void insert(HashTable* table, int key, int value) {
int index = hash(key);
while (table->keys[index] != -1) { // 发生冲突时,进行线性探测
index = (index + 1) % TABLE_SIZE; // 计算下一个探测位置
}
table->keys[index] = key;
table->values[index] = value;
}
// 查找键对应的值
int find(HashTable* table, int key) {
int index = hash(key);
while (table->keys[index] != -1) { // 在哈希表中进行线性探测
if (table->keys[index] == key) {
return table->values[index]; // 找到键,返回对应的值
}
index = (index + 1) % TABLE_SIZE; // 继续探测下一个位置
}
return -1; // 未找到键,返回 -1
}
// 主函数
int main() {
HashTable* table = createHashTable();
// 插入键值对
insert(table, 12, 120);
insert(table, 22, 220);
insert(table, 32, 320);
// 查找键对应的值
printf("Value for key 12: %d\n", find(table, 12));
printf("Value for key 22: %d\n", find(table, 22));
printf("Value for key 32: %d\n", find(table, 32));
printf("Value for key 42: %d\n", find(table, 42)); // 键不存在
return 0;
}
二次探测(Quadratic Probing):
二次探测是一种解决哈希冲突的开放寻址法方法之一。与线性探测不同,二次探测的探测步长不是固定的增量,而是通过一个二次函数计算得到的。具体步骤如下:
- 当发生哈希冲突时,计算出下一个探测位置。
- 计算下一个探测位置时,使用一个二次函数,而不是简单地加上固定的增量。典型的二次函数为f(i)=i^2。
- 检查该位置是否为空,如果为空,则将键值对存储在该位置;如果不为空,则继续计算下一个探测位置。
- 如果在探测过程中遍历了整个哈希表仍未找到空闲位置,表示哈希表已满,需要进行相应的处理(例如重新哈希或扩展哈希表)。
以下是一个简单的示例,演示了如何使用二次探测来处理哈希冲突:
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
// 哈希表结构
typedef struct {
int keys[TABLE_SIZE];
int values[TABLE_SIZE];
} HashTable;
// 创建哈希表
HashTable* createHashTable() {
HashTable* table = (HashTable*)malloc(sizeof(HashTable));
for (int i = 0; i < TABLE_SIZE; ++i) {
table->keys[i] = -1; // 初始化所有键为 -1,表示空闲位置
}
return table;
}
// 哈希函数:除留余数法
int hash(int key) {
return key % TABLE_SIZE;
}
// 插入键值对到哈希表中(使用二次探测解决冲突)
void insert(HashTable* table, int key, int value) {
int index = hash(key);
int i = 1;
while (table->keys[index] != -1) { // 发生冲突时,进行二次探测
index = (index + i * i) % TABLE_SIZE; // 计算下一个探测位置
i++; // 递增二次探测的步长
}
table->keys[index] = key;
table->values[index] = value;
}
// 查找键对应的值
int find(HashTable* table, int key) {
int index = hash(key);
int i = 1;
while (table->keys[index] != -1) { // 在哈希表中进行二次探测
if (table->keys[index] == key) {
return table->values[index]; // 找到键,返回对应的值
}
index = (index + i * i) % TABLE_SIZE; // 计算下一个探测位置
i++; // 递增二次探测的步长
}
return -1; // 未找到键,返回 -1
}
// 主函数
int main() {
HashTable* table = createHashTable();
// 插入键值对
insert(table, 12, 120);
insert(table, 22, 220);
insert(table, 32, 320);
// 查找键对应的值
printf("Value for key 12: %d\n", find(table, 12));
printf("Value for key 22: %d\n", find(table, 22));
printf("Value for key 32: %d\n", find(table, 32));
printf("Value for key 42: %d\n", find(table, 42)); // 键不存在
return 0;
}
双重哈希(Double Hashing)
双重哈希不是简单地在哈希表中依次查找下一个位置,而是使用第二个独立的哈希函数来计算探测的步长。具体步骤如下:
- 当发生哈希冲突时,计算出第一个哈希函数的结果作为起始位置。
- 如果该位置为空,则将键值对存储在该位置。
- 如果该位置不为空,则使用第二个哈希函数计算出一个步长,然后依次探测下一个位置,直到找到一个空闲位置为止。
- 将键值对存储在空闲位置上。
双重哈希的优点在于它可以提供更好的探测步长,从而减少哈希冲突的发生,并且相对于线性探测和二次探测,它更均匀地分布键值对。
以下是一个简单的示例,演示了如何使用双重哈希来处理哈希冲突:
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
// 哈希表结构
typedef struct {
int keys[TABLE_SIZE];
int values[TABLE_SIZE];
} HashTable;
// 创建哈希表
HashTable* createHashTable() {
HashTable* table = (HashTable*)malloc(sizeof(HashTable));
for (int i = 0; i < TABLE_SIZE; ++i) {
table->keys[i] = -1; // 初始化所有键为 -1,表示空闲位置
}
return table;
}
// 哈希函数1:除留余数法
int hash1(int key) {
return key % TABLE_SIZE;
}
// 哈希函数2:另一种简单的哈希函数
int hash2(int key) {
return 7 - (key % 7); // 7 是一个较小的质数,确保哈希函数2与哈希表大小互质
}
// 插入键值对到哈希表中(使用双重哈希解决冲突)
void insert(HashTable* table, int key, int value) {
int index = hash1(key);
int step = hash2(key);
while (table->keys[index] != -1) { // 发生冲突时,进行双重哈希探测
index = (index + step) % TABLE_SIZE; // 计算下一个探测位置
}
table->keys[index] = key;
table->values[index] = value;
}
// 查找键对应的值
int find(HashTable* table, int key) {
int index = hash1(key);
int step = hash2(key);
while (table->keys[index] != -1) { // 在哈希表中进行双重哈希探测
if (table->keys[index] == key) {
return table->values[index]; // 找到键,返回对应的值
}
index = (index + step) % TABLE_SIZE; // 继续探测下一个位置
}
return -1; // 未找到键,返回 -1
}
// 主函数
int main() {
HashTable* table = createHashTable();
// 插入键值对
insert(table, 12, 120);
insert(table, 22, 220);
insert(table, 32, 320);
// 查找键对应的值
printf("Value for key 12: %d\n", find(table, 12));
printf("Value for key 22: %d\n", find(table, 22));
printf("Value for key 32: %d\n", find(table, 32));
printf("Value for key 42: %d\n", find(table, 42)); // 键不存在
return 0;
}
2.链表法(Chaining)
链表法通过在哈希表的每个槽点(slot)上维护一个链表或其他形式的容器来存储具有相同哈希值的键值对。当发生冲突时,新的键值对会被添加到相应槽点所对应的链表中。
具体步骤如下:
- 对键进行哈希运算,得到哈希值。
- 使用哈希值确定键值对应该存储的槽点(slot)。
- 如果槽点上已经有键值对存在,将新的键值对添加到链表的末尾;如果槽点为空,则创建一个新的链表,并将新的键值对作为链表的第一个元素。
- 当需要查找或删除某个键值对时,根据哈希值找到对应的槽点,然后遍历链表查找目标键值对。
链表法的优点是简单易实现,并且可以有效地处理哈希冲突,适用于大多数情况。但是,当链表过长时,会影响哈希表的性能,因此需要在实际应用中进行适当的调优,例如通过动态调整哈希表大小、使用更高效的哈希函数等方式来优化。
以下是一个简单的示例,演示了如何使用链表法来处理哈希冲突:
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
// 键值对结构
typedef struct Node {
int key;
int value;
struct Node* next;
} Node;
// 哈希表结构
typedef struct {
Node* slots[TABLE_SIZE];
} HashTable;
// 创建新的节点
Node* createNode(int key, int value) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->key = key;
newNode->value = value;
newNode->next = NULL;
return newNode;
}
// 创建哈希表
HashTable* createHashTable() {
HashTable* table = (HashTable*)malloc(sizeof(HashTable));
for (int i = 0; i < TABLE_SIZE; ++i) {
table->slots[i] = NULL; // 初始化所有槽点为空
}
return table;
}
// 哈希函数:除留余数法
int hash(int key) {
return key % TABLE_SIZE;
}
// 插入键值对到哈希表中(使用链表法解决冲突)
void insert(HashTable* table, int key, int value) {
int index = hash(key);
Node* newNode = createNode(key, value);
if (table->slots[index] == NULL) {
table->slots[index] = newNode; // 槽点为空,直接插入新节点
} else {
Node* current = table->slots[index];
while (current->next != NULL) {
current = current->next;
}
current->next = newNode; // 槽点非空,遍历链表找到末尾插入新节点
}
}
// 查找键对应的值
int find(HashTable* table, int key) {
int index = hash(key);
Node* current = table->slots[index];
while (current != NULL) {
if (current->key == key) {
return current->value; // 找到键,返回对应的值
}
current = current->next;
}
return -1; // 未找到键,返回 -1
}
// 主函数
int main() {
HashTable* table = createHashTable();
// 插入键值对
insert(table, 12, 120);
insert(table, 22, 220);
insert(table, 32, 320);
// 查找键对应的值
printf("Value for key 12: %d\n", find(table, 12));
printf("Value for key 22: %d\n", find(table, 22));
printf("Value for key 32: %d\n", find(table, 32));
printf("Value for key 42: %d\n", find(table, 42)); // 键不存在
return 0;
}
四、哈希表的实现:
哈希表通常由一个数组和一个哈希函数组成。哈希函数将键映射到数组的索引位置,然后在该位置存储键值对。在处理哈希冲突时,需要根据所选择的解决方法调整哈希表的存储结构。
以下示例演示了如何使用 C 语言实现一个简单的哈希表。哈希表由一个固定大小的数组构成,数组的每个元素是一个指向哈希表节点的指针。每个节点包含键和值。通过哈希函数计算键的哈希值,将其映射到数组的索引,然后将节点插入到对应的索引位置。在查找时,通过哈希函数计算键的哈希值,然后在对应的索引位置查找节点。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TABLE_SIZE 10
// 定义哈希表节点结构
typedef struct {
char* key;
int value;
} Node;
// 定义哈希表结构
typedef struct {
Node* nodes[TABLE_SIZE];
} HashTable;
// 哈希函数,计算键的哈希值
unsigned int hash(const char* key) {
unsigned int hash = 0;
for (int i = 0; i < strlen(key); i++) {
hash = hash * 31 + key[i];
}
return hash % TABLE_SIZE;
}
// 创建哈希表
HashTable* createHashTable() {
HashTable* table = (HashTable*)malloc(sizeof(HashTable));
for (int i = 0; i < TABLE_SIZE; i++) {
table->nodes[i] = NULL;
}
return table;
}
// 插入键值对到哈希表中
void insert(HashTable* table, const char* key, int value) {
unsigned int index = hash(key);
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->key = strdup(key);
newNode->value = value;
table->nodes[index] = newNode;
}
// 查找键对应的值
int find(HashTable* table, const char* key) {
unsigned int index = hash(key);
if (table->nodes[index] != NULL && strcmp(table->nodes[index]->key, key) == 0) {
return table->nodes[index]->value;
}
return -1; // 如果键不存在,则返回-1
}
// 主函数
int main() {
HashTable* table = createHashTable();
// 插入键值对
insert(table, "apple", 10);
insert(table, "banana", 20);
insert(table, "orange", 30);
// 查找键对应的值
printf("Value for 'apple': %d\n", find(table, "apple"));
printf("Value for 'banana': %d\n", find(table, "banana"));
printf("Value for 'orange': %d\n", find(table, "orange"));
printf("Value for 'grape': %d\n", find(table, "grape")); // 键不存在
return 0;
}