1.散列表(Hash)
查找的本质: 已知对象找位置。
有序安排对象:全序、半序
直接“算出”对象位置:散列
时间复杂度几乎是常量:O(1),即查找时间与问题规模无关
散列查找法的两项基本工作:
计算位置:构造散列函数确定关键词存储位置;
解决冲突:应用某种策略解决多个关键词位置相同的问题
散列(Hashing) 的基本思想是:
①以关键字key为自变量,通过一个确定的函数 h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址。
②可能不同的关键字会映射到同一个散列地址上,即h(keyi) = h(keyj)(当keyi ≠keyj),称为“冲突(Collision)”。
----需要某种冲突解决策略
2.散列函数的构造方法
散列函数两个关键:
①计算简单,以便提高转换速度;
②关键词对应的地址空间分布均匀,以尽量减少冲突。
数字关键词的散列函数构造
①直接定址法
取关键词的某个线性函数值为散列地址,即
h(key) = a * key + b (a、b为常数)
②除留余数法
散列函数为:h(key) = key mod p (一般p取素数)
③数字分析法
分析数字关键字在各位上的变化情况,取比较随机的位作为散列地址
Eg:取11位手机号码key的后4位作为地址:
散列函数为:h(key) = atoi(key+7) (char *key)
④折叠法
把关键词分割成位数相同的几个部分,然后叠加
Eg: 56793542
542
793
+ 056
———
1391
h(56793542) = 391
⑤平方取中法
Eg: 56793542
56793542
x 56793542
—————————
3225506412905764
h(56793542) = 641
字符关键词的散列函数构造
①一个简单的散列函数——ASCII码加和法
对字符型关键词key定义散列函数如下:
h(key) = (Σkey[i]) mod TableSize
②简单的改进——前3个字符移位法(易造成空间浪费)
h(key)=(key[0]*27^2 + key[1]*27 + key[2]) mod TableSize
③好的散列函数——移位法
涉及关键词所有n个字符,并且分布得很好:
Eg:h(“abcde”)=‘a’*324+’b’*323+’c’*322+’d’*32+’e’
Index Hash ( const char *Key, int TableSize )
{
unsigned int h = 0; /* 散列函数值,初始化为0 */
while ( *Key != ‘\0’) /* 位移映射 */
h = ( h << 5 ) + *Key++;
return h % TableSize;
}
3.冲突处理方法 及 性能分析
平均查找长度(ASL)用来度量散列表查找效率:成功、不成功
关键词的比较次数,取决于产生冲突的多少
影响产生冲突多少有以下三个因素:
①散列函数是否均匀;
②处理冲突的方法;
③散列表的装填因子α。
开放地址法:换个位置 一旦产生了冲突(该地址已有其它元素),就按某 种规则去寻找另一空地址
①线性探测法(Linear Probing)
线性探测法:以增量序列 1,2,……,(TableSize -1)循环试探下一个存储地址。
成功平均查找长度(ASLs) 查找表中关键词的平均查找比较次数
不成功平均查找长度 (ASLu) 不在散列表中的关键词的平均查找次数
②平方探测法 (Quadratic Probing)--- 二次探测
平方探测法:以增量序列1^2,-1^2,2^2,-2^2,……,q^2,-q^2且q ≤ [TableSize/2](向下取整) 循环试探下一个存储地址。
Eg:平方探测法如何找到未填充空间
定理:如果散列表长度TableSize是某个4k+3(k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间。
③双散列探测法 (Double Hashing)
双散列探测法: di 为i*h2(key),h2(key)是另一个散列函数探测序列成:h2(key),2h2(key),3h2(key),……(对任意的key,h2(key) ≠ 0 )
探测序列还应该保证所有的散列存储单元都应该能够被探测到。选择以下形式有良好的效果:
h2(key) = p - (key mod p) (p < TableSize,p、TableSize都是素数)
④再散列 (Rehashing)
当散列表元素太多(即装填因子 α太大)时,查找效率会下降;
实用最大装填因子一般取 0.5 <= α<= 0.85
当装填因子过大时,解决的方法是加倍扩大散列表,这个过程叫做“再散列(Rehashing)”
链地址法:同一位置的冲突对象组织在一起
分离链接法:将相应位置上冲突的所有关键词存储在同一个单链表中
Eg: h(key) = key mod 11
所有地址链表的平均长度定义成装填因子α,α有可能超过1。
不难证明:其期望探测次数 p为:
4.性能分析
①选择合适的 h(key) ,散列法的查找效率期望是常数O(1),它几乎与关键字的空间的大小n无关!也适合于关键字直接比较计算量大的问题
②它是以较小的α为前提。因此,散列方法是一个以空间换时间
③散列方法的存储对关键字是随机的,不便于顺序查找关键字,也不适合于范围查找,或最大值最小值查找。
开放地址法
优:散列表是一个数组,存储效率高,随机查找。
缺:散列表有“聚集”现象
分离链法
散列表是顺序存储和链式存储的结合,链表部分的存储效率和查找效率都比较低。
优:关键字删除不需要“懒惰删除”法,从而没有存储“垃圾”。
缺:太小的α可能导致空间浪费,大的α又将付出更多的时间代价。不均匀的链表长度导致时间效率的严重下降。
5.
1 //哈希表平方探测法 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <math.h> 5 6 #define MAXTABLESIZE 100000 /* 允许开辟的最大散列表长度 */ 7 typedef int ElementType; /* 关键词类型用整型 */ 8 typedef int Index; /* 散列地址类型 */ 9 typedef Index Position; /* 数据所在位置与散列地址是同一类型 */ 10 /* 散列单元状态类型,分别对应:有合法元素、空单元、有已删除元素 */ 11 typedef enum { Legitimate, Empty, Deleted } EntryType; 12 13 typedef struct HashEntry Cell; /* 散列表单元类型 */ 14 struct HashEntry{ 15 ElementType Data; /* 存放元素 */ 16 EntryType Info; /* 单元状态 */ 17 }; 18 19 typedef struct TblNode *HashTable; /* 散列表类型 */ 20 struct TblNode { /* 散列表结点定义 */ 21 int TableSize; /* 表的最大长度 */ 22 Cell *Cells; /* 存放散列单元数据的数组 */ 23 }; 24 25 /* 返回大于N且不超过MAXTABLESIZE的最小素数 */ 26 int NextPrime( int N ) 27 { 28 int i, p = (N%2)? N+2 : N+1; /*从大于N的下一个奇数开始 */ 29 30 while( p <= MAXTABLESIZE ) { 31 for( i=(int)sqrt(p); i>2; i-- ) 32 if ( !(p%i) ) break; /* p不是素数 */ 33 if ( i==2 ) break; /* for正常结束,说明p是素数 */ 34 else p += 2; /* 否则试探下一个奇数 */ 35 } 36 return p; 37 } 38 39 HashTable CreateTable( int TableSize ) 40 { 41 HashTable H; 42 int i; 43 44 H = (HashTable)malloc(sizeof(struct TblNode)); 45 H->TableSize = NextPrime(TableSize);/* 保证散列表最大长度是素数 */ 46 H->Cells = (Cell *)malloc(H->TableSize*sizeof(Cell));/* 声明单元数组 */ 47 /* 初始化单元状态为 空单元 */ 48 for( i=0; i<H->TableSize; i++ ) 49 H->Cells[i].Info = Empty; 50 51 return H; 52 } 53 54 Position Hash(ElementType Key, int TableSize ) 55 { 56 return Key % TableSize; 57 } 58 59 /*平方探测法1^2,-1^2,2^2,-2^2 …*/ 60 Position Find( HashTable H, ElementType Key ) 61 { 62 Position CurrentPos, NewPos; 63 int CNum = 0; /* 记录冲突次数 */ 64 65 NewPos = CurrentPos = Hash( Key, H->TableSize ); /* 初始散列位置 */ 66 /* 当该位置的单元非空,并且不是要找的元素时,发生冲突 */ 67 while( H->Cells[NewPos].Info!=Empty && H->Cells[NewPos].Data!=Key ) { 68 /* 字符串类型的关键词需要 strcmp 函数!! */ 69 /* 统计1次冲突,并判断奇偶次 */ 70 if( ++CNum%2 ){ /* 奇数次冲突 */ 71 NewPos = CurrentPos + (CNum+1)*(CNum+1)/4; /* 增量为+[(CNum+1)/2]^2 */ 72 if ( NewPos >= H->TableSize ) 73 NewPos = NewPos % H->TableSize; /* 调整为合法地址 */ 74 } 75 else { /* 偶数次冲突 */ 76 NewPos = CurrentPos - CNum*CNum/4; /* 增量为-(CNum/2)^2 */ 77 while( NewPos < 0 ) 78 NewPos += H->TableSize; /* 调整为合法地址 */ 79 } 80 } 81 return NewPos; /* 此时NewPos或者是Key的位置,或者是一个空单元的位置(表示找不到)*/ 82 } 83 84 bool Insert( HashTable H, ElementType Key ) 85 { 86 Position Pos = Find( H, Key ); /* 先检查Key是否已经存在 */ 87 if( H->Cells[Pos].Info != Legitimate ) { /* 如果这个单元没有被占,说明Key可以插入在此 */ 88 H->Cells[Pos].Info = Legitimate; 89 H->Cells[Pos].Data = Key; 90 /*字符串类型的关键词需要 strcpy 函数!! */ 91 return true; 92 } 93 else { 94 printf("键值已存在"); 95 return false; 96 } 97 } 98 99 int main() 100 { 101 HashTable hash; 102 hash = CreateTable(5); //real size 0 1 2 3 4 5 6 103 printf("size = %d\n",hash->TableSize); 104 Insert(hash,1); 105 Insert(hash,5); 106 Insert(hash,6); 107 Insert(hash,7); 108 Insert(hash,8); 109 Insert(hash,9); 110 Insert(hash,10); 111 return 0; 112 }
1 //分离链接法 2 #include <iostream> 3 #include <cstdio> 4 #include <cstdlib> 5 #include <cstring> 6 #include <math.h> 7 using namespace std; 8 9 #define MAXTABLESIZE 100000 /* 允许开辟的最大散列表长度 */ 10 #define KEYLENGTH 15 /* 关键词字符串的最大长度 */ 11 typedef char ElementType[KEYLENGTH+1]; /* 关键词类型用字符串 */ 12 typedef int Index; /* 散列地址类型 */ 13 14 /******** 以下是单链表的定义 ********/ 15 typedef struct LNode *PtrToLNode; 16 struct LNode { 17 ElementType Data; 18 PtrToLNode Next; 19 }; 20 typedef PtrToLNode Position; 21 typedef PtrToLNode List; 22 /******** 以上是单链表的定义 ********/ 23 24 typedef struct TblNode *HashTable; /* 散列表类型 */ 25 struct TblNode { /* 散列表结点定义 */ 26 int TableSize; /* 表的最大长度 */ 27 List Heads; /* 指向链表头结点的数组 */ 28 }; 29 30 int NextPrime( int N ) 31 { /* 返回大于N且不超过MAXTABLESIZE的最小素数 */ 32 int i, p = (N%2)? N+2 : N+1; /*从大于N的下一个奇数开始 */ 33 34 while( p <= MAXTABLESIZE ) { 35 for( i=(int)sqrt(p); i>2; i-- ) 36 if ( !(p%i) ) break; /* p不是素数 */ 37 if ( i==2 ) break; /* for正常结束,说明p是素数 */ 38 else p += 2; /* 否则试探下一个奇数 */ 39 } 40 return p; 41 } 42 43 HashTable CreateTable( int TableSize ) 44 { 45 HashTable H; 46 int i; 47 48 H = (HashTable)malloc(sizeof(struct TblNode)); 49 H->TableSize = NextPrime(TableSize);/* 保证散列表最大长度是素数 */ 50 H->Heads = (List)malloc(H->TableSize*sizeof(struct LNode));/* 以下分配链表头结点数组 */ 51 52 /* 初始化表头结点 */ 53 for( i=0; i<H->TableSize; i++ ) { 54 H->Heads[i].Data[0] = '\0'; 55 H->Heads[i].Next = NULL; 56 } 57 58 return H; 59 } 60 61 Index Hash(ElementType Key, int TableSize ) 62 { 63 64 return (*Key - 'a') % TableSize; 65 } 66 67 68 Position Find( HashTable H, ElementType Key ) 69 { 70 Position P; 71 Index Pos; 72 73 Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */ 74 P = H->Heads[Pos].Next; /* 从该链表的第1个结点开始 */ 75 /* 当未到表尾,并且Key未找到时 */ 76 while( P && strcmp(P->Data, Key) ) 77 P = P->Next; 78 79 return P; /* 此时P或者指向找到的结点,或者为NULL */ 80 } 81 82 bool Insert( HashTable H, ElementType Key ) 83 { 84 Position P, NewCell; 85 Index Pos; 86 87 P = Find( H, Key ); 88 if ( !P ) { /* 关键词未找到,可以插入 */ 89 NewCell = (Position)malloc(sizeof(struct LNode)); 90 strcpy(NewCell->Data, Key); 91 Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */ 92 /* 将NewCell插入为H->Heads[Pos]链表的第1个结点 */ 93 NewCell->Next = H->Heads[Pos].Next; 94 H->Heads[Pos].Next = NewCell; 95 return true; 96 } 97 else { /* 关键词已存在 */ 98 printf("键值已存在"); 99 return false; 100 } 101 } 102 103 void DestroyTable( HashTable H ) 104 { 105 int i; 106 Position P, Tmp; 107 108 /* 释放每个链表的结点 */ 109 for( i=0; i<H->TableSize; i++ ) { 110 P = H->Heads[i].Next; 111 while( P ) { 112 Tmp = P->Next; 113 free( P ); 114 P = Tmp; 115 } 116 } 117 free( H->Heads ); /* 释放头结点数组 */ 118 free( H ); /* 释放散列表结点 */ 119 } 120 121 int main() 122 { 123 HashTable hash; 124 hash = CreateTable(5); //real size 7: 0 1 2 3 4 5 6 125 Insert( hash, "a" ); 126 Insert( hash, "b" ); 127 Insert( hash, "c" ); 128 Insert( hash, "d" ); 129 Insert( hash, "e" ); 130 Insert( hash, "h" ); 131 Insert( hash, "g" ); 132 return 0; 133 }