1. 程式人生 > >查詢-雜湊查詢

查詢-雜湊查詢

1.雜湊的相關概念

雜湊技術是在記錄的儲存位置和它的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個儲存位置f(key)。查詢時,根據這個確定的對應關係找到給定值key的對映f(key),若查詢集合中存在這個記錄,則必定在f(key)的位置上。

這裡我們把這種對應關係f稱為雜湊函式,又稱為雜湊(Hash)函式。按這個思想,採用雜湊技術將記錄儲存在一塊連續的儲存空間中,這塊連續儲存空間稱為散列表或雜湊表(Hash table)。那麼關鍵字對應的記錄儲存位置,我們稱為雜湊地址。

2.散列表查詢步驟

(1)在儲存時,通過雜湊函式計算記錄的雜湊地址,並按此雜湊地址儲存該記錄。

(2)當查詢記錄時,我們通過同樣的雜湊函式計算記錄的雜湊地址,並按此雜湊地址訪問該記錄。

雜湊技術既是一種儲存方法,也是一種查詢方法。然而它與線性表、樹、圖等結構不同的是,前面幾種結構,資料元素之間都存在某種邏輯關係,可以用連線圖表示出來,而雜湊技術的記錄之間不存在什麼邏輯關係,它只與關鍵字有關聯。因此,雜湊主要是面向查詢的儲存結構。
雜湊結束最適合的求解問題是查詢與給定值相等的記錄。對於查詢來說,簡化了比較過程,效率就會大大提高。但雜湊技術不具備很多常規資料結構的能力。

在理想的情況下,每一個關鍵字,通過雜湊函式計算出來的地址都是不一樣的,可現實中,這只是一個理想。我們時常會碰到兩個關鍵字key1≠key2,但是卻沒有f(key1)=f(key2),這種現象我們稱為衝突(collision),並把key1和key2稱為這個雜湊函式的同義詞(synonym)。

3.雜湊函式的構造方法

(1)直接定址法

我們可以取關鍵字的某個線性函式值為雜湊地址,即f(key)=akey+b(ab)**

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

(2)數字分析法

如果我們的關鍵字是位數較多的數字,比如我們的11位手機號”130xxxx1234”,其中前三位是接入號,一般對應不同運營商公司的子品牌,如130是聯通如意通、136是移動神州行、153是電信等;中間四位是HLR識別號,表示使用者號的歸屬地;後四位才是真正的使用者號。

若我們現在要儲存某家公司員工登記表,如果用手機號作為關鍵字,那麼極有可能前7位都是相同的。那麼我們選擇後面的四位稱為雜湊地址就是不錯的選擇。如果這樣的抽取工作還是容易出現衝突問題,還可以對抽取出來的數字再進行反轉(如1234改成4321)、右環位移(如1234改成4123)、左環位移、甚至前兩數與後兩數疊加(如1234改成12+34=46)等方法。總的目的就是為了提供一個雜湊函式,能夠合理地將關鍵字分配到散列表的各位置。

這裡我們提到了一個關鍵詞-抽取。抽取方法是使用關鍵字的一部分來計算雜湊儲存位置的方法,這在雜湊函式中是常常用到的手段。

數字分析法通常適合處理關鍵字位數比較大的情況,如果事先知道關鍵字的分佈且關鍵字的若干位分佈較均勻,就可以考慮這個方法。

(3)平方取中法

這個方法計算很簡單,假設關鍵字是1234,那麼它的平方就是1522756,再抽取中間的3位就是227,用做雜湊地址。再比如關鍵字是4321,那麼它的平方就是18671041,抽取中間的3位就可以是671,也可以是710,用做雜湊地址。平方取中法比價適合於不知道關鍵字的分佈,而位數又不是很大的情況。

(4)摺疊法

摺疊法是將關鍵字從左到右分割成位數相等的幾部分(注意最後一部分位數不夠時可以短些),然後將這幾部分疊加求和,並按散列表表長,取後幾位作為雜湊地址。

比如我們的關鍵字是9876543210,散列表表長為三位,我們將它分為四組,987|654|321|0,然後將它們疊加求和987+654+321+0=1962,再求後3位得到雜湊地址為962。

有時可能這還不能夠保證均勻分佈,不妨從一端向另一端來回摺疊後對齊相加。比如我們將987和321反轉,再與654和0相加,變成789+654+123+0=1566,此時雜湊地址為566。

摺疊法事先不需要知道關鍵字的分佈,適合關鍵字位數較多的情況。

(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對應的下標位置衝突了。

甚至極端一些,對於下表的關鍵字,如果我們讓p為12的話,就可能出現下面的情況,所有的關鍵字都得到了0這個地址數,這未免也太糟糕了點。

我們不選用p=12來做除留餘數法,而選用p=11,如下表所示。

此時就只有12和144有衝突,相對來說,就要好很多。

因此根據前輩們的經驗,若散列表表長為m,通常p為小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。

(6)隨機數法

選擇一個隨機數,取關鍵字的隨機函式值為它的雜湊地址。也就是f(key)=random(key)。這裡random是隨機函式。當關鍵字的長度不等時,採用這個方法構造雜湊函式是比較合適的。

4.處理雜湊衝突的方法

(1)開放定址法

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

fi(key)=(f(key)+di)MODm(di)

(2)再雜湊函式法

對於我們的散列表來說,我們事先準備多個雜湊函式。

fi(key)=RHi(key)(i=1,2,...,k)

這裡 就是不同的雜湊函式,你可以把前面說的什麼除留餘數、摺疊、平方取中全部用上。每當發生雜湊地址衝突時,就換一個雜湊函式計算,相信總會有一個可以吧衝突解決掉。這種方法能夠使得關鍵字不產生聚集,當然,相應地也增加了計算的時間。

(3)鏈地址法

將所有關鍵字為同義詞的記錄儲存在一個單鏈表中,我們稱這種表為同義詞子表,在散列表中只儲存所有同義詞子表的頭指標。對於關鍵字集合{12,67,56,16,25,37,22,29,15,47,48,34},我們同樣的用12為除數,進行除留餘數法,可得到如下圖所示結構,此時,已經不存在什麼衝突換址的問題,無論有多少個衝突,都只是在當前位置給單鏈表增加結點的問題。

鏈地址法對於可能會造成很多衝突的雜湊函式來說,提供了絕不會出現找不到地址的保障。當然,這也就帶來了查詢時需要遍歷單鏈表的效能損耗。

(4)公共溢位去法

這個方法其實就更加好理解,你不是衝突嗎?好吧,凡事衝突的都跟我走,我給你們這些衝突找個地兒待著。這就如同孤兒院收留所有無家可歸的孩子一樣,我們為所有衝突的關鍵字建立了一個公共的溢位區來存放。

就前面的例子而言,我們共有三個關鍵字{37,48,34}與之前的關鍵字位置有衝突,那麼將它們儲存到溢位表中,如下圖所示:

在查詢時,對給定值通過雜湊函式計算出雜湊地址後,先與基本表的相應位置進行比對,如果相等,則查詢成功;如果不相等,則到溢位表去進行順序查詢。如果相對於基本表而言,有衝突的資料很少的情況下,公共溢位區的結構對查詢效能來說還是非常高的。

5.散列表查詢實現

(1)散列表查詢演算法實現

首先是需要定義一個散列表結構以及一些相關的常數。其中HashTable就是散列表結構。結構當中的elem為一個動態陣列。

**#define SUCCESS 1**
**#define UNSUCCESS 0**
**#define HASHSIZE 12** /*定義散列表長為陣列的長度*/
**#define NULLKEY -32768**
typedef struct
{
    int *elem;  /*資料元素儲存基址,動態分配陣列*/
    int count;  /*當前資料元素個數*/
}HashTable;
int m=0;    /*散列表表長,全域性變數*/

/*初始化散列表*/
Status InitHashTable(HashTable *H)
{
    int i;
    m=HASHSIZE;
    H->count=m;
    H->elem=(int *)malloc(m*sizeof(int));
    for(i=0;i<m;i++)
        H->elem[i]=NULLKEY;
    return OK;
}

為了插入時計算地址,我們需要定義雜湊函式,雜湊函式可以根據不同情況更改演算法。

/*雜湊函式*/
int Hash(int key)
{
    return key % m; /*除留餘數法*/
}

初始化完成後,我們可以對散列表進行插入操作。假設我們插入的關鍵字集合就是前面的{12,67,56,16,25,37,22,29,15,47,48,34}。

/*插入關鍵字進散列表*/
void InsertHash(HashTable *H,int key)
{
    int addr = Hash(key);   /*求雜湊地址*/
    while(H->elem[addr] != NULLKEY) /*如果不為空,則衝突*/
        addr = (addr+1) % m;    /*開放地址法的線性探測*/
    H->elem[addr] = key;        /*直到有空位後插入關鍵字*/
}

程式碼中插入關鍵字時,首先算出雜湊地址,如果當前地址不為空關鍵字,則說明有衝突。此時我們應用開放地址法的線性探測進行重新定址,此處也可更改為鏈地址法等其他解決衝突的辦法。

散列表存在後,我們在需要時就可以通過散列表查詢要的記錄。

/*散列表查詢關鍵字*/
Status SearchHash(HashTable H,int key,int *addr)
{
    *addr = Hash(key);  /*求雜湊地址*/
    while(H.elem[*addr] != key) /*如果不為空,則衝突*/
    {
        *addr = (*addr+1) % m;  /*開放定址法的線性探測*/
        if(H.elem[*addr] == NULLKEY || *addr == Hash(key))
        {
            return UNSUCCESS;   /*則說明關鍵字不存在*/
        }
    }

    return SUCCESS;
}

查詢程式碼與插入的程式碼非常類似,只需做一個不存在關鍵字的判斷而已。

(2)散列表查詢實現程式碼(Java)

工程目錄結構

散列表查詢類

package com.red.hash.search;

public class HashSearch {

    public static int searchHash(int[] hash, int hashLength, int key) {
        //雜湊函式
        int hashAddress = key % hashLength;

        //指定hashAddress對應值存在但不是關鍵值,則用開放定址法解決
        while(hash[hashAddress] != 0 && hash[hashAddress] != key) {
            hashAddress = (++hashAddress) % hashLength;
        }

        //查詢到了開放單元,表示查詢失敗
        if(hash[hashAddress]==0) {
            return -1;
        }

        return hashAddress;
    }

    //資料插入雜湊表
    public static void insertHash(int[] hash, int hashLength, int data) {
        //雜湊函式
        int hashAddress = data % hashLength;

        //如果key存在,則說明已經被別人佔用,此時必須解決衝突
        while(hash[hashAddress] != 0) {
            //用開放定址法找到
            hashAddress = (++hashAddress) % hashLength;
        }

        //將data存入字典中
        hash[hashAddress] = data;
    }

}

測試類

package com.red.hash.search;

public class HashSearchTest {
    public static void main(String[] args) {

        int hashLength = 12;
        int[] hash = new int[hashLength];

        HashSearch.insertHash(hash, hashLength, 1);
        HashSearch.insertHash(hash, hashLength, 2);
        HashSearch.insertHash(hash, hashLength, 3);
        HashSearch.insertHash(hash, hashLength, 4);

        int location1 = HashSearch.searchHash(hash, hashLength, 1);
        int location2 = HashSearch.searchHash(hash, hashLength, 2);
        int location3 = HashSearch.searchHash(hash, hashLength, 3);
        int location4 = HashSearch.searchHash(hash, hashLength, 4);
        System.out.println("1所在的位置:" + location1);
        System.out.println("2所在的位置:" + location2);
        System.out.println("3所在的位置:" + location3);
        System.out.println("4所在的位置:" + location4);
    }
}

輸出結果

1所在的位置:1
2所在的位置:2
3所在的位置:3
4所在的位置:4

6.複雜度分析

單純論查詢複雜度,對於無衝突的hash表而言,查詢複雜度為O(1)(在查詢之前需要構建相應的Hash表)。