1. 程式人生 > >資料結構(查詢)

資料結構(查詢)

查詢

實驗簡介

介紹二分查詢和雜湊查詢,二分查詢是對於有序序列,每次都縮小一半查詢範圍的查詢方法,而雜湊查詢是關鍵字與在資料集中的位置一一對應,通過這種對應關係能快速地找到資料。

查詢部分和排序部分的示例程式碼可以通過如下程式碼來獲取

wget http://labfile.oss.aliyuncs.com/courses/20/Search_Sort.zip

一、二分查詢

前面幾章我們已經講了幾種線性和非線性的資料結構,瞭解了資料的儲存方式,這一章我們來講下怎麼查詢這些資料。

順序查詢想必大家都知道,就是從頭到尾比較資料集中的每一個數據,以此來獲得想要的資料,但當資料集中擁有的資料較多時,這種方法的效率就會很低。

二分查詢是比順序查詢效率高的一種查詢演算法,但它只適用於有序的資料集。二分查詢也叫折半查詢,它的查詢步驟為:首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查詢關鍵字比較,如果兩者相等,則查詢成功;否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查詢關鍵字,則進一步查詢前一子表,否則進一步查詢後一子表。重複以上過程,直到找到滿足條件的記錄,使查詢成功,或直到子表不存在為止,此時查詢不成功,如下圖所示。

下面是二分查詢的程式碼實現:

#include <stdio.h>
#include <stdlib.h>

int
BinarySearch(int *array, int key, int low, int high)
{ int mid; while (low <= high) { mid = (low + high) / 2; if (key == array[mid]) { return mid; } else if (key < array[mid]) { high = mid - 1; } else
{ low = mid + 1; } } return 0; } int main() { int n, i, key, position; int *array; printf("請輸入有序陣列的大小:"); scanf("%d", &n); array = (int*) malloc(sizeof(int) * n); printf("請按升序輸入資料:\n"); for (i = 0; i < n; i++) { scanf("%d", &array[i]); } printf("請輸入想要查詢的數:"); scanf("%d", &key); if (position = BinarySearch(array, key, 0, n - 1)) { printf("%d的位置為:%d\n", key, position); } else { printf("%d不存在\n", key); } }

二、雜湊查詢

1. 散列表

通常我們查詢資料都是通過一個一個地比較來進行,那麼有沒有可能有這樣的一種方法,要尋找的資料與其在資料集中的位置存在一種對應的關係,通過這種關係就能找到資料的位置。其實這種方法已經存在了,這個對應關係稱為雜湊函式(雜湊函式),由這個思想建立的表就稱為散列表(雜湊表)。

2. 雜湊函式的構造

要構造雜湊表首先需要有雜湊函式,並且這個雜湊函式需要儘可能地減少衝突,通常有下面幾種構造方法:

(1) 直接定址法

我們通過一個例子來講解,如果我們現在要對0-20歲的進行人口統計,那麼我們對年齡這個關鍵字就可以直接用年齡的數字作為地址。此時f(key) = key。

這個時候,我們可以得出這麼個雜湊函式:f(0) = 0,f(1) = 1,……,f(20) = 20。這個是根據我們自己設定的直接定址來的。人數我們可以不管,我們關心的是如何通過關鍵字找到地址。

如果我們現在要統計的是80後出生年份的人口數,那麼我們對出生年份這個關鍵字可以用年份減去1980來作為地址。此時f (key) = key-1980。

假如今年是2000年,那麼1980年出生的人就是20歲了,此時 f(2000) = 2000 - 1980,可以找得到地址20,地址20裡儲存了資料“人數500萬”。

也就是說,我們可以取關鍵字的某個線性函式值為雜湊地址,即:

f(key) = a × key + b

這樣的雜湊函式優點就是簡單、均勻,也不會產生衝突,但問題是這需要事先知道關鍵字的分佈情況,適合査找表較小且連續的情況。由於這樣的限制,在現實應用中,直接定址法雖然簡單,但卻並不常用。

(2) 數字分析法

數字分析法是在知道關鍵字的情況下,取關鍵字的儘量不重複的幾位值組成雜湊地址。

(3) 平方取中法

平方取中法就是取關鍵字平方後的中間幾位為雜湊地址。

(4) 摺疊法

摺疊法是將關鍵字分為位數相等的幾部分,最後一部分的位數可以不等,然後把這幾部分的值(捨去進位)相加作為雜湊地址。

(5) 除留餘數法

除留餘數法此方法為最常用的構造雜湊函式方法。對於散列表長為m的雜湊函式公式為: 

f( key ) = key mod p ( p ≤ m )

mod是取模(求餘數)的意思。事實上,這方法不僅可以對關鍵字直接取模,也可在摺疊、平方取中後再取模。

很顯然,本方法的關鍵就在於選擇合適的p,p如果選得不好,就可能會容易產生同義詞。下面我們來舉個例子看看:

有一個關鍵字,它有12個記錄,現在我們要針對它設計一個散列表。如果採用除留餘數法,那麼可以先嚐試將雜湊函式設計為f(key) = key mod 12的方法。比如29 mod 12 = 5,所以它儲存在下標為5的位置。

不過這也是存在衝突的可能的,因為12=2×6=3×4。如果關鍵字中有像18(3×6)、30(5×6)、42(7×6)等數字,它們的餘數都為6,這就和78所對應的下標位置衝突了。

使用除留餘數法的一個經驗是,若散列表表長為m,通常p為小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。實踐證明,當P取小於散列表長的最大質數時,產生的雜湊函式較好。

(6) 隨機數法

隨機數法是選擇一個隨機函式,取關鍵字的隨機函式值作為雜湊地址。

3. 處理衝突

前面在雜湊函式的構造中我們發現雜湊地址可能會產生衝突,所以處理衝突也是構造散列表中重要的一部分,通常處理衝突的方法有下面幾種:

(1) 開發定址法

所謂的開放定址法就是一旦發生了衝突,就去尋找下一個空的雜湊地址,只要散列表足夠大,空的雜湊地址總能找到,並將記錄存入,公式為:

fi(key) = (f(key)+di) MOD m (di=1,2,3,......,m-1)

用開放定址法解決衝突的做法是:當衝突發生時,使用某種探測技術在散列表中形成一個探測序列。沿此序列逐個單元地查詢,直到找到給定的關鍵字,或者碰到一個開放的地址(即該地址單元為空)為止(若要插入,在探查到開放的地址,則可將待插入的新結點存入該地址單元)。查詢時探測到開放的地址則表明表中無待查的關鍵字,即查詢失敗。

比如說,我們的關鍵字集合為{12,67,56,16,25,37,22,29,15,47,48,34},表長為12。 我們用雜湊函式f(key) = key mod l2。

當計算前S個數{12,67,56,16,25}時,都是沒有衝突的雜湊地址,直接存入:

計算key = 37時,發現f(37) = 1,此時就與25所在的位置衝突。

於是我們應用上面的公式f(37) = (f(37)+1) mod 12 = 2。於是將37存入下標為2的位置。這其實就是房子被人買了於是買下一間的作法:。

接下來22,29,15,47都沒有衝突,正常的存入:

到了 key=48,我們計算得到f(48) = 0,與12所在的0位置衝突了,不要緊,我們f(48) = (f(48)+1) mod 12 = 1,此時又與25所在的位置衝突。於是f(48) = (f(48)+2) mod 12=2,還是衝突……一直到 f(48) = (f(48)+6) mod 12 = 6時,才有空位,機不可失,趕快存入:

我們把這種解決衝突的開放定址法稱為線性探測法。

二次探測法

考慮深一步,如果發生這樣的情況,當最後一個key=34,f(key)=10,與22所在的位置衝突,可是22後面沒有空位置了,反而它的前面有一個空位置,儘管可以不斷地求餘數後得到結果,但效率很差。

因此我們可以改進di = 12, -12, 22, -22,……, q2, -q2 (q <= m/2),這樣就等於是可以雙向尋找到可能的空位置。

對於34來說,我們取di即可找到空位置了。另外增加平方運算的目的是為了不讓關鍵字都聚集在某一塊區域。我們稱這種方法為二次探測法。

fi(key) = (f(key)+di) MOD m (di = 12, -12, 22, -22,……, q2, -q2, q <= m/2)

隨機探測法

還有一種方法,是在衝突時,對於位移量di採用隨機函式計算得到,我們稱之為隨機探測法。

此時一定會有人問,既然是隨機,那麼查詢的時候不也隨機生成嗎?如何可以獲得相同的地址呢?這是個問題。這裡的隨機其實是偽隨機數。

偽隨機數是說,如果我們設定隨機種子相同,則不斷呼叫隨機函式可以生成不會重複的數列,我們在査找時,用同樣的隨機種子,它每次得到的數列是相同的,相同的di當然可以得到相同的雜湊地址。

fi(key) = (f(key)+di) MOD m (di是一個隨機數列)

總之,開放定址法只要在散列表未填滿時,總是能找到不發生衝突的地址,是我們常用的解決衝突的辦法。

(2) 再雜湊法

再雜湊法是當雜湊地址衝突時,用另外一個雜湊函式再計算一次,這種方法減少了衝突,但增加了計算的時間。

(3) 鏈地址法

前面我們談到了雜湊衝突處理的開放定址法,它的思路就是一旦發生了衝突,就去尋找下一個空的雜湊地址。那麼,有衝突就非要換地方嗎?我們直接就在原地處理行不行呢?

可以的,於是我們就有了鏈地址法。

將所有關鍵字雜湊地址相同的記錄儲存在一個單鏈表中,我們稱這種表為同義詞子表,在散列表中只儲存所有同義詞子表的頭指標。

對於關鍵字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我們用12為除數,進行除留餘數法:

此時,已經不存在什麼衝突換址的問題,無論有多少個衝突,都只是在當前位置給單鏈表增加結點的問題。鏈地址法解決衝突的做法是:將所有關鍵字雜湊地址相同的結點連結在同一個單鏈表中。若選定的散列表長度為m,則可將散列表定義為一個由m個頭指標組成的指標陣列T[0..m-1]。凡是雜湊地址為i的結點,均插入到以T[i]為頭指標的單鏈表中。T中各分量的初值均應為空指標。在拉鍊法中,裝填因子α可以大於1,但一般均取α≤1。

鏈地址法的優勢是對於可能會造成很多衝突的雜湊函式來說,提供了絕不會出現找不到地址的保障。當然,這也就帶來了査找時需要遍歷單鏈表的效能損耗,不過效能損耗在很多場合下也不是什麼大問題。

(4) 建立公共溢位區

這種方法的基本思想是:將散列表分為基本表和溢位表兩部分,凡是和基本表發生衝突的元素,一律填入溢位表。

三、小結

這一章我們講了二分查詢和雜湊查詢,二分查詢是對於有序序列,每次都縮小一半查詢範圍的查詢方法,而雜湊查詢是關鍵字與在資料集中的位置一一對應,通過這種對應關係能快速地找到資料,雜湊查詢中雜湊函式的構造和處理衝突的方法尤為重要,常見雜湊函式的構造方法有直接定址法、數字分析法、平方取中法、摺疊法、除留餘數法和隨機數法,處理衝突的方法有開放定址法、再雜湊法、鏈地址法和建立公共溢位區。