简介
哈希表,也称散列表,是实现字典操作的一种有效的数据结构。尽管最坏情况下,散列表查找一个元素的时间与链表中查找的时间相同,达到了O(n)。然而在实际应用中,散列表查找的性能是极好的。在一些合理的假设下,在散列表中可以查找一个元素的平均时间是O(1)。
哈希表的精髓在于哈希二字上面,也就是数学里面常用到的映射关系。它是通过哈希函数将关键字映射到表中的某个位置上进行存放,以实现快速插入和查询的。
注意,哈希表里一般存放的字典类型数据,即(key, value)的数据,是根据key去存取value。
常用哈希表的构造方法
(一)直接定址法
取关键字或者关键字的某个线性函数值为哈希地址。
比如:H(Key) = key或H(key) = a * key + b,其中a,b为常数
(二)数字分析法
事先知道关键字的集合,且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布比较均匀的若干位,构成哈希地址。
(三)平方取中法
当无法确定关键字中哪几位分布比较均匀时,可以先求出关键字的平方值,然后根据可使用空间的大小,选取平方数的中间几位为哈希表的地址。
比如:关键字:1234,关键字的平方:1522756,哈希函数值:227
(四)折叠法
按哈希表的地址位数将关键字分成位数相等的几部分(最后一部分较短),然后将这几部分相加,舍弃最高位后的结果就是该关键字的哈希地址。
(五)除留余数法
取关键字被某个不大于哈希表长度的数求余,得到的结果作为哈希地址
H(Key) = key % p,其中p小于哈希表长度
(六)随机数法
采用一个伪随机函数作为哈希函数,即 H(key) = random(key)
处理冲突
通过哈希函数去计算哈希值,难免会有冲突的时候,解决冲突的方法有如下几种:
- 开放定址法: 依靠数组中的空位解决碰撞冲突
- 线性探测法:直接检测散列表的下一个位置(即索引值加1),如果仍冲突,继续。
- 二次探测法:即H + 1 2, H + 22, H + 32.。。
- 伪随机探测
- 再哈希法:使用多个哈希函数,第一个冲突时,使用第二个哈希函数,直到不冲突为止
- 链地址法:将所有哈希地址相同的关键字,都链接在同一个链表中。
- 公共溢出法:建立一个公共溢出区,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。
性能分析
哈希法中影响关键字比较次数的因素有三个:哈希函数,处理冲突的方法,哈希表的装填因子。
装填因子 a 的定义如下: a = 哈希表中元素的个数 / 哈希表的长度
a 可描述哈希表的装满程度。a 越小,发生冲突的可能性越小; a 越大 ,发生冲突的可能性越大。
案例
代码
![](https://i-blog.csdnimg.cn/blog_migrate/7a67eaeaf1afb07e418f8a4f5890fa83.png)
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */
#include<malloc.h> /* malloc()等 */
#include<stdio.h> /* EOF(=^Z或F6),NULL */
#include<process.h> /* exit() */
/* 函数结果状态代码 */
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
/* 对两个数值型关键字的比较约定为如下的宏定义 */
#define EQ(a,b) ((a)==(b))
#define LT(a,b) ((a)<(b))
#define LQ(a,b) ((a)<=(b))
#define NULLKEY 0 /* 0为无记录标志 */
#define N 10 /* 数据元素个数 */
typedef int KeyType; /* 设关键字域为整型 */
typedef struct
{
KeyType key;
int ord;
}ElemType; /* 数据元素类型 */
/* ----------------------------- 开放定址哈希表的存储结构 ---------------------------*/
int hashsize[] = { 11,19,29,37 }; /* 哈希表容量递增表,一个合适的素数序列 */
int m = 0; /* 哈希表表长,全局变量 */
typedef struct
{
ElemType *elem; /* 数据元素存储基址,动态分配数组 */
int count; /* 当前数据元素个数 */
int sizeindex; /* hashsize[sizeindex]为当前容量 */
}HashTable;
#define SUCCESS 1
#define UNSUCCESS 0
#define DUPLICATE -1
/* ---------------------------------------------------------------------------------------------*/
/* -------------------------------- 哈希函数的基本操作 -------------------------------*/
Status InitHashTable(HashTable *H)
{ /* 操作结果: 构造一个空的哈希表 */
int i;
(*H).count = 0; /* 当前元素个数为0 */
(*H).sizeindex = 0; /* 初始存储容量为hashsize[0] */
m = hashsize[0];
(*H).elem = (ElemType*)malloc(m * sizeof(ElemType));
if (!(*H).elem)
exit(OVERFLOW); /* 存储分配失败 */
for (i = 0; i < m; i++)
(*H).elem[i].key = NULLKEY; /* 未填记录的标志 */
return OK;
}
void DestroyHashTable(HashTable *H)
{ /* 初始条件: 哈希表H存在。操作结果: 销毁哈希表H */
free((*H).elem);
(*H).elem = NULL;
(*H).count = 0;
(*H).sizeindex = 0;
}
unsigned Hash(KeyType K)
{ /* 一个简单的哈希函数(m为表长,全局变量) */
return K % m;
}
void collision(int *p, int d) /* 线性探测再散列 */
{ /* 开放定址法处理冲突 */
*p = (*p + d) % m;
}
Status SearchHash(HashTable H, KeyType K, int *p, int *c)
{ /* 在开放定址哈希表H中查找关键码为K的元素,若查找成功,以p指示待查数据 */
/* 元素在表中位置,并返回SUCCESS;否则,以p指示插入位置,并返回UNSUCCESS */
/* c用以计冲突次数,其初值置零,供建表插入时参考。算法9.17 */
*p = Hash(K); /* 求得哈希地址 */
while (H.elem[*p].key != NULLKEY && !EQ(K, H.elem[*p].key))
{ /* 该位置中填有记录.并且关键字不相等 */
(*c)++;
if (*c < m) //注意:这里很重要,课本中是没有这个比较限制的,则程序可能在 while 循环中出不去。
collision(p, *c); /* 求得下一探查地址p */
else
break;
}
if EQ(K, H.elem[*p].key)
return SUCCESS; /* 查找成功,p返回待查数据元素位置 */
else
return UNSUCCESS; /* 查找不成功(H.elem[p].key==NULLKEY),p返回的是插入位置 */
}
Status InsertHash(HashTable *, ElemType); /* 对函数的声明 */
void RecreateHashTable(HashTable *H) /* 重建哈希表 */
{ /* 重建哈希表 */
int i, count = (*H).count;
ElemType *p, *elem = (ElemType*)malloc(count * sizeof(ElemType));
p = elem;
printf("重建哈希表\n");
for (i = 0; i < m; i++) /* 保存原有的数据到elem中 */
if (((*H).elem + i)->key != NULLKEY) /* 该单元有数据 */
*p++ = *((*H).elem + i);
(*H).count = 0;
(*H).sizeindex++; /* 增大存储容量 */
m = hashsize[(*H).sizeindex];
p = (ElemType*)realloc((*H).elem, m * sizeof(ElemType));
if (!p)
exit(OVERFLOW); /* 存储分配失败 */
(*H).elem = p;
for (i = 0; i < m; i++)
(*H).elem[i].key = NULLKEY; /* 未填记录的标志(初始化) */
for (p = elem; p < elem + count; p++) /* 将原有的数据按照新的表长插入到重建的哈希表中 */
InsertHash(H, *p);
}
Status InsertHash(HashTable *H, ElemType e)
{ /* 查找不成功时插入数据元素e到开放定址哈希表H中,并返回OK; */
/* 若冲突次数过大,则重建哈希表。算法9.18 */
int c, p;
c = 0;
if (SearchHash(*H, e.key, &p, &c)) /* 表中已有与e有相同关键字的元素 */
return DUPLICATE;
else if (c < hashsize[(*H).sizeindex] / 2) /* 冲突次数c未达到上限,(c的阀值可调) */
{ /* 插入e */
(*H).elem[p] = e;
++(*H).count;
return OK;
}
else
RecreateHashTable(H); /* 重建哈希表 */
return ERROR;
}
void TraverseHash(HashTable H, void(*Vi)(int, ElemType))
{ /* 按哈希地址的顺序遍历哈希表 */
int i;
printf("哈希地址0~%d\n", m - 1);
for (i = 0; i < m; i++)
if (H.elem[i].key != NULLKEY) /* 有数据 */
Vi(i, H.elem[i]);
}
Status Find(HashTable H, KeyType K, int *p)
{ /* 在开放定址哈希表H中查找关键码为K的元素,若查找成功,以p指示待查数据 */
/* 元素在表中位置,并返回SUCCESS;否则,返回UNSUCCESS */
int c = 0;
*p = Hash(K); /* 求得哈希地址 */
while (H.elem[*p].key != NULLKEY && !EQ(K, H.elem[*p].key))
{ /* 该位置中填有记录.并且关键字不相等 */
c++;
if (c < m)
collision(p, c); /* 求得下一探查地址p */
else
return UNSUCCESS; /* 查找不成功(H.elem[p].key==NULLKEY) */
}
if EQ(K, H.elem[*p].key)
return SUCCESS; /* 查找成功,p返回待查数据元素位置 */
else
return UNSUCCESS; /* 查找不成功(H.elem[p].key==NULLKEY) */
}
/* --------------------------------------------------------------------------------------------------*/
void print(int p, ElemType r)
{
printf("address=%d (%d,%d)\n", p, r.key, r.ord);
}
void main()
{
ElemType r[N] = { {17,1},{60,2},{29,3},{38,4},{1,5},{2,6},{3,7},{4,8},{60,9},{13,10} };
HashTable h;
int i, p;
Status j;
KeyType k;
InitHashTable(&h);
for (i = 0; i < N - 1; i++)
{ /* 插入前N-1个记录 */
j = InsertHash(&h, r[i]);
if (j == DUPLICATE)
printf("表中已有关键字为%d的记录,无法再插入记录(%d,%d)\n", r[i].key, r[i].key, r[i].ord);
}
printf("按哈希地址的顺序遍历哈希表:\n");
TraverseHash(h, print);
printf("请输入待查找记录的关键字: ");
scanf("%d", &k);
j = Find(h, k, &p);
if (j == SUCCESS)
print(p, h.elem[p]);
else
printf("没找到\n");
j = InsertHash(&h, r[i]); /* 插入第N个记录 */
if (j == ERROR) /* 重建哈希表 */
j = InsertHash(&h, r[i]); /* 重建哈希表后重新插入第N个记录 */
printf("按哈希地址的顺序遍历重建后的哈希表:\n");
TraverseHash(h, print);
printf("请输入待查找记录的关键字: ");
scanf("%d", &k);
j = Find(h, k, &p);
if (j == SUCCESS)
print(p, h.elem[p]);
else
printf("没找到\n");
DestroyHashTable(&h);
}
运行结果: