哈希表(哈希函数和处理哈希冲突)
- 前言
关于哈希表的主题的小记原计划5月23日完成,由于本人新冠阳性,身体发烧乏力,周末感觉身体状况稍加恢复,赶紧打开电脑把本文完成,特别秉承“写是为了更好地思考,更好地思考才能取得更大进步”的思想,不敢懈怠,赶紧把学习到的东西记录下来。
首先我们要搞明白为什么要发明哈希表的技术?我们学习和讨论过各种结构(线性表、树、图等),数据记录在结构中的相对位置是随机的,记录和关键字之间不存在确定的关系,因此,在查找记录时,需进行和关键字进行一系列的比较。查找效率依赖于查找过程中所比较的次数。哈希表的发表就是希望不经过任何比较,一次存取便能得到查找记录,可以大大提升查找、插入、删除等基本操作的效率。
要实现哈希表的技术思想,我们必须在储存位置和它的关键字之间建立一个确定的对应关系或对应函数f,使每个关键字和数据结构的为转移储存位置相对应。因此在查找关键字key的时候,我们只需调取函数f(k),计算出数据的储存位置,若结构中存在的关键字和key相等,那么必定在f(key)的位置上。因此不需要进行任何比较便可直接取得查询的记录。在此我们称这个对应关系为哈希函数(Hash function),按这个思想建立的表称为哈希表(Hash table)。
- 哈希函数
2.1 哈希冲突
由于哈数是一类影像函数,那么它就比不可避免出现这样一类情况,原始的关键字key_m和key_n并不相等,但是经过哈希函数处理之后f(key_m)=f(key_n),这种情况下就出现所谓的“哈希冲突”。比如我们有5个关键字k1…k5,经过哈希函数h(x)影像之后,h(k2)=h(k5),那么就说k2和k5在h(x)哈希函数影像下出现冲突。
实际应用当中,只能减少哈希冲突的概率,而没有办法杜绝哈希冲突,因为哈希实际上是对关键字进行了某种程度信息上的压缩,导致的后果就是压缩的信息可能相同,需要再次进行区分。
2.2 哈希函数
构造哈希函数的方法有很多,在介绍各种方法之前,首先需要明确什么是好的哈希函数。若对关键字集合当中的任何一个关键字,经过哈希函数映射到地址中的任意地址的概率是相等的,则称作此类哈希函数是均匀的哈希函数。换句话手,就是经过哈希函数映射后,得到一个随机地址,以便使一组关键字的哈希地址分布在整个区间中,从而减少冲突。
2.3 构造哈希函数的常用方法
a) 直接地址法
取关键字或关键字某个线性函数的值为哈希地址,
H
(
K
e
y
)
=
a
∗
k
e
y
+
b
H(Key)=a*key+b
H(Key)=a∗key+b
由于直接定址所得关键字地址和集合相同,对于不同的关键字不会发生哈希冲突,但在实际应用中,使用这种哈希哈数非常少。
b) 数字分析法
假设关键字都是以10为基的数,并且哈希表中的关键字都是事先知道的,则可以取关键字的若干位组成哈希地址。
c) 平方取中法
取关键字平方后的中间几位为哈希地址。这是一种较常用的构造哈希函数的方法。通常在选定哈希哈数的时候不一定能知道关键字的全部情况,取其中哪几位也不一定合适,而一个数字平方后的中间几位和数的每一位都相关,由此随机分布的关键字的哈希地址也是随机的。
d) 折叠法
将关键字分为位数相同的几部分,然后取这几个部分的叠加和作为哈希地址,这个方法称为折叠法(folding)。关键字位数很多,而且关键字每一位上数字分布大致均匀时,可以采用折叠法得到哈希地址。
e)除留余数法
取关键字被某个不大于表长m的数p除后所得余数为哈希地址,也即是说,
H
(
k
e
y
)
=
k
e
y
M
O
D
p
(
p
≤
m
)
H(key)=key\ MOD\ p\ \ (p≤m)
H(key)=key MOD p (p≤m)
这是最简单,最常用的构造哈希函数的方法,不仅对关键字可以直接取模,也可在折叠,平方取中后进行取模处理。
- 哈希冲突处理方法
“好”的哈希函数可以减少哈希冲突概率,但不能避免,因此,如何处理哈希冲突是哈希造表不可缺少的一个方面。通常处理哈希冲突有下列几种方法:
3.1 开放定址法
H
i
=
(
H
(
k
e
y
)
+
d
i
)
M
O
D
m
(
i
=
1
,
2
,
.
.
.
k
)
,
k
<
=
(
m
−
1
)
H_i=(H(key)+d_i)\ MOD\ m\ \ \ (i=1,2,...k), k<=(m-1)
Hi=(H(key)+di) MOD m (i=1,2,...k),k<=(m−1)
H(key)为哈希函数,m为哈希表长,di为增量序列,可用下列三种取法:
(1)di=1,2,3,…m-1, 线性探测
(2) di= ±12,±22,…±k2 称为二次探测再散列
(3) di= 伪随机序列
3.2 链地址法
将所有关键字为同义词的记录存储在同一线性表中。假定某哈希函数产生的哈希地址在区间[0,m-1]上,则设立一个指针型向量
Chain ChainHash[m]
其每个分量的初始地址都是空指针,凡是哈希地址为i的记录,都插入到头指针为ChainHash[i]的链表中。为了便于查找、插入和删除操作,需要保证后续链表按关键字有序。
- 哈希函数实现
本文采用链地址方法处理冲突,哈希函数采用最简单的除留余数法。
4.1 头文件
/**
* @file hash_table.h
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-05-23
*
* @copyright Copyright (c) 2023
*
*/
#ifndef HASH_TABLE_H
#define HASH_TABLE_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdbool.h>
#include "../00_introduction/Status.h"
#define EQ(a,b) ((a)==(b))
#define LT(a,b) ((a)<(b))
#define LQ(a,b) ((a)<=(b))
#define MAX_LEN 20
#define m 13 //采用除留余数法,除数定义为13
typedef int KeyType;
typedef char* Record;
typedef struct ElemType
{
KeyType key;
Record value;
}ElemType;
typedef struct HashNode
{
KeyType key;
struct HashNode *next;
} HashNode, *HashPtr;
typedef enum NodeType
{
head,
intermediate
}NodeType;
typedef struct Result
{
NodeType type;
HashPtr *node_ptr; //待返回的节点
HashPtr *node_ptr_2; //待返回节点的前一节点,主要用于删除操作
int flag; // 0= search failure, 1 = search success
} Result;
typedef struct HashTable
{
ElemType *elem; //Use the dynamic programming to allocate the space
int count; //Number of element in the current hashtable
int size_index; //Capacity of hash table
} HashTable;
typedef struct SSTable
{
ElemType *elem;
int len;
} SSTable;
/**
* @brief Create a static table
*
* @param fp File to pointer
* @param st Static table
*/
void create_table(FILE *fp, SSTable *st);
/**
* @brief Intialize the HashPtr as Null pointer
*
* @param hash_chain
* @param m Number of pointer of hash
*/
void init_hash(HashPtr *hash_chain);
/**
* @brief Use hash function to map to the address
* 采用取余的方法
* @param key Key value
* @return int Return hash address
*/
int hash_function(KeyType key);
/**
* @brief search 'key' from the hash table
*
* @param ht Hash table variable
* @param key Key value
* @param p Position of key value
* @param c Number of collision in the search
* @return Result
*/
Result search_hash(HashPtr *hash, KeyType key);
/**
* @brief Insert one element into the hashtable
*
* @param hash Pointer to hash table
* @param key Element type
* @return Status -Return success or unsuccess
*/
Status insert_hash(HashPtr *hash, KeyType key);
/**
* @brief Insert one element into the hashtable
*
* @param hash Pointer to hash table
* @param key Element type
* @return Status -Return success or unsuccess
*/
Status delete_hash(HashPtr *hash, KeyType key);
#endif
4.2 函数实现
/**
* @file hash_table.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-05-23
*
* @copyright Copyright (c) 2023
*
*/
#ifndef HASH_TABLE_C
#define HASH_TABLE_C
#include "hash_table.h"
void create_table(FILE *fp, SSTable *st)
{
int n;
char str[MAX_LEN];
int i;
n=0;
// 当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
while(fgets(str,MAX_LEN,fp)!=NULL)
{
n++;
}
fseek(fp,0,SEEK_SET);
st->len=n;
st->elem=(ElemType *)malloc(sizeof(ElemType)*(n+1));
for(i=1;i<=n;i++)
{
st->elem[i].value=(Record)malloc(sizeof(char)*MAX_LEN);
memset(st->elem[i].value,0,sizeof(char)*MAX_LEN);
fscanf(fp,"%d %s",&(st->elem[i].key),st->elem[i].value);
}
return;
}
void init_hash(HashPtr *hash_chain)
{
int i;
for(i=0;i<m;i++)
{
*(hash_chain+i)=NULL;
}
return;
}
int hash_function(KeyType key)
{
return (key%m);
}
Result search_hash(HashPtr *hash, KeyType key)
{
Result res;
int k;
HashPtr *p;
HashPtr *pre_p;
bool termination=false;
bool found =false;
p = (HashPtr *)malloc(sizeof(HashPtr));
pre_p = (HashPtr *)malloc(sizeof(HashPtr));
k=hash_function(key);
if(hash[k]==NULL || ((hash[k]!=NULL) && key<= hash[k]->key))
{
res.flag=0;
res.type=head;
res.node_ptr=hash+k;
if ((hash[k] != NULL) && key == hash[k]->key)
{
res.flag=1;
}
return res;
}
*p=hash[k];
*pre_p=NULL;
while ((*p) && !termination)
{
if((*p)->key==key)
{
termination = true;
found =true;
}
else if (key < (*p)->key)
{
termination =true;
}
else
{
*pre_p=*p;
*p=(*p)->next;
}
}
if(found)
{
res.flag=1;
res.type=intermediate;
res.node_ptr=p;
res.node_ptr_2=pre_p;
return res;
}
else
{
res.flag = 0;
res.type = intermediate;
res.node_ptr = pre_p;
res.node_ptr_2=p;
return res;
}
}
Status insert_hash(HashPtr *hash, KeyType key)
{
Result res;
HashPtr new_node;
HashPtr temp;
res=search_hash(hash,key);
if(res.flag==1)
{
return ERROR;
}
else
{
new_node=(HashPtr)malloc(sizeof(HashNode));
new_node->key=key;
new_node->next=NULL;
if(res.type==head)
{
new_node->next=*(res.node_ptr);
*(res.node_ptr)=new_node;
}
else
{
new_node->next = (*(res.node_ptr))->next;
(*(res.node_ptr))->next = new_node;
}
}
return OK;
}
Status delete_hash(HashPtr *hash, KeyType key)
{
Result res;
HashPtr temp;
res = search_hash(hash, key);
if (res.flag == 0)
{
return ERROR;
}
else
{
if (res.type == head)
{
(*(res.node_ptr)) = (*(res.node_ptr))->next;
}
else The previous next will be the current next
{
(*(res.node_ptr_2))->next = (*(res.node_ptr))->next;
}
}
}
#endif
4.3测试函数
/**
* @file hash_table_main.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-05-21
*
* @copyright Copyright (c) 2023
*
*/
#ifndef HASH_TABLE_MAIN_C
#define HASH_TABLE_MAIN_C
#include "hash_table.c"
int main(void)
{
int i;
FILE *fp;
SSTable st;
HashPtr hash[m];
fp=fopen("data.txt","r");
create_table(fp,&st);
init_hash(hash);
for(i=1;i<=st.len;i++)
{
insert_hash(hash,st.elem[i].key);
}
delete_hash(hash,14);
printf("This is the end of test\n");
getchar();
fclose(fp);
return EXIT_SUCCESS;
}
#endif
- 小结
本文对哈希表、哈希函数以及处理哈希冲突的方法进行总结,并且利用C语言对实现了简单的哈希插入、查找和删除操作。
参考资料:
《数据结构》清华大学,严蔚敏