查找 之 散列表查找(哈希表)
基础概念
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).这里对应关系f称为散列函数,又称为哈希(Hash)函数。
采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。
散列技术既是一种存储方法,也是一种查找方法。
散列技术最适合的求解问题是查找与给定值相等的记录。不适合一对多的查找,也不适合范围查找。
散列技术中的两个关键问题:
散列查找中,时常会碰到两个关键字key1!=key2,但是却有f(key1)==f(key2),这种现象称为冲突(collision),并把key1和key2称为这个散列函数的同义词(synonym)。
几种散列函数的构造方法
直接定址法
取关键字的某个线性函数值为散列地址,即
f(key)=a×key+b (a,b为常数)
优点:简单、均匀、不会产生冲突;
确定:需事先知道关键字的分布情况,适合查找表比较小且连续的情况。
不常用。
数字分析法
抽取关键字的一部分来计算散列位置的方法,也可以对抽取出来的数字再进行反转,循环移位,甚至前两数与后两数叠加等方法。适合处理关键字比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。
平方取中法
例如:关键字1234,其平方值为1522756,取其中间三位就是227,用作散列地址。
平方取中适用于不知道关键字的分布,而位数又不是很大的情况。
折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
例如:关键字9876543210,散列表表长为三位,我们将它分为四组,987|654|321|0,然后将他们叠加求和987+654+321+0=1962,再求后3位得到散列地址为962.
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
除留余数法
此法为最常用的构造散列函数方法,对于散列表长为m的散列函数公式为:
f(key) = key mod p (p<=m)
根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
随机数法
取关键字的随机函数值为它的散列地址,f(key)=random(key).
当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
设计散列函数需要考虑的因素:
处理散列冲突的方法
开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入.公式:
fi(key)=(f(key)+di) MOD m (di=1,2,3,……,m-1)
这种解决冲突的开放定址法称为线性探测法。
当表中第i,i+1,i+2位置上已填有记录时,下一哈希地址为i,i+1,i+2和i+3的记录都将填入i+3的位置。这种第一个哈希地址不同的记录争夺同一个后继哈希地址的现象成为“聚集”(堆积)。 堆积使得需要不断处理冲突。可以改进di=1,-1,4,-4,9,-9,……,q*q,-q*q,(q<=m/2);正负号相间等于是可以双向寻找可能的空位置,增加平方运算的目的是为了不让关键字都聚集在某一块区域,这种方法称为二次探测法。
另外,在冲突时,对于位移量di可以采用伪随机函数计算得到,这种方法称之为随即探测法。
再散列函数法
事先准备多个散列函数
fi(key) = RHi(key) (i=1,2,…k)
这里RHi就是不同的散列函数,每当发生散列地址冲突时,就换一个散列函数计算,相信总会有一个可以把冲突解决掉。
链地址法
将所有关键字为同义词的记录存储在一个单链表中,这种表称为同义词子表,在散列表中只存储所有同义词子表的头指针。
特点
(1)不产生“聚集”现象,故ASL较短;
(2)结点空间动态申请,适合于表前无法确定表长的情况;
(3)删除结点的操作易于实现;
(4)α=n/m较大时,所用空间比开放地址法多。但α越大,
开放地址法所需探查次数越多。
公共溢出区法
将所有冲突的关键字发那个在一个公共的溢出区来存放,当散列查找不成功时,则到溢出表去进行顺序查找。
散列表查找算法实现
/*代码源自《大话数据结构》一书*/ #include "stdio.h" #include "stdlib.h" #include "io.h" #include "math.h" #include "time.h" #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 100 /* 存储空间初始分配量 */ #define SUCCESS 1 #define UNSUCCESS 0 #define HASHSIZE 12 /* 定义散列表长为数组的长度 */ #define NULLKEY -32768 typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */ typedef struct { int *elem; /* 数据元素存储基址,动态分配数组 */ int count; /* 当前数据元素个数 */ }HashTable; int m=0; /* 散列表表长,全局变量 */ /* 初始化散列表 */ Status InitHashTable(HashTable *H) { int i; m=HASHSIZE; H->count=m; H->elem=(int *)malloc(m*sizeof(int)); for(i=0;i<m;i++) H->elem[i]=NULLKEY; return OK; } /* 散列函数 */ int Hash(int key) { return key % m; /* 除留余数法 */ } /* 插入关键字进散列表 */ void InsertHash(HashTable *H,int key) { int addr = Hash(key); /* 求散列地址 */ while (H->elem[addr] != NULLKEY) /* 如果不为空,则冲突 */ { addr = (addr+1) % m; /* 开放定址法的线性探测 */ } H->elem[addr] = key; /* 直到有空位后插入关键字 */ } /* 散列表查找关键字 */ Status SearchHash(HashTable H,int key,int *addr) { *addr = Hash(key); /* 求散列地址 */ while(H.elem[*addr] != key) /* 如果不为空,则冲突 */ { *addr = (*addr+1) % m; /* 开放定址法的线性探测 */ if (H.elem[*addr] == NULLKEY || *addr == Hash(key)) /* 如果循环回到原点 */ return UNSUCCESS; /* 则说明关键字不存在 */ } return SUCCESS; } int main() { int arr[HASHSIZE]={12,67,56,16,25,37,22,29,15,47,48,34}; int i,p,key,result; HashTable H; key=39; InitHashTable(&H); for(i=0;i<m;i++) InsertHash(&H,arr[i]); result=SearchHash(H,key,&p); if (result) printf("查找 %d 的地址为:%d \n",key,p); else printf("查找 %d 失败。\n",key); for(i=0;i<m;i++) { key=arr[i]; SearchHash(H,key,&p); printf("查找 %d 的地址为:%d \n",key,p); } return 0; }
散列表查找性能分析
如果没有冲突,散列查找的时间复杂度为O(1).
影响散列查找的平均查找长度因素
1.散列函数是否均匀
2.处理冲突的方法
3.散列表的装填因子
所谓装填因子α=填入表中的记录个数/散列表长度。α越大,产生冲突的可能性就越大。