昨天學習搜索中兩種常見且簡單的搜索方式---線性搜索和二分搜索,今天則學習了另一種比較重要且相對較複雜的搜索方式----散列法搜索
要知道的是,散列法搜索不像昨天介紹的兩種搜索方式,線性搜索和二分搜索中元素可以排列在任意位置,而散列法搜索則是根據各元素的值來確定存儲位置,然後將位置保管在散列表中,從而實現數據的高速搜索。其中散列表是一種數據結構,能對包含關鍵字的數據集合高效地執行動態插入,搜索,刪除操作。雖然鏈表也能完成同樣操作,但搜索和刪除的複雜度都高達O(n).
散列表由能容納m個元素的數組T,以及根據關鍵字決定數組下標的函數共同組成。就像上面所說的,元素的位置室友元素值決定的,這就比較像一個字典了。散列表大致可通過以下方法實現:
insert(data)
T[h(data.key)]=data
search(data)
return T[h(data.key)];
這裏我們默認data.key是整數,當然,不是整數的情況下,我們也可以通過一些手段將其轉化爲整數用來作爲下標。
而h(k)是根據k值求數組下標的函數,稱爲散列函數。散列函數的值域爲[0,m-1],其中m是數組T的長度,爲滿足這個要求,我們一般將散列函數定義爲取餘運算,保證輸出值在0~m-1之間,比如:
h(k)=k mod m
但是這個函數也許大家也能很容易發現她的問題,那就是不同的key對應同一散列值的情況,導致不同的元素存儲在數組同一位置。爲了解決這種問題,我們有很多方法。開放地址法就是解決這種衝突的常用手段。
這裏我們使用的是雙散列結構的開放地址法,即使用兩個散列函數共同確定元素位置:
H(k)=h(k,i)=(h1(k)+i*h2(k))mod m
散列函數h(k,i)擁有關鍵字k和i兩個參數。其中i是發生衝突後計算下一個散列值的次數。也就是說一開始添加元素時會調用h(k,0),如果發生衝突,就會依次調用h(k,1),h(k,2),h(k,3).......
雙散列結構的開放地址法的實現方法如下:
int h1(key) {return key mod M;}
int h2(key) {return 1+(key mod (M-1));}
int h(key,i) {return (h1(key)+i*h2(key)) mod m}
int insert(T,key)
{
i=0;
while(true)
j=h(key,i)
if(T[j]==Null) //未發生衝突,直接插入並返回下標
T[j]=key
return j
else //發生衝突,i自增,開始下一次的插入
i++;
}
int search(T,key)
{
i=0;
while(true)
j=h(key,i)
if(T[j]==key) //找到元素,返回下標
return j
else if(T[j]==Null or i>=m) //查找結束,未找到元素,返回空
else i++ //查找未結束,繼續向下一個位置查找
}
這裏要理解爲啥找到T[j]==null就直接返回空表示未查找到,因爲雖然key雖然可能發生衝突存在多個可能的下標的位置上,但是存放的順序也是固定,也就是說不可能跳過h(key,i)的位置直接先存放在h(key,i+1)的位置,所以在找打j=h(key,i)上元素爲空,那麼後面的h(key,i+1),h(key,i+2)位置肯定也不需要查找了,肯定是不存在的。
還有個要注意的問題,由於發生衝突時,每次移動的位置是確定的,都是h2(k),那麼就要保證數組的長度m與h2(k)必須是互質的,否則會不斷循環的發生衝突,比如說對於有個值key,使得h1(key)=3,h2(key)=4,而數組的長度m=16,那麼在發證衝的情況下,尋找的下標依次是3,7,11,(3+3*4) mod16=15,(3+4*4) mod16=3,7,11.....這樣就會不斷地在3 7 11 15位置上發生衝突,這樣的話肯定是不可以的。所以爲了保證不出現這種情況,我們可以特意讓m爲質數,然後取一個小於m的值作爲h2(key),或者最簡單的方法就是直接令h2(k)=1,這樣使得發生衝突時依次查找後面的位置。
簡單介紹了這種方法後,做個小題目吧,就直接用書上的題目了---
題目:請實現一個能執行以下命令的簡易"字典"。
1.insert str:向當前字典插入字符串str
2.find str:當前字典中包含str時輸出yes,不包含時輸出no
輸入:第一行輸入命令數n.隨後n行按順序駛入n個命令。命令格式如上。
輸出:對於find命令輸出yes或no.每個輸出佔一行。
限制: 輸入的字符串僅僅由'A','C','G','T'四種字母構成。
1<=字符串長度<=12
n<=1000000
輸入示例: 輸出示例:
6 yes
insert AAA no
insert AAC yes
find AAA
find CCC
insert CCC
find CCC
散列法搜索的原理已經介紹了,下面直接附上源代碼:
#include<stdio.h>
#include<string.h>
#define M 1046527
#define NIL (-1)
#define L 14
char H[M][L];
int getChar(char ch)
{
switch(ch)
{
case 'A':return 1;
case 'B':return 2;
case 'C':return 3;
case 'D':return 4;
default:return -1;
}
}
long long getKey(char str[])
{
long long sum=0,p=1;
for(int i=0;i<strlen(str);i++)
{
sum+=p*getChar(str[i]);
p*=5;
}
return sum;
}
int h1(int key) {return key%M;}
int h2(int key) {return 1+(key%(M-1));}
int find(char str[])
{
long long key,h;
key=getKey(str); //將字符串轉換爲數值
for(int i=0;;i++)
{
h=(h1(key)+i*h2(key))%M;
if(strcmp(H[h],str)==0) return 1; //strcmp(str1,str2),若str1==str2,則返回零;若str1<str2,則返回負數;若str1>str2,則返回正數。
else if(strlen(H[h])==0) return 0; //找到的位置上字符串爲空,則找不到該字符串
}
return 0;
}
int insert(char str[])
{
long long key,h;
key=getKey(str);
for(int i=0;;i++)
{
h=(h1(key)+i*h2(key))%M;
if(strcmp(H[h],str)==0) return 1; //在字典中已經有了該"單詞",不進行插入
else if(strlen(H[h])==0)
{
strcpy(H[h],str);
return 0; //不存在該"單詞",則在首次爲空的位置進行插入
}
}
return 0;
}
int main()
{
int n,h;
char str[L],com[9];
for(int i=0;i<M;i++) H[i][0]='\0'; //初始化散列表,令所有字符串爲空
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%s %s",com,str);
if(com[0]=='i')
{
insert(str);
}
else{
if(find(str))
printf("yes\n");
else{
printf("no\n");
}
}
}
return 0;
}
PS:本篇博客因爲代碼格式的問題修改了好幾次,在這裏和大家道個歉~