1、基本概念
散列(hash)同順序、鏈接和索引一樣,是存儲數據的又一種方法。
散列存儲的基本思想是:以數據(通常爲集合)中的每個元素的關鍵字K爲自變量,通過一種函數h(K)計算出函數值,
把這個值解釋爲一塊連續存儲空間(即數組空間或文件空間)的單元地址(即下標),將該元素存儲到這個單元中。
散列存儲中使用的函數h(K),稱爲散列函數或哈希函數,它實現關鍵字到存儲地址的映射(或稱轉換),h(K)的值稱爲散列地址或哈希地址;
使用的數組空間是數據進行散列存儲的地址空間,所以被稱之爲散列表或哈希表(hash list或hash table)。
在散列表上進行查找時,首先根據給定的關鍵字K,用與散列存儲時使用的同一散列函數h(K)計算出散列地址,然後按此地址從散列表中取出對應的元素。
假定一個集合爲
S={18,75,60,43,54,90,46}
其中每個整數可以是元素本身,也可以僅是元素的關鍵字,用以代表整個元素。爲了散列存儲該集合,假定選取的散列函數爲:
h(K)=K % m
即用元素的關鍵字K整除以散列表的長度m,取餘數(即爲0至m-1範圍內的一個數)作爲存儲該元素的散列地址,
這裏假定K和m均爲正整數,並且m要大於等於待散列存儲的集合的長度n。在此例中,n=7,所以假定取m=13,則得到的每個元素的散列地址爲:
h(18)=18 % 13=5 h(75)=75 %13=10
h(60)=60 % 13=8 h(43)=43 %13=4
h(54)=54 % 13=2 h(90)=90 %13=12
h(46)=46 % 13=7
若根據散列地址把元素存儲到散列表H[m]中,則存儲映象爲:
2、 散列函數
1. 直接定址法
h(K) = K + C
這種方法關鍵字分配不連續,存儲空間嚴重浪費
2. 除留餘數法
除留餘數法是用關鍵字K除以散列表長度m所得餘數作爲散列地址的方法。對應的散列函數h(K)爲:
h(K) = K % m
這種方法計算簡單,適用範圍廣
3. 數字分析法
數字分析法是取關鍵字中某些取值較分散的數字位做爲散列地址的方法。
4. 平方取中法
平方取中法是取關鍵字平方的中間幾位作爲散列地址的方法,具體取多少位視實際要求而定。
5. 摺疊法
摺疊法是首先將關鍵字部分分割成位數相同的幾段(最後一段的位數若不足應補0),段的位數取決於散列地址的位數,由實際需要而定,然後將他們的疊加和
(捨去最高位進位)作爲散列地址的方法。
3、 處理衝突的方法
1、開放定址法
(1) 線性探查法
d=h(K)
di=(di-1+1) % m (1≤i≤m-1, d0=d)
向前面的例子中構造的H散列表中再插入關鍵字分別爲31和58的兩個元素,若發生衝突則使用線性探查法處理。
先看插入關鍵字爲31的元素的情況。關鍵字爲31的散列地址爲h(31)=31 % 13=5,因H[5]單元已被佔用,接着探查下一個即下標爲6的單元,因該單元空閒,所以關鍵字爲31的元素被存儲到下標爲6的單元中,此時對應的散列表H爲:
再看插入關鍵字爲58的元素的情況。關鍵字爲58的散列地址爲h(58)=58 % 13=6,因H[6]已被佔用,接着探查下一個即下標爲7的單元,因H[7]仍不爲空,再接着探查下標爲8的單元,這樣當探查到下標爲9的單元時,才查找到一個空閒單元,所以把關鍵字爲58的元素存入該單元中,此時對應的散列表H爲:
利用線性探查法處理衝突容易造成元素的堆積或稱聚集。
(2) 平方探查法
平方探查法的探查序列爲d,d+12,d+22,…,或表示爲(d+i2) % m (0≤i≤m-1)。若使用遞推公式表示,則爲:
d=h(K)
di=(di-1+2i-1)% m (1≤i≤m-1, d0=d)
這種方法能夠較好的避免堆積現象,它的缺點是不能探查到散列表上的所有單元,但只能探查到一半單元。
(3) 雙散列函數探查法
這種方法使用兩個散列函數h1和h2,其中h1和前面的h(K)一樣,以關鍵字爲自變量,產生一個0至m-1之間的數作爲散列地址,h2也以關鍵字爲自變量,
產生一個1至m-1之間的,並和m互素的數作爲探查序列的地址增量。
雙散列函數的探查序列爲:
d=h1(K)
di=(di-1+h2(K))% m (1≤i≤m-1, d0=d)
2. 鏈接法
鏈接發又稱開散列法,它是把發生衝突的同義詞元素用單鏈錶鏈接起來的方法。
假定一個線性表B爲:
B=(18,75,60,43,54,90,46,31,58,73,15,34)
爲了進行散列存儲,假定採用的散列函數爲:
h(K)=K % 13
當發生衝突時,假定採用鏈接法處理,則得到的散列表如下圖所示。
用鏈接法處理衝突,雖然比開放定址法多佔用一些存儲空間用來存儲鏈接指針,但它可以減少在插入和查找過程中同關鍵字的比較次數。
4、散列表的運算操作
下面通過實例程序能夠調試利用數組存儲的散列表算法:
#include<stdio.h>
#include<stdlib.h>
#define HashMaxSize 43 //定義散列存儲空間的最大長度爲一個素數43
#define NullTag -100 //假定用 -100 作爲空閒關鍵字標記
#define DeleteTag -200 //假定用 -200 作爲已刪除元素的關鍵字標記
struct ElemType
{
int key; //假定關鍵字爲整型
char rest[20];//該元素類型的其他域
};
typedef struct ElemType hashlist1[HashMaxSize];//定義數組存儲的散列表的類型
typedef int KeyType; //定義元素關鍵字的類型爲整型
int H(KeyType key, int m)//可採用任一種合適的構造散列函數的方法來計算散列地址
{//這裏是按關鍵字與m求餘的方式
return key % m;
}
//1、初始化散列表
void InitHashList(hashlist1 HT)//把散列表HT中每一單元的關鍵字key域都設置爲空標誌
{
int i;
for (i = 0; i < HashMaxSize; i++)
HT[i].key = NullTag;
}//在該算法中,關鍵字類型爲整型,NullTag爲事先定義的全局常量-100,當關鍵字類型爲
//字符串時它爲空串"\0",應採用字符串函數進行比較或賦值(下同)
//2、清空一個散列表
void ClearHashList(hashlist1 HT)
{
int i;
for (i = 0; i < HashMaxSize; i++)
HT[i].key = NullTag;
}
//3、向散列表插入一個元素
int Insert(hashlist1 HT, int m, struct ElemType x)//向長度爲m的散列表HT中插入一個元素x
{
int d = H(x.key, m);
int temp = d;
while (HT[d].key != NullTag)//用線性探查法處理衝突
{
d = (d + 1) % m;
if (d == temp)
{
printf("散列表空間已被佔滿,應重建!\n");
return 0;
}
}
HT[d] = x;
return 1
d = (d + 1) % m;
if (d == temp)
return -1;
}
return -1;
}
//5、從散列表中刪除一個元素
int Delete(hashlist1 HT, int m, KeyType K)
{
int d = H(K, m);
int temp = d;
while (HT[d].key != NullTag)
{
if (HT[d].key == K)
{
HT[d].key = DeleteTag;
return 1;
}
else
d = (d + 1) % m;
if (d == temp) //循環一週仍未找到則返回0表示失敗
return 0;
}
return 0;
}//在這個算法中,使用DeleteTag爲刪除標記,而不是直接把被刪除元素單元置空,否則就割斷
//了元素的查找路徑,該位置也可以爲後續插入元素所用,只需將插入算法中國while循環條件
//改爲 HT[d].key != NullTag && HT[d].key != DeleteTag 即可。
//顯示輸出散列表中的所有元素,假定只輸出元素的關鍵字
void PrintHashList(hashlist1 HT, int m)
{
int i;
printf("散列表爲:");
for (i = 0; i < m; i++)
printf("%d,", HT[i].key);
printf("\n");
}
//主函數
void main()
{
int n, m, i, j;
struct ElemType x;
hashlist1 ht;
InitHashList(ht);
printf("從鍵盤輸入待散列元素的個數n和散列表長度m:");
do
{
scanf(" %d %d", &n, &m);
if (n > m || m > HashMaxSize)
printf("重輸n和m值:");
}while (n > m || m > HashMaxSize);
printf("從鍵盤向散列表輸入%d個元素的關鍵字:\n", n);
for (i = 0; i < n; i++)
{
scanf(" %d", &x.key);
Insert(ht, m, x);
}
PrintHashList(ht, m);
printf("輸入待刪除一批元素的關鍵字,用-1作爲結束!\n");
do
{
scanf(" %d", &x.key);
if (x.key == -1)
break;
Delete(ht, m, x.key);
}while(1);
PrintHashList(ht, m);
printf("輸入待插入散列表的一批元素的關鍵字,用-1作爲結束!\n");
do
{
scanf(" %d", &x.key);
if (x.key == -1)
break;
Insert(ht, m, x);
}while(1);
PrintHashList(ht, m);
printf("從鍵盤上輸入一批待查找元素的關鍵字,用-1作爲結束!\n");
do
{
scanf(" %d", &x.key);
if (x.key == -1)
break;
if ((j = Search(ht, m, x.key)) != -1)
printf("查找關鍵字爲%d的元素成功,返回下標值爲%d\n", x.key, j);
else
printf("查找關鍵字爲%d的元素失敗!\n", x.key);
}while(1);
ClearHashList(ht);
}
上面程序處理衝突是採用的開放定址法,運行結果如下:
若採用鏈接法處理衝突,則具體運算如下:
struct sNode
{
ELemType data;
struct sNode* next;
};
typedef struct sNode* hashlist2[HashMaxSize];
//1、初始化散列表
void InitHashList(hashlist2 HT)
{
int i;
for (i = 0; i < HashMaxSize; i++)
HT[i] = NULL;
}
//2、清空一個散列表
void ClearHashList(hashlist2 HT)
{
int i;
struct sNode* p;
for (i = 0; i < HashMaxSize; i++)
{
p = HT[i];
while (p != NULL)
{
HT[i] = p->next;
free(p);
p = HT[i];
}
}
}
//3、向散列表插入一個元素
int Insert(hashlist2 HT, int m, struct ElemType x)
{
int d = H(x.key, m);
struct sNode* p = malloc(sizeof(struct sNode));
if (p == NULL)
{
printf("內存空間用完!\n");
return 0;
}
p->data = x;
p->next = HT[d];//把新節點插入到d單鏈表的表頭
HT[d] = p;
return 1;
}
//4、從散列表中查找一個元素
struct ELemType* Search(hashlist2 HT, int m, KeyType K)
{
int d = H[K, m];
struct sNode* p = HT[d];
while (p != NULL)
{
if (p->data.key == K)
return &(p->data);
else
p = p->next;
}
return NULL;
}
//5、從散列表中刪除一個元素
int Delete(hashlist2 HT, int m, KeyType K)
{
int d = H(K, m);
struct sNode* p = HT[d], *q;
if (p == NULL)
return 0;
if (p->data.key == K)//若表頭結點爲被刪除的結點
{
HT[d] = p->next;
free(p);
return 1;
}
q = p->next;
while (q != NULL) //從第二個結點開始向下查找被刪除的元素
{
if (q->data.key == K)
{
p->next = q->next;
free(q);
return 1;
}
else
{
p = q;
q = q->next;
}
}
return 0;
}