layout: post
title: 散列查找(哈希表)
date: 2017-05-20
tag: 數據結構和算法
目錄
- TOC
{: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)--- 二次探測
- 平方探測法:以增量序列1^2,-1^2,2^2,-2^2,……,q^2,-q^2且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;
}
分離鏈接法