散列表

什么散列

散列是提供對任何有名項提供存取操作和刪除操作的列表。這種結(jié)構(gòu)的目的是提供常數(shù)時間的基本操作。

舉個例子:如果所有元素都是16-bits且不帶正負號的整數(shù),范圍是0-65535。那么簡單運用一個 array 就可以滿足上面的期望。首先配置一個arrayA.,擁有65536個元素,初值全部為0,每個元素值代表相應(yīng)元素出現(xiàn)的次數(shù)。如果插入元素i,就執(zhí)行A[i]++,如果刪除元素i,就執(zhí)行A[i]--,如果搜素元素就檢查A[i]是否為0。以上的每一個操作都是常數(shù)時間。這種解法的額外負擔(dān)是array的空間和初始化時間。

繪圖1.jpg

但是這種解法存在2個現(xiàn)實的問題:

  • 如果元素很多,那么要準(zhǔn)備的數(shù)組大小就很大,空間占用太大。
  • 如果元素是其他的類型(非整型),那么就不能作為數(shù)組索引。

第一個問題:
我們是可以采用一個映射函數(shù),將大數(shù)轉(zhuǎn)換為小數(shù),比如給定整數(shù)x,那么
x % array.size(),那么就會得到一個范圍在 [ 0,array.size() - 1 ] 之間的數(shù),也可以作為數(shù)組索引,但這會引入新的問題:如何解決碰撞沖突的問題。

第二個問題:
我們可以將一個非整數(shù)類型的轉(zhuǎn)換為整型,比如字符串"string",轉(zhuǎn)換為ASCII編碼。

線性探測

當(dāng)用散列函數(shù)計算出元素的插入位置,而該位置已經(jīng)不能使用時,最簡單的辦法就是循序往下一一尋找,直到找到一個可用的位置為止。
元素的刪除則可以采用"惰性刪除",也就是標(biāo)記刪除記號,實際刪除則待散列表重新整理時再進行。

線性探測.jpg

但線性探測會有一個不好的現(xiàn)象是:如圖最后狀態(tài)中,除非元素進過散列函數(shù)計算之后直接得出位置在#4 ~ #7,否則#4 ~ #7永遠不可能會被優(yōu)先考慮,因為(8 9 0 1 2已被占,遇到?jīng)_突會線性巡訪整個表格) #3一直會被優(yōu)先考慮。
這樣會暴露出一個問題:主集團(primary clustering)陷阱。散列表中是一大團被用過的方格,插入操作極有可能在主集團所形成的泥濘中奮力爬行,不斷解決碰撞問題,最后再找到空位置。然而插入之后又助長了主集團的長度。

public class LinearProbingHashST<Key, Value> {
    private int n;            // 當(dāng)前key-value元素對數(shù)
    private int m;            // 線性表(key-value)的總長度
    private Key[] keys;     // the keys
    private Value[] vals;   // the values

    public void put(Key key, Value val) {
        .....省略參數(shù)的有效性判斷
        // 如果已經(jīng)用掉50%,則擴容,減小主集團的影響
        if (n >= m / 2)
          resize(2 * m);      // resize里面會重新計算散列值,插入元素
    
        // 線性探測過程
        for (int i = hash(key); keys[i] != null; i = (i + 1) % m) {
            // 如果已經(jīng)存在key,則更新value
            if (keys[i].equals(key)) {
                vals[i] = val;
                    return;
            }
        }
        keys[i] = key;
        vals[i] = val;
        n++;
    }
    
    public void delete(Key key) {
        ...... 省略參數(shù)有效性判斷
        // find position i of key
        int i = hash(key);
        while (!key.equals(keys[i]))
            i = (i + 1) % m;
        
      // delete key and associated value
        keys[i] = null;
        vals[i] = null;

        // rehash all keys in same cluster
        i = (i + 1) % m;
        while (keys[i] != null) {
            // delete keys[i] an vals[i] and reinsert
            Key keyToRehash = keys[i];
            Value valToRehash = vals[i];
            keys[i] = null;
            vals[i] = null;
            n--;
            put(keyToRehash, valToRehash);
            i = (i + 1) % m;
        }
        n--;
        // halves size of array if it's 12.5% full or less
        if (n > 0 && n <= m / 8)
            resize(m / 2);
    }
}

二次探測

二次探測主要用于解決主集團的問題。
其命名由來是因為解決碰撞的方程式:F(i) = i^2是一個二次方程式。更明確的說,如果散列函數(shù)計算出新元素位置H,而該位置實際上已經(jīng)被使用,那么我們就依次嘗試H+12,H+22,H+3^2......而不是像線性探測一樣依序嘗試:H+1,H+2,H+3.......

二次探測.jpg

二次探測可以消除主集團,但卻可能造成次集團:兩個元素經(jīng)散列函數(shù)計算出來的位置若相同,那么插入時所探測的位置也相同,形成某種浪費。

但總體來說,還是二次探測相比于線性探測,仍然值得選擇。

開鏈法

開鏈法是在每一個表格元素中維護一個list,散列函數(shù)分配某一個list,然后再那個list身上執(zhí)行元素的插入,搜尋,刪除等操作。雖然針對list是一種線性搜索,但list夠短。速度還是很快。

開鏈法jpg

散列函數(shù)


參考資料
[1] 《STL源碼剖析》侯捷
[2] 《算法》4th [美] Robert Sedgewick,Kevin Wayne

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 數(shù)據(jù)結(jié)構(gòu)與算法--散列表 之前學(xué)習(xí)了基于鏈表的順序查找、基于有序數(shù)組的二分查找、二叉查找樹、紅黑樹,這些算法在查找...
    sunhaiyu閱讀 663評論 3 5
  • 散列表是支持 INSERT 、DELETE 和 SEARCH 的字典操作,其是對普通數(shù)組概念的推廣,因為可以對數(shù)組...
    點融黑幫閱讀 886評論 0 11
  • 本文主要介紹散列表(Hash Table)這一常見數(shù)據(jù)結(jié)構(gòu)的原理與實現(xiàn)。由于個人水平有限,文章中難免存在不準(zhǔn)確或是...
    absfree閱讀 16,377評論 2 35
  • 什么是哈希表? 哈希表(Hash table,也叫散列表),是根據(jù)關(guān)鍵碼值(Key value)而直接進行訪問的數(shù)...
    郝程序猿閱讀 2,247評論 1 7
  • 概念 散列表的實現(xiàn)常常叫做散列(hashing)。散列是一種用于以常數(shù)平均時間執(zhí)行插入、刪除和查找的技術(shù)。散列函數(shù)...
    NoFacePeace閱讀 336評論 0 0