哈希表查找(Hash Table Search)是一种基于哈希表的查找技术。哈希表是一种数据结构,它通过哈希函数将键映射到表中一个位置来访问记录,以加快查找的速度。理想情况下,哈希表可以提供O(1)的时间复杂度进行查找、插入和删除操作。
哈希表查找的基本步骤如下:
-
哈希函数的构造:哈希函数是将关键字(键)映射到哈希表中的一个位置(哈希地址)的函数。构造良好的哈希函数可以减少哈希冲突(两个关键字映射到同一个位置)的发生。
-
处理冲突:即使哈希函数设计得很好,冲突仍然不可避免。处理冲突的方法有开放地址法(线性探测、二次探测等)、链地址法、公共溢出区法等。
-
查找过程:
- 使用哈希函数计算关键字的哈希地址。
- 根据哈希地址访问哈希表中的位置。
- 如果位置上没有元素或者元素的关键字与待查关键字匹配,则查找成功。
- 如果发生冲突,根据选定的冲突处理方法继续查找,直到找到匹配的元素或者确定元素不在哈希表中。
哈希表查找的效率在很大程度上取决于哈希函数的质量和冲突处理方法。一个优秀的哈希函数应该能够将关键字均匀地分布在整个哈希表中,以减少冲突的可能性。而有效的冲突处理方法可以确保在发生冲突时仍能快速地找到正确的元素。
哈希表查找的优点是能够在平均情况下提供非常高的查找效率,特别是在数据量很大时。然而,它的缺点包括哈希函数的构造可能比较复杂,且在最坏情况下(如发生大量冲突)查找效率会下降。此外,哈希表通常需要更多的内存空间来存储哈希表和额外的冲突处理信息。
1.直接地址法
直接地址法是一种简单的哈希技术,它不使用哈希函数来计算数据的存储位置,而是直接使用关键字作为数据的索引或地址。这种方法的优点是实现简单,没有冲突处理的问题,查找时间复杂度为O(1)。然而,直接地址法也有其局限性,它要求关键字的范围较小,且连续,否则会造成空间的大量浪费。
直接地址法的基本思想是:
-
确定关键字范围:假设关键字的范围是从1到m。
-
创建直接地址表:创建一个大小为m的数组,数组的索引范围从1到m。
-
存储数据:当插入一个关键字为k的数据项时,直接将其存储在数组的第k个位置上。
-
查找数据:当需要查找关键字为k的数据项时,直接访问数组的第k个位置即可。
直接地址法的例子可以是用来存储一个班级学生的成绩,假设学生的编号是从1到n,那么可以直接使用一个大小为n的数组,数组的索引就是学生的编号,数组中对应位置的值就是该学生的成绩。
示例:使用直接地址法来存储和检索学生的分数。
#include <stdio.h>
#define MAX_STUDENTS 10
// 函数声明
void storeScore(int scores[], int studentId, int score);
int findScore(int scores[], int studentId);
int main() {
// 创建一个大小为MAX_STUDENTS的数组来存储分数
int scores[MAX_STUDENTS] = {0}; // 初始化为0
// 存储学生的分数
storeScore(scores, 1, 90);
storeScore(scores, 2, 85);
// ...以此类推
// 查找并打印学生3的分数
int studentId = 3;
int score = findScore(scores, studentId);
printf("学生%d的分数是: %d\n", studentId, score);
return 0;
}
// 存储分数的函数
void storeScore(int scores[], int studentId, int score) {
if (studentId >= 1 && studentId <= MAX_STUDENTS) {
scores[studentId - 1] = score; // 数组索引从0开始,所以减1
} else {
printf("学生编号超出范围\n");
}
}
// 查找分数的函数
int findScore(int scores[], int studentId) {
if (studentId >= 1 && studentId <= MAX_STUDENTS) {
return scores[studentId - 1]; // 数组索引从0开始,所以减1
} else {
printf("学生编号超出范围\n");
return -1; // 返回-1表示无效的学生编号
}
}
在这个示例中,我们定义了一个MAX_STUDENTS
常量来表示最大学生数,创建了一个数组scores
来存储分数。storeScore
函数用于存储学生的分数,而findScore
函数用于查找学生的分数。注意,由于C语言数组索引从0开始,我们需要在访问数组时将学生编号减1以获取正确的索引。
2.叠加法
叠加法(或称累加法)是一种简单的哈希函数构造方法,它通过将关键字的各个位的数字相加,得到一个哈希值。这种方法适用于关键字是由数字组成的情况,例如电话号码、身份证号码等。
叠加法的步骤如下:
-
确定关键字的位数:首先需要确定关键字的位数,例如一个电话号码可能有11位数字。
-
叠加关键字的各位数字:将关键字的每一位数字相加,得到一个总和。
-
取模运算:为了将哈希值映射到哈希表的索引范围内,通常需要对叠加的结果进行取模运算,即哈希值 = 总和 % 哈希表的大小。
-
处理冲突:由于叠加法可能会产生冲突,因此需要设计冲突处理机制,如开放地址法、链地址法等。
叠加法的优点是简单易实现,但它通常不是最佳的哈希函数构造方法,因为它可能会产生较多的冲突,尤其是当关键字的位数较多时。关键字的数字分布可能不均匀,导致哈希值的分布也不均匀,从而降低了哈希表的性能。
下面是一个使用叠加法构造哈希函数的C语言示例:
#include <stdio.h>
#define TABLE_SIZE 10
// 假设关键字是一个整数
unsigned int hash(unsigned int key) {
unsigned int sum = 0;
while (key > 0) {
sum += key % 10; // 取关键字的最后一位数字并累加到sum
key /= 10; // 移除关键字的最后一位数字
}
return sum % TABLE_SIZE; // 对sum进行取模运算以得到哈希值
}
int main() {
unsigned int key = 12345; // 示例关键字
unsigned int hashValue = hash(key);
printf("哈希值为: %u\n", hashValue);
return 0;
}
在这个示例中,我们定义了一个hash
函数,它接受一个整数关键字,通过叠加关键字的各位数字来构造哈希值。最后,我们对叠加的结果进行取模运算,以得到哈希表中的索引。
3.数字分析法
数字分析法是一种哈希函数的构造方法,它基于对关键字的分析,选取关键字的一部分或全部作为哈希值。这种方法适用于关键字具有一定规律或分布特点的情况,例如,关键字的后几位数字重复性高或者某些位上的数字分布不均匀等。
数字分析法的步骤如下:
-
分析关键字的分布特点:对关键字进行统计分析,找出其中的规律或分布特点。
-
选择哈希值的构成部分:根据关键字的分布特点,选择最有可能均匀分布的几位数字作为哈希值。
-
处理冲突:由于数字分析法可能会产生冲突,因此需要设计冲突处理机制,如开放地址法、链地址法等。
数字分析法的优点是可以根据关键字的特性来设计哈希函数,从而尽可能减少冲突的发生。然而,这种方法需要事先对关键字进行分析,且适用性有限,因为它依赖于关键字的特定分布。
下面是一个使用数字分析法的C语言示例:
#include <stdio.h>
#define TABLE_SIZE 10
// 假设关键字是一个整数,且我们知道关键字的最后两位数字是均匀分布的
unsigned int hash(unsigned int key) {
return key % 100; // 取关键字的最后两位数字作为哈希值
}
int main() {
unsigned int key = 12345; // 示例关键字
unsigned int hashValue = hash(key);
printf("哈希值为: %u\n", hashValue);
return 0;
}
4.平方取中法
平方取中法(Mid-Square Method)是一种构造哈希函数的方法,它首先计算关键字的平方,然后取平方值的中间几位作为哈希地址。这种方法的基本思想是通过平方操作增加关键字的复杂性,从而使得哈希地址的分布更加均匀,减少哈希冲突的概率。
平方取中法的步骤如下:
-
计算关键字的平方:对关键字进行平方运算,得到一个结果。
-
选择中间位数:从平方结果中选取中间的几位数作为哈希地址。通常选择的结果应该与哈希表的大小相匹配,以便直接用作哈希表的索引。
-
处理冲突:由于平方取中法仍然可能产生冲突,因此需要设计冲突处理机制,如开放地址法、链地址法等。
平方取中法的优点是简单易实现,且对于某些关键字分布能够产生较好的哈希效果。然而,这种方法也存在一些缺点,例如,它可能不适用于所有类型的关键字,且对于某些关键字分布可能会产生较多的冲突。
下面是一个使用平方取中法的C语言示例:
#include <stdio.h>
#include <math.h>
#define TABLE_SIZE 100
// 假设关键字是一个整数
unsigned int hash(unsigned int key) {
unsigned long long square = (unsigned long long)key * key; // 计算关键字的平方
unsigned int hashValue;
// 取平方值的中间几位作为哈希地址
// 假设TABLE_SIZE是10的幂,这里取中间两位
hashValue = (square / TABLE_SIZE) % TABLE_SIZE;
return hashValue;
}
int main() {
unsigned int key = 28; // 示例关键字
unsigned int hashValue = hash(key);
printf("哈希值为: %u\n", hashValue);
return 0;
}
在这个示例中,我们假设关键字是一个整数,我们计算关键字的平方,然后取平方值的中间几位作为哈希地址。这里我们假设哈希表的大小是100,因此我们取平方值的中间两位作为哈希值。实际应用中,需要根据哈希表的大小和关键字的特性来决定取平方值的哪些位数。
5.保留余数法
保留余数法(Modulo Division Hashing)是一种简单的哈希函数构造方法,它通过取关键字与哈希表大小(通常是一个质数)的模(余数)来计算哈希地址。这种方法是最常见的哈希函数构造方法之一,因为它简单且易于实现。
保留余数法的步骤如下:
-
选择一个合适的模数:通常选择一个不大于哈希表大小的质数作为模数。这样可以更好地分布关键字到哈希表的各个位置,减少哈希冲突的概率。
-
计算哈希地址:对关键字进行模运算,即取关键字与模数的余数,得到哈希地址。
-
处理冲突:由于保留余数法可能会产生冲突,因此需要设计冲突处理机制,如开放地址法、链地址法等。
保留余数法的优点是计算简单,对于大多数关键字分布能够产生较好的哈希效果。然而,这种方法也存在一些缺点,例如,当关键字分布不均匀时,可能会产生较多的冲突。
下面是一个使用保留余数法的C语言示例:
#include <stdio.h>
#define TABLE_SIZE 11 // 通常选择一个质数作为哈希表的大小
// 假设关键字是一个整数
unsigned int hash(unsigned int key) {
return key % TABLE_SIZE; // 取关键字与哈希表大小的模
}
int main() {
unsigned int key = 28; // 示例关键字
unsigned int hashValue = hash(key);
printf("哈希值为: %u\n", hashValue);
return 0;
}
在这个示例中,我们假设关键字是一个整数,我们选择哈希表的大小为11(一个质数),然后通过取关键字与11的模来计算哈希地址。这个简单的哈希函数将关键字的余数作为索引值,但实际应用中可能需要根据关键字的特性和哈希表的大小来选择一个合适的模数。
6.处理冲突的方法
线性探查法
线性探查法(Linear Probing)是一种解决哈希冲突的方法,也称为线性开放地址法。当发生哈希冲突时,线性探查法通过逐个检查哈希表中的下一个地址,直到找到一个空地址为止,然后将冲突的元素存储在该地址中。
线性探查法的步骤如下:
-
计算哈希地址:使用哈希函数计算关键字的哈希地址。
-
检查冲突:如果哈希地址已被占用,即发生冲突,则检查下一个地址(通常是哈希地址+1)。
-
找到空地址:如果下一个地址也被占用,继续检查下一个地址,直到找到一个空地址。
-
存储元素:将冲突的元素存储在找到的空地址中。
-
查找元素:查找元素时,从原始哈希地址开始,如果地址被占用且关键字不匹配,则按照相同的方式探查下一个地址,直到找到元素或遇到空地址为止。
示例:
#include <stdio.h>
#include <stdbool.h>
#define TABLE_SIZE 10
// 哈希表
int hashTable[TABLE_SIZE] = {0};
// 哈希函数
unsigned int hash(int key) {
return key % TABLE_SIZE;
}
// 插入元素
void insert(int key) {
int index = hash(key);
while (hashTable[index] != 0) {
index = (index + 1) % TABLE_SIZE; // 线性探查
}
hashTable[index] = key;
}
// 查找元素
bool search(int key) {
int index = hash(key);
int originalIndex = index;
while (hashTable[index] != 0) {
if (hashTable[index] == key) {
return true;
}
index = (index + 1) % TABLE_SIZE; // 线性探查
if (index == originalIndex) { // 如果回到原点,表示已遍历整个哈希表
return false;
}
}
return false;
}
int main() {
// 插入元素
insert(3);
insert(7);
insert(11); // 哈希地址为0,但0已被占用,因此需要探查
// 查找元素
printf("元素3的存在性: %s\n", search(3) ? "存在" : "不存在");
printf("元素7的存在性: %s\n", search(7) ? "存在" : "不存在");
printf("元素11的存在性: %s\n", search(11) ? "存在" : "不存在");
printf("元素5的存在性: %s\n", search(5) ? "存在" : "不存在");
return 0;
}
在这个示例中,我们定义了一个简单的哈希表和哈希函数,并实现了插入和查找操作。当插入元素时,如果发生冲突,我们使用线性探查法找到下一个空地址。在查找元素时,我们也是从原始哈希地址开始,如果地址被占用且关键字不匹配,则继续探查下一个地址。
链地址法
链地址法(Chaining)是一种解决哈希冲突的方法,它使用链表来处理多个关键字映射到同一个哈希地址的情况。在链地址法中,哈希表的每个槽位(bucket)或称为桶,都对应一个链表。当发生冲突时,冲突的关键字会被添加到同一个链表中。
链地址法的步骤如下:
-
计算哈希地址:使用哈希函数计算关键字的哈希地址。
-
处理冲突:如果哈希地址已被占用,即发生冲突,则将冲突的关键字添加到该地址对应的链表中。
-
查找元素:查找元素时,首先计算关键字的哈希地址,然后在对应的链表中搜索关键字。
示例:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int key;
struct Node *next;
} Node;
typedef struct {
Node **table;
int size;
} HashTable;
// 哈希函数
unsigned int hash(int key, int size) {
return key % size;
}
// 创建哈希表
HashTable *createHashTable(int size) {
HashTable *hashTable = (HashTable *)malloc(sizeof(HashTable));
hashTable->size = size;
hashTable->table = (Node **)calloc(size, sizeof(Node *));
return hashTable;
}
// 插入元素
void insert(HashTable *hashTable, int key) {
int index = hash(key, hashTable->size);
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->key = key;
newNode->next = NULL;
// 插入到链表的头部
newNode->next = hashTable->table[index];
hashTable->table[index] = newNode;
}
// 查找元素
bool search(HashTable *hashTable, int key) {
int index = hash(key, hashTable->size);
Node *current = hashTable->table[index];
while (current != NULL) {
if (current->key == key) {
return true;
}
current = current->next;
}
return false;
}
int main() {
HashTable *hashTable = createHashTable(10);
// 插入元素
insert(hashTable, 3);
insert(hashTable, 7);
insert(hashTable, 11); // 哈希地址与3相同,但会被添加到链表中
// 查找元素
printf("元素3的存在性: %s\n", search(hashTable, 3) ? "存在" : "不存在");
printf("元素7的存在性: %s\n", search(hashTable, 7) ? "存在" : "不存在");
printf("元素11的存在性: %s\n", search(hashTable, 11) ? "存在" : "不存在");
printf("元素5的存在性: %s\n", search(hashTable, 5) ? "存在" : "不存在");
// 释放哈希表
// 注意:这里没有释放链表中的节点,实际应用中需要释放所有节点
free(hashTable->table);
free(hashTable);
return 0;
}
在这个示例中,我们定义了一个哈希表结构,其中包含一个指针数组,每个指针指向一个链表的头节点。我们还定义了链表节点的结构。在插入元素时,如果发生冲突,我们将新节点添加到对应链表的头部。在查找元素时,我们遍历对应的链表来查找关键字。
结语
以上就是哈希表查找的基本使用方法了,本次代码分享到此结束,后续还会分享数据结构与算法的有关知识。最后的最后还请大家点点赞,点点关注,谢谢大家!