散列查找(哈希表)
目录
- TOC
散列表
- 现有的查找算法,对数据量特别大的时候不适用
- 填装因子(Loading Factor):设散列表空间大小为m,填入表中元素个数为n,则a=n/m为散列表的填装因子。
- 散列(Hashing) 的基本思想是:
①以关键字key为自变量,通过一个确定的函数 h(散列函数),计算出对应的函数值h(key),作为数据对象的存储地址。
②可能不同的关键字会映射到同一个散列地址上,即h(keyi) = h(keyj)(当keyi ≠keyj),称为“冲突(Collision)”----需要某种冲突解决策略
散列函数的构造方法
- 散列函数两个关键:
①计算简单,以便提高转换速度;
②关键词对应的地址空间分布均匀,以尽量减少冲突。
数字关键字的散列函数构造
-
①直接定址法:取关键词的某个线性函数值为散列地址,即
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
字符关键词的散列函数构造
- 举例:
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;
}
处理冲突的方法
- 换个位置:开放地址法
- 同一位置的冲突对象组织在一起:链地址法
开放地址法
- 在开放地址散列表中,删除操作要小心。通常只能“懒惰删除”,即需要增加一个“删除标记(Deleted)”,而并不是真正删除它,以便查找事不会“断链”,其空间可以再下次插入时重用。
线性探测法
-
以增量序列 1,2,……,(TableSize -1)循环试探下一个存储地址。
-
性能分析
平方探测法 (Quadratic Probing)--- 二次探测
- 平方探测法:以增量序列12,-12,22,-22,……,q2,-q2且q ≤ TableSize/2 循环试探下一个存储地址。
- 定理:如果散列表长度TableSize是某个4k+3(k是正整数)形式的素数时,平方探测法就可以探查到整个散列表空间
//哈希表平方探测法
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define MAXTABLESIZE 100000 /* 允许开辟的最大散列表长度 */
typedef int ElementType; /* 关键词类型用整型 */
typedef int Index; /* 散列地址类型 */
typedef Index Position; /* 数据所在位置与散列地址是同一类型 */
/* 散列单元状态类型,分别对应:有合法元素、空单元、有已删除元素 */
typedef enum { Legitimate, Empty, Deleted } EntryType;
typedef struct HashEntry Cell; /* 散列表单元类型 */
struct HashEntry{
ElementType Data; /* 存放元素 */
EntryType Info; /* 单元状态 */
};
typedef struct TblNode *HashTable; /* 散列表类型 */
struct TblNode { /* 散列表结点定义 */
int TableSize; /* 表的最大长度 */
Cell *Cells; /* 存放散列单元数据的数组 */
};
/* 返回大于N且不超过MAXTABLESIZE的最小素数 */
int NextPrime( int N )
{
int i, p = (N%2)? N+2 : N+1; /*从大于N的下一个奇数开始 */
while( p <= MAXTABLESIZE ) {
for( i=(int)sqrt(p); i>2; i-- )
if ( !(p%i) ) break; /* p不是素数 */
if ( i==2 ) break; /* for正常结束,说明p是素数 */
else p += 2; /* 否则试探下一个奇数 */
}
return p;
}
HashTable CreateTable( int TableSize )
{
HashTable H;
int i;
H = (HashTable)malloc(sizeof(struct TblNode));
H->TableSize = NextPrime(TableSize);/* 保证散列表最大长度是素数 */
H->Cells = (Cell *)malloc(H->TableSize*sizeof(Cell));/* 声明单元数组 */
/* 初始化单元状态为 空单元 */
for( i=0; i<H->TableSize; i++ )
H->Cells[i].Info = Empty;
return H;
}
Position Hash(ElementType Key, int TableSize )
{
return Key % TableSize;
}
/*平方探测法1^2,-1^2,2^2,-2^2 …*/
Position Find( HashTable H, ElementType Key )
{
Position CurrentPos, NewPos;
int CNum = 0; /* 记录冲突次数 */
NewPos = CurrentPos = Hash( Key, H->TableSize ); /* 初始散列位置 */
/* 当该位置的单元非空,并且不是要找的元素时,发生冲突 */
while( H->Cells[NewPos].Info!=Empty && H->Cells[NewPos].Data!=Key ) {
/* 字符串类型的关键词需要 strcmp 函数!! */
/* 统计1次冲突,并判断奇偶次 */
if( ++CNum%2 ){ /* 奇数次冲突 */
NewPos = CurrentPos + (CNum+1)*(CNum+1)/4; /* 增量为+[(CNum+1)/2]^2 */
if ( NewPos >= H->TableSize )
NewPos = NewPos % H->TableSize; /* 调整为合法地址 */
}
else { /* 偶数次冲突 */
NewPos = CurrentPos - CNum*CNum/4; /* 增量为-(CNum/2)^2 */
while( NewPos < 0 )
NewPos += H->TableSize; /* 调整为合法地址 */
}
}
return NewPos; /* 此时NewPos或者是Key的位置,或者是一个空单元的位置(表示找不到)*/
}
bool Insert( HashTable H, ElementType Key )
{
Position Pos = Find( H, Key ); /* 先检查Key是否已经存在 */
if( H->Cells[Pos].Info != Legitimate ) { /* 如果这个单元没有被占,说明Key可以插入在此 */
H->Cells[Pos].Info = Legitimate;
H->Cells[Pos].Data = Key;
/*字符串类型的关键词需要 strcpy 函数!! */
return true;
}
else {
printf("键值已存在");
return false;
}
}
int main()
{
HashTable hash;
hash = CreateTable(5); //real size 0 1 2 3 4 5 6
printf("size = %d\n",hash->TableSize);
Insert(hash,1);
Insert(hash,5);
Insert(hash,6);
Insert(hash,7);
Insert(hash,8);
Insert(hash,9);
Insert(hash,10);
return 0;
}
哈希表平方探测法
双散列探测法 (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)”
分离链接法
- 同一位置的冲突对象组织在一起
- 分离链接法:将相应位置上冲突的所有关键词存储在同一个单链表中
- 所有地址链表的平均长度定义成装填因子α,α有可能超过1。
//分离链接法
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <math.h>
using namespace std;
#define MAXTABLESIZE 100000 /* 允许开辟的最大散列表长度 */
#define KEYLENGTH 15 /* 关键词字符串的最大长度 */
typedef char ElementType[KEYLENGTH+1]; /* 关键词类型用字符串 */
typedef int Index; /* 散列地址类型 */
/******** 以下是单链表的定义 ********/
typedef struct LNode *PtrToLNode;
struct LNode {
ElementType Data;
PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;
/******** 以上是单链表的定义 ********/
typedef struct TblNode *HashTable; /* 散列表类型 */
struct TblNode { /* 散列表结点定义 */
int TableSize; /* 表的最大长度 */
List Heads; /* 指向链表头结点的数组 */
};
int NextPrime( int N )
{ /* 返回大于N且不超过MAXTABLESIZE的最小素数 */
int i, p = (N%2)? N+2 : N+1; /*从大于N的下一个奇数开始 */
while( p <= MAXTABLESIZE ) {
for( i=(int)sqrt(p); i>2; i-- )
if ( !(p%i) ) break; /* p不是素数 */
if ( i==2 ) break; /* for正常结束,说明p是素数 */
else p += 2; /* 否则试探下一个奇数 */
}
return p;
}
HashTable CreateTable( int TableSize )
{
HashTable H;
int i;
H = (HashTable)malloc(sizeof(struct TblNode));
H->TableSize = NextPrime(TableSize);/* 保证散列表最大长度是素数 */
H->Heads = (List)malloc(H->TableSize*sizeof(struct LNode));/* 以下分配链表头结点数组 */
/* 初始化表头结点 */
for( i=0; i<H->TableSize; i++ ) {
H->Heads[i].Data[0] = '\0';
H->Heads[i].Next = NULL;
}
return H;
}
Index Hash(ElementType Key, int TableSize )
{
return (*Key - 'a') % TableSize;
}
Position Find( HashTable H, ElementType Key )
{
Position P;
Index Pos;
Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */
P = H->Heads[Pos].Next; /* 从该链表的第1个结点开始 */
/* 当未到表尾,并且Key未找到时 */
while( P && strcmp(P->Data, Key) )
P = P->Next;
return P; /* 此时P或者指向找到的结点,或者为NULL */
}
bool Insert( HashTable H, ElementType Key )
{
Position P, NewCell;
Index Pos;
P = Find( H, Key );
if ( !P ) { /* 关键词未找到,可以插入 */
NewCell = (Position)malloc(sizeof(struct LNode));
strcpy(NewCell->Data, Key);
Pos = Hash( Key, H->TableSize ); /* 初始散列位置 */
/* 将NewCell插入为H->Heads[Pos]链表的第1个结点 */
NewCell->Next = H->Heads[Pos].Next;
H->Heads[Pos].Next = NewCell;
return true;
}
else { /* 关键词已存在 */
printf("键值已存在");
return false;
}
}
void DestroyTable( HashTable H )
{
int i;
Position P, Tmp;
/* 释放每个链表的结点 */
for( i=0; i<H->TableSize; i++ ) {
P = H->Heads[i].Next;
while( P ) {
Tmp = P->Next;
free( P );
P = Tmp;
}
}
free( H->Heads ); /* 释放头结点数组 */
free( H ); /* 释放散列表结点 */
}
int main()
{
HashTable hash;
hash = CreateTable(5); //real size 7: 0 1 2 3 4 5 6
Insert( hash, "a" );
Insert( hash, "b" );
Insert( hash, "c" );
Insert( hash, "d" );
Insert( hash, "e" );
Insert( hash, "h" );
Insert( hash, "g" );
return 0;
}
分离链接法
Reference
C/C++基本语法学习
STL
C++ primer