哈希表
哈希表又叫散列表,关键值通过哈希函数映射到数组上,查找时通过关键值直接访问数组。哈希函数指的是关键值和存储位置建立的对应关系。
哈希函数指的是关键值和存储位置建立的对应关系,查找时只要根据这个关系就能找到目标位置。一般我们只要通过一次查找就能找到目标位置,但有些关键字需要多次比较和查找才能找到,这是为什么呢?因为哈希表里,可能存在关键字不同但是哈希地址相同的情况,也就是产生了冲突。一般情况下,冲突是不可避免的,因为关键字集合往往比哈希地址集合大很多。要提高哈希表的查找效率,关键在于合理的构造哈希函数和优秀的解决冲突的方法。
哈希函数的构造方法
直接寻址法,即取关键字的值或者关键字的某个函数变换值,线性的映射到存储地址上。如果关键字的数量和跨度不是很大,直接寻址法是最简单最有效的构造方法了,并且还可以避免冲突。但是如果关键字的数量和跨度很大的话,这种方法就用不了了,例如有 n 个关键字,值最小的为 0,最大的为 1010,这种情况下就不能用直接寻址法了,因为没有足够的空间可用来存储。
除留余数法,我们将关键字对整数 p 取的余数直接做为存储地址,整数 p 一般取小于等于哈希表长度 size 的最大质数,如果关键字不是整数,比如是一个字符串,可以先将其做个转换,然后再对 p 取余。选择优秀的 p 可以减少冲突的发生。
其他的构造方法还有分析数字法,随机数法等等。设计哈希函数没有统一的方法,同一个哈希函数不一定能适用所有问题,其产生的影响也是不一样的。但哈希函数的设计又是至关重要的,设计哈希函数时要达到两个要求:
- 计算简单,计算复杂的哈希函数会增加查询的时间;
- 关键字尽可能地均分到存储地址上,这样可以减少冲突。
处理冲突的方法
开放地址法
如果发生冲突,那么就使用某种策略寻找下一存储地址,直到找到一个不冲突的地址或者找到关键字,否则一直按这种策略继续寻找。如果冲突次数达到了上限则终止程序,表示关键字不存在哈希表里。一般常见的策略有这么几种:
- 线性探测法,如果当前的冲突位置为
d
,那么接下来探测地址为d+1
、d+2
、d+3
等,也就是从冲突地址往后面一个一个探测。 - 线性补偿探测法,它形成的探测地址为
d+m
,d+2*m
,d+3m
等,与线性探测法不同,这里的查找单位不是1,而是m
,为了能遍历到哈希表里所有位置,我们设置m
和表长size
互质。 - 随机探测法,这种方法和前两种方法类似,这里的查找单位不是一个固定值,而是一个随机序列。
- 二次探测法,它形成的探测地址为 d+12、d-12、d+22,d-22等,这种方法在冲突位置左右跳跃着寻找探测地址。
线性探测法容易形成“堆聚”的情况,即很多记录就连在一块,而且一旦形成“堆聚”,记录会越来越多。另外,开放地址法都有一个缺点,删除操作显得十分复杂,我们不能直接删除关键字所在的记录,否则在查找删除位置后面的元素时,可能会出现找不到的情况,因为删除位置上已经成了空地址,查找到这里会终止查找。
链地址法
该方法将所有哈希地址相同的结点构成一个单链表,单链表的头结点存在哈希数组里。链地址法常出现在经常插入和删除的情况下。
相比开放地址法,链地址法有以下优点:
- 不会出现“堆聚”现象,哈希地址不同的关键字不会发生冲突;
- 不需要重建哈希表,在链地址法里,因为结点都是动态申请的,所以不会出现哈希表里存满关键字的情况;
- 相比开放地址法,关键字删除更方便,只需要找到指定结点,删除该结点即可。
开放地址法和链地址法各有千秋,适用于不同情况。当关键字规模少的时候,开放地址法比链地址法更节省空间,因为用链地址法可能会存在哈希数组出现大量空地址的情况,而在关键字规模大的情况下,链地址法就比开放地址法更节省空间,链表产生的指针域可以忽略不计,关键字多,哈希数组里产生的空地址就少了。
再哈希法
多设置几个hash函数,当冲突发生时,多尝试几次不同的函数,直至找到不冲突的位置
建立公共溢出区
单独建立溢出区,当发生冲突时,将溢出元素存放到公共溢出区内,查找时在对应位置找不到的情况下,就去公共溢出区查找。
BKDR哈希算法
/*************************************************************************
> File Name: 7.hash.c
> Author: 陈杰
> Mail: 15193162746@163.com
> Created Time: 2021年04月03日 星期日 20时17分18秒
> BKDRHash算法演示,冲突处理方法拉链法
************************************************************************/
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node {
char *str;
struct Node *next;
} Node;
typedef struct HashTable{
Node **data;
int size;
} HashTable;
/*
* 初始化一个结点
* @param str: 字符串
* @param head: 链表头
* */
Node *init_node(char *str, Node *head) {
Node *p = (Node *)malloc(sizeof(Node));
p->str = strdup(str);
p->next = head;
return p;
}
/*
* 初始化一个hash表
* @param h: 哈希表容量
* */
HashTable *init_hashtable(int n) {
HashTable *h = (HashTable *)malloc(sizeof(HashTable));
h->size = n << 1;
h->data = (Node **)calloc(h->size, sizeof(Node *));
return h;
}
/*
* 哈希函数
* @param str: 要计算的字符串
* */
int BKDRHash(char *str) {
int seed = 31, hash = 0;
for(int i = 0; str[i]; i++) hash = hash * seed + str[i];
return hash & 0x7fffffff;
}
/*
* 插入操作
* @param h: 要操作的hash表
* @param str: 字符串
* */
int insert(HashTable *h, char *str) {
int hash = BKDRHash(str);
int ind = hash % h->size;
h->data[ind] = init_node(str, h->data[ind]);
}
/*
* 查找操作
* @param h: 要操作的hash表
* @param str: 字符串
* */
int search(HashTable *h, char *str) {
int hash = BKDRHash(str);
int ind = hash % h->size;
Node *p = h->data[ind];
while(p && strcmp(p->str, str)) p = p->next;
return p != NULL;
}
/*
* 清除链表
* @param node: 链表头结点
* */
void clear_node(Node *node) {
if(node == NULL) return;
Node *p = node, *q;
while(p) {
q = p->next;
free(p->str);
free(p);
p = q;
}
return;
}
/*
* 清理哈希表
* @param h: 要操作的hash表
* */
void clear(HashTable *h) {
if(h == NULL) return;
for(int i = 0; i < h->size; i++) clear_node(h->data[i]);
free(h->data);
free(h);
}
int main() {
int op;
#define MAX_N 100
char str[MAX_N + 5] = {0};
HashTable *h = init_hashtable(MAX_N + 5);
while(~scanf("%d %s", &op, str)) {
switch(op) {
case 0: {
printf("insert %s to HashTable\n\n", str);
insert(h, str);
}break;
case 1: {
printf("search %s from HashTable result = %d\n\n", str, search(h, str));
}break;
}
}
#undef MAX_N
clear(h);
return 0;
}