一、哈希表相关概念
1、哈希函数的基本概念
哈希表又称散列表。
哈希表存储的基本思想是:以数据表中的每个记录的关键字 k为自变量,通过一种函数H(k)计算出函数值。把这个值解释为一块连续存储空间(即数组空间)的单元地址(即下标),将该记录存储到这个单元中。在此称该函数H为哈希函数或散列函数。按这种方法建立的表称为哈希表或散列表。
理想情况下,哈希函数在关键字和地址之间建立了一个一一对应关系,从而使得查找只需一次计算即可完成。由于关键字值的某种随机性,使得这种一一对应关系难以发现或构造。因而可能会出现不同的关键字对应一个存储地址。即k1≠k2,但H(k1)=H(k2),这种现象称为冲突。把这种具有不同关键字值而具有相同哈希地址的对象称“同义词”。
在大多数情况下,冲突是不能完全避免的。这是因为所有可能的关键字的集合可能比较大,而对应的地址数则可能比较少。
对于哈希技术,主要研究两个问题:
(1)如何设计哈希函数以使冲突尽可能少地发生。
(2)发生冲突后如何解决。
2、哈希函数的构造方法
常见的构造方法有很多种,如直接定址法,数字分析法,平方取中法等。接下来,我们介绍其中的几种:
(1)除留余数法
取关键字k被某个不大于表长m的数p除后所得余数作为哈希函数地址的方法。即:
H(k)=k mod p
这种方法的关键是选择好p。使得数据集合中的每一个关键字通过该函数转化后映射到哈希表的任意地址上的概率相等。理论研究表明,一般取p为小于m的最大质数或不包含小于20的质因素的合数。
(2)平方取中法
先将关键字平方,然后取其中间几位作为散列地址。所取位数由地址空间范围决定。若地址空间小于所取位数值决定的范围,可通过乘以一比例因子来解决。
(3)折叠法
把关键字分割成位数相等(最后一部分的位数可以不同)的几部分,然后通过折叠后将几部分进行相加,丢掉进位位,所得值即为散列地址。散列的位数由地址空间的位数而定。
分割方法:从右至左
相加方法有两种:
移位叠加:将分割后的各部分低位对齐相加。
界间叠加:将某些部分倒置后再相加。相当于把关键字看成一张纸,从一端向另一端沿间界逐层折叠,再把相应位数相加。
3、哈希函数的冲突检测方法
假设哈希表的地址范围为0~m-l,当对给定的关键字k,由哈希函数H(k)算出的哈希地址为i(0≤i≤m-1)的位置上已存有记录,这种情况就是冲突现象。
处理冲突就是为该关键字的记录找到另一个“空”的哈希地址。即通过一个新的哈希函数得到一个新的哈希地址。如果仍然发生冲突,则再求下一个,依次类推。直至新的哈希地址不再发生冲突为止。
常用的处理冲突的方法有开放地址法、链地址法等几类。
(1)开放地址法
当发生冲突时,将依次探测“下一个位置”,直到找到其关键字相匹配的元素或找到一个空位插入。设哈希空间长度为m,“下一个位置”由下式确定:
Hi=(H(key)+di) mod m
H(key):哈希函数
m:哈希表长度
di:求“下一个位置”的增量
di的确定方法
a) 线性探测再散列
di=1,2,…,m-1。
这种di的取法称为线性探测再散列。即“下一个位置”为哈希表的直接后继。若当di=m-1时仍未查到,则说明表满,还要查找另外的溢出表。缺点:容易产生“二次聚集”
b)二次探测再散列
di=12,-12,22,-22,…,±k2 (k≤m/2)
c)伪随机探测再散列
di由一个伪随机函数发生器产生的一个伪随机数序列来确定。
(2)链地址法
将所有关键字为同义词的记录存储在同一链表中。设哈希地址在区间[0..m-1]上,设置一个指针向量:
Chain chainhash[m];
每个分量的初始状态为空,凡哈希地址为i的的记录则插入到chainhash[i]的链表中。插入的位置可以在表头、表尾,也可在中间。为了查找的方便,可以使同一链表中记录的关键字有序。如
K={19,14,23,01,68,20,84,27,55,11,10,79}
H(key)=key mod 13,存储链表如图中所示:
二、哈希表C语言描述
三、哈希表C语言实现
1 #include "stdio.h" 2 3 #include "stdlib.h" 4 5 #define SUCCESS 1 6 7 #define UNSUCCESS 0 8 9 #define DUPLICATE -1 10 11 #define OK 1 12 13 #define ERROR -1 14 15 #define EQ(a,b) ((a)==(b)) 16 17 #define LT(a,b) ((a)< (b)) 18 19 #define LQ(a,b) ((a)<=(b)) 20 21 #define BT(a,b) ((a)> (b)) 22 23 #define NULLKEY -111 24 25 int hashsize[]={11,19,29,37}; // 哈希表容量递增表, 26 27 //一个合适的素数序列 28 29 int m=0; // 哈希表表长,全局变量 30 31 typedef int KeyType; 32 33 typedef int info; 34 35 typedef struct 36 37 { 38 39 KeyType key; 40 41 //info otherinfo; 42 43 }ElemType; 44 45 typedef struct 46 47 { 48 49 ElemType *elem; 50 51 int count; 52 53 int sizeindex; 54 55 }HashTable; 56 57 58 59 int InitHashTable(HashTable &H) 60 61 { // 操作结果: 构造一个空的哈希表 62 63 int i; 64 65 H.count=0; // 当前元素个数为0 66 67 H.sizeindex=0; // 初始存储容量为hashsize[0] 68 69 m=hashsize[0]; 70 71 H.elem=(ElemType*)malloc(m*sizeof(ElemType)); 72 73 if(!H.elem) 74 75 exit(0); // 存储分配失败 76 77 for(i=0;i<m;i++) 78 79 H.elem[i].key=NULLKEY; // 未填记录的标志 80 81 return OK; 82 83 } 84 85 void DestroyHashTable(HashTable &H) 86 87 { // 初始条件: 哈希表H存在。操作结果: 销毁哈希表H 88 89 free(H.elem); 90 91 H.elem=NULL; 92 93 H.count=0; 94 95 H.sizeindex=0; 96 97 }//DestroyHashTable 98 99 int Hash(KeyType K) 100 101 { // 一个简单的哈希函数(m为表长,全局变量) 102 103 //除留余数法 104 105 return K%m; 106 107 }//Hash 108 109 void collision(int &p,int d) // 线性探测再散列 110 111 { // 开放定址法处理冲突 112 113 p=(p+d)%m; 114 115 }//collision 116 117 118 119 int SearchHash(HashTable H,KeyType K,int &p,int &c) 120 121 { 122 123 p=Hash(K); //构造哈希函数 124 125 while(H.elem[p].key!=NULLKEY&&!EQ(K,H.elem[p].key)) 126 127 { 128 129 collision(p,++c); //冲突检测 130 131 if(c>=m) break; 132 133 } 134 135 if(EQ(K,H.elem[p].key)) 136 137 return SUCCESS; 138 139 else return UNSUCCESS; 140 141 }//SearchHash 142 143 int InsertHash(HashTable &H,ElemType e); 144 145 void RecreateHashTable(HashTable &H) // 重建哈希表 146 147 { // 重建哈希表 148 149 int i,count=H.count; 150 151 ElemType *p,*elem=(ElemType*)malloc(count*sizeof(ElemType)); 152 153 p=elem; 154 155 printf("重建哈希表\n"); 156 157 for(i=0;i<m;i++) // 保存原有的数据到elem中 158 159 if((H.elem+i)->key!=NULLKEY) // 该单元有数据 160 161 *p++=*(H.elem+i); 162 163 H.count=0; 164 165 H.sizeindex++; // 增大存储容量 166 167 m=hashsize[H.sizeindex]; 168 169 p=(ElemType*)realloc(H.elem,m*sizeof(ElemType)); 170 171 if(!p) 172 173 exit(-1); // 存储分配失败 174 175 H.elem=p; 176 177 for(i=0;i<m;i++) 178 179 H.elem[i].key=NULLKEY; // 未填记录的标志(初始化) 180 181 for(p=elem;p<elem+count;p++) // 将原有的数据按照新的表长插入到重建的哈希表中 182 183 InsertHash(H,*p); 184 185 }//RecreateHashTable 186 187 188 189 int InsertHash(HashTable &H,ElemType e) 190 191 { // 查找不成功时插入数据元素e到开放定址哈希表H中,并返回OK; 192 193 // 若冲突次数过大,则重建哈希表 194 195 int c,p; 196 197 c=0; 198 199 if(SearchHash(H,e.key,p,c)) // 表中已有与e有相同关键字的元素 200 201 return DUPLICATE; 202 203 else if(c<hashsize[H.sizeindex]/2) // 冲突次数c未达到上限,(c的阀值可调) 204 205 { // 插入e 206 207 H.elem[p]=e; 208 209 ++H.count; 210 211 return OK; 212 213 } 214 215 else 216 217 RecreateHashTable(H); // 重建哈希表 218 219 return ERROR; 220 221 } 222 223 int InsertHashD(HashTable &H) 224 225 { 226 227 ElemType e; 228 229 printf("input the data until -1\n"); 230 231 scanf("%d",&e.key); 232 233 while(e.key!=-1) 234 235 { 236 237 InsertHash(H,e); 238 239 printf("input the data until -1\n"); 240 241 scanf("%d",&e.key); 242 243 }//while 244 245 return 1; 246 247 }//InsertHashD 248 249 int SearchHashD(HashTable &H) 250 251 { 252 253 KeyType key; 254 255 int p=0,c=0; 256 257 printf("input the data you want to search:\n"); 258 259 scanf("%d",&key); 260 261 if(SearchHash(H,key,p,c)) 262 263 printf("the location is %d,%d\n",p,H.elem[p].key); 264 265 else printf("Search Failed!\n"); 266 267 return 1; 268 269 }//SearchHashD 270 271 void print(int p,ElemType r) 272 273 { 274 275 printf("address=%d (%d)\n",p,r.key); 276 277 }//print 278 279 void TraverseHash(HashTable H,void(*Vi)(int,ElemType)) 280 281 { // 按哈希地址的顺序遍历哈希表 282 283 printf("哈希地址0~%d\n",m-1); 284 285 for(int i=0;i<m;i++) 286 287 if(H.elem[i].key!=NULLKEY) // 有数据 288 289 Vi(i,H.elem[i]); 290 291 }//TraverseHash 292 293 void TraverseHashD(HashTable &H) 294 295 { 296 297 TraverseHash(H,print); 298 299 }//TraverseHashD 300 301 int main() 302 303 { 304 305 HashTable H; 306 307 InitHashTable(H); 308 309 InsertHashD(H); 310 311 SearchHashD(H); 312 313 TraverseHashD(H); 314 315 DestroyHashTable(H); 316 317 return 1; 318 319 }
四、复杂度分析
从哈希表的查找过程可见:
1、虽然哈希表在关键字与记录的存储位置之间建立了直接映象,但由于冲突的产生,使得哈希表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需以平均查找长度作为衡量哈希表的查找效率的度量。
2、查找过程中需与给定值进行比较的关键字的个数取决于下面三种因素:
哈希函数
处理冲突的方法
哈希表的装填因子
哈希函数的好坏首先影响出现冲突的频繁程度
假定哈希函数是“均匀的”,即不同的哈希函数对同一组随机的关键字,产生冲突的可能性相同。
对同一组关键字,设定相同的哈希函数,则不同的处理冲突的方法得到的哈希表不同,它的平均查找长度也不同。
若处理冲突的方法相同,其平均查找长度依赖于哈希表的装填因子。
冲突的多少与表的填满程度有关,填满程度用α表示:
α=表中记录数/哈希表的长度
α标志哈希表的装满程度。
α越小,发生冲突的可能性越小,反之,α越大,表中已填入的记录越多,再填记录时,发生冲突的可能性越大。查找时,给定值需与之进行比较的关键字个数就越多,检索越慢。