数据结构——哈希表
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
哈希表的概念
哈希表是一种高效的查找结构,它利用记录的关键字确定其存储位置,具体表现为把关键字K映射到一个有限的连续的地址集D,这种光系H可以表示为:
H称为哈希函数或散列函数。按哈希函数构建的表称为哈希表。
冲突与同义词
在插入关键字时,可能会遇到该关键字的哈希值与已有关键字哈希值相同的情况,即发生了冲突
哈希函数值相同的关键字称为同义词
在实际情况中,关键字集通常是大于哈希地址集的,哈希冲突是必须要解决的问题。根据一定的哈希函数和处理冲突的方法把关键字集K映射到一个有限的连续的地址集D,并以表的形式记录的过程称为哈希造表或散列,所得存储位置称哈希地址或散列地址
哈希函数的构造方法
哈希函数的构造方法应该注意两个原则:
- 计算过程尽量简单
- 哈希函数尽量均匀。
常见的构造方法有以下这些
- 直接定址法
- 除留余数法
- 数字分析法
- 折叠法
- 平方取中法
直接定址法
最简单的做法是直接用关键字作为哈希地址,也可以通过对线性函数的操作获得地址,如:
除留余数法
可以将关键字求模
%p
不仅可以对关键字直接取模,也可以在折叠、平方取中等运算后再取模。
平方取中法
先取关键字的平方,在选取结果的中间若干位作为哈希地址
处理冲突的方法
链地址法
链地址法是处理冲突较为便利的方法。链地址法将关键字为同义词的记录放在一个带有头指针的单链表中。若哈希表地址区间长度为l,我们就可以将哈希表定义为一个由l个头指针组成的指针数组T,只要是哈希地址同为i,都将其插入T[i]为头指针的单链表中,这样就可以解决冲突。
\
开放定址法
开放定址法是在哈希表的地址空间内解决冲突,实现上会比链地址法复杂不少。插入时若发生冲突,就会利用某种探测方法得到另一个空闲地址,若不冲突,则插入,否则求下一个地址,查找的探测过程与插入相同。
线性探测法
思想:将哈希表看成一个循环的空间,线性探测法的探测地址序列可表示为:
Hi = (H(key)+i) %m 1≤i≤m-1
Hi表示出现冲突时,第i次探测的地址空间
二次探测法
二次探测法的探测不是连续的,而是跳跃的,可以减少记录堆集从而方便下一次的插入。二次探测法的探测地址序列可表示为: Hi = (H(key)+di) %m 1≤i≤m-1 di = 1^2, -1^2, 2^2, -2^2,…, k^2, -k^2 (k≤m/2)
链地址哈希表的实现
-
链地址哈希表的类型定义
-
typedef struct Node { RcdType r; struct Node *next; } Node; typedef struct { Node **rcd; //指针类型,大小动态分配 int size; // 哈希表容量 int count; // 当前表中含有的记录个数 int (*hash)(KeyType key, int hashSize); // 函数指针变量,用于选取的哈希函数 } HashTable;
-
-
链地址哈希表的接口
- Status InitHash(HashTable &H, int size, int (hash)(KeyType,int)); // 初始化哈希表 Status DestroyHash(HashTable &H); // 销毁哈希表 Node SearchHash(HashTable H, KeyType key); // 查找 Status InsertHash(HashTable &H, RcdType e); // 插入 Status deleteHash(HashTable &H, KeyType key, RcdType &e); // 删除
链地址哈希表的初始化
Status InitHash(HashTable &H, int size, int (*hash)(KeyType,int)) { //初始化哈希表
int i;
H.rcd = (Node**)malloc(size*sizeof(Node*));
if(NULL==H.rcd) return OVERFLOW;
for(i=0; i<size; i++) H.rcd[i] = NULL;
H.size = size; H.hash = hash; H.count = 0;
return OK;
}
链地址哈希表的查找操作
int hash(int key, int hashSize) {
return (3*key) % hashSize;
}
Node* SearchHash(HashTable &H, int key) {
int p = H.hash(key, H.size);
Node* np;
for(np=H.rcd[p]; np!=NULL; np=np->next)
if(np->r.key==key) return np;
return NULL;
}
链地址哈希表的插入操作
Status InsertHash(HashTable &H, RcdType e) { // 在哈希表H中插入记录e
int p; Node *np;
if((np=SearchHash(H, e.key))==NULL) { // 查找不成功时插入到表头
p = H.hash(e.key, H.size);
np = (Node*)malloc(sizeof(Node)); if(NULL== np)return OVERFLOW;
np->r = e; np->next = H.rcd[p]; H.rcd[p] = np; H.count++; return OK;
} else return ERROR;
}
开放定址哈希表的实现
-
开放定址哈希表的类型定义
-
typedef struct { RcdType *rcd; // 记录存储基址,动态分配数组 int size; // 哈希表容量 int count; // 当前表中含有的记录个数 int *tag; //标记, 0:空; 1:有效;-1:已删除 int (*hash)(KeyType key, int hashSize); // 函数指针变量,选取的哈希函数 void (*collision)(int &hashValue, int hashSize); // 函数指针变量,用于处理冲突的函数 } HashTable;
-
开放定址哈希表的初始化操作
void collision(int &hashValue, int hashSize) {
hashValue = (hashValue +1)% hashSize;
}
Status InitHash(HashTable &H, int size, int (*hash)(KeyType, int),
void(*collision( int &,int)) {
int i;
H.rcd = (RcdType*)malloc(size*sizeof(RcdType));
H.tag = (int*)malloc(size*sizeof(int));//为记录和标记域分配空间
if(NULL==H.rcd || NULL==H.tag) return OVERFLOW;
for(i=0; i<size; i++) H.tag[i] = 0; // 将哈希表中标记域赋值为空标记
H.size = size; H.count = 0; H.hash = hash; H. collision = collision ;
return OK;
}
标记0 1 -1分别表示空闲、存在、已删除
开放定址哈希表的查找操作
Status SearchHash (HashTable H, KeyType key, int &p, int &c) {
p = H.hash(key, H.size); // 求得哈希地址
while((1==H.tag[p] && H.rcd[p].key!=key) || -1==H.tag[p])) {
H.collision(p, H.size); c++;
} // 求得下一探测地址p
if(H.rcd[p].key == key && 1==H.tag[p] )
return SUCCESS;
else return UNSUCCESS;
}
开放定址哈希表的插入操作
调用查找函数,查找成功,不需插入,返回-1;否则插入并返回冲突次数
int InsertHash(HashTable &H, RcdType e) { // 在哈希表H中插入记录e。
int c=0, j;
if(SUCCESS == SearchHash(H, e.key, j, c)) return -1;
else {
H.rcd[j] = e; //将记录e插入位置j
H.tag[j] = 1; //设置对应的标志位为1
++H.count; //哈希表记录个数加1
return c; //返回查找时发生冲突的次数
}
}
开放定址法删除记录的处理
从哈希表中删除一个记录时,不是直接将其单元置为空闲,而是要打上删除标记,因为空闲单元是查找失败的条件,这样可能会使查找提前结束。
Status deleteHash(HashTable &H, KeyType key, RcdType &e) {
int j, c;
if(UNSUCCESS == SearchHash(H, key, j, c))
return UNSUCCESS; // 表中没有与 key 相同关键字的记录
else {
e = H.rcd[j]; // 被删除的记录
H.tag[j] = -1; // 删除标记
H.count--; //哈希表记录个数减1
return SUCCESS;
}
}