雙散列和再散列暨散列表總結(附理解)

原文鏈接:https://www.cnblogs.com/hongshijie/p/9443452.html


先說明一下,她們兩個屬於不同的範疇,雙散列屬於開放定址法,仍是一種解決衝突的策略。而再散列是爲了解決插入操作運行時間過長、插入失敗問題的策略。簡而言之,她們的區別在於:前者讓散列表做的“對”(把衝突元素按規則安排到合理位置),後者讓散列表具有了可擴充性,可以動態調整(不用擔心填滿了怎麼辦)。


雙散列

我們來考察最後一個衝突解決方法,雙散列(double hashing)。常用的方法是讓F(i)= i * hash2( x ),這意思是用第二個散列函數算出x的散列值,然後在距離hash2( x ),2hash2( x )的地方探測。hash2( x )作爲關鍵,必須要合理選取,否則會引起災難性的後果——各種撞車。這個策略暫時不做過多分析了。


再散列

之前說過,對於使用平方探測法的閉散列裏,如果元素填的太滿的話後續插入將耗費過長的時間,甚至可能Insert失敗,因爲這裏面會有太多的移動和插入混合操作。怎麼辦呢?一種解決方法是建立另外一個大約兩倍大的表,再用一個新的散列函數,掃描整個原始表然後按照新的映射插入到新的表裏。


再散列的目的是爲了後續的插入方便。

比如我們把{6, 15, 23, 24,6}插入到Size=7的閉散列裏,Hash(x)= x % 7,用線性探測的方法解決衝突,會得到這樣一個結果:

在這裏插入圖片描述

現在還剩23,把這個插入之後,整個表裏就填滿了70%以上:

在這裏插入圖片描述

於是我們要建立一個新的表,newSize=17,這是離原規模2倍大小的最近素數。新的散列函數是Hash( x ) = x % 17。掃描原來的表,把所有元素插入到新的表裏,得到這個:

在這裏插入圖片描述

這一頓操作就是再散列。可以看出這會付出很昂貴的代價:運行時間O(N)O(N),不過慶幸的是實際情況裏並不會經常需要我們再散列,都是等快填滿了才做一次,所以還沒那麼差。得說明一下,這種技術是對程序員友好而對用戶不友好的。因爲如果我們把這種結構應用於某個程序,那並不會有什麼顯著的效果,另一方面,如果再散列作爲交互系統的一部分運行,可能使用戶感到系統變慢。所以到底用不用還是要權衡一番的,運行速度不敏感的場景就可以用,方便自己,因爲這個技術把程序員從對錶規模的擔心中解放出來了


具體實現可以用平方探測以很多種方式實現:

  1. 只要表有一半滿了就做
  2. 只有當插入失敗時才做(這種比較極端)
  3. 途中策略:當表到達某個裝填因子時再做。

由於隨着裝填因子的增加,表的性能會有所下降,所以第三個方法或許是最好的。再散列把程序員從對錶規模的擔心中解放出來了,這一點的重要之處在於在複雜程序中散列表不可能一開始就做得很大,然後高枕無憂。因爲我們也不知道多大才夠用,所以能使她動態調整這個特性就很有必要了。實現的時候也比較簡單

HashTable Rehash(HashTable H) {
    int i,OldSize;
    Cell *OldCells;
    
    OldCells=H->TheCells;
    OldSize=H->TableSize;
    
    //新建一個原規模*2的表
    H=Init(OldSize<<1);
    
    //掃描原表,重新插入到新表裏
    for (i=0; i<OldSize; i++) {
        if (OldCells[i].Info==Legitimate) {
            Insert(OldCells[i].value, H);
        }
    }
    
    free(OldCells);
    return H;
}

散列篇的開頭就說了,這不是一種單純的技術,而是一種思想。所以我們不必機械地理解她,可以把這種思想靈活地用在其他結構中,比如在隊列變滿的時候,可以聲明一個雙倍大小的數組,然後拷貝過來,釋放原來的隊列。這就有點像向量的規模調整了,聯繫的普遍性再一次得到印證。


回顧

散列篇到這裏就要結束了,在收尾之際我們不妨做一個總結,回眸下這一路沿途的風景。

散列表可以用O(1)O(1)的平均時間完成insert和Find,在使用散列的時候要尤其注意裝填因子的問題,因爲他是保證時間上確界的關鍵。對於分離鏈接法,儘量讓λ接近1。對於開放定址法來說,不到萬不得已就別讓λ太大,儘量保持λ<=0.5。如果用線性探測,性能會隨着λ趨向於1而急劇下降。再散列運算可以通過表的伸縮來完成,這樣就會保持λ處於合理範圍,而且優點還在於,如果當下空間緊缺的話,這麼做是很棒的策略。

比較一下二叉查找樹和散列,二叉查找樹也可以實現Insert和Find,效率會比散列低一些,O(logN)。雖說這方面慢了一點,但是二叉樹能支持更多的操作,比如可以FindMin和FindMax,這個散列就做不到了。還有,二叉查找樹可以迅速找到在一定範圍內的所有元素,散列也做不到,而且O(logN )也不會比O(1)慢太多,因爲查找樹不需要做乘除法,就彌補了一些速度缺陷,綜上看來她們也算是各有千秋。

說完了平均時間,再說說最壞情況散列的最壞情況一般是實現的缺憾,而二叉樹的最壞情況呢,是輸入序列有序的時候,那這個時候根據BST規則,二叉樹會退化成一條單鏈,升序的輸入會導致一捺的情形,降序輸入會形成一撇。這要是再增刪查改付出的可就是O(N)了。平衡查找樹的實現相對複雜一些,所以如果不需要有序的信息以及對輸入是否排序有要求的話,就該選擇散列這種結構。

散列還有着豐富的應用,這裏舉四個例子:第一個,編譯器使用散列表跟蹤源代中聲明的變量,這種數據結構叫做符號表。散列表示這種問題的理想應用,因爲只有Insert和Find操作。而且標識符一般都很短,所以根據這個短字符串能迅速算出哈希值。第二個,在圖論的應用,對於節點有實際名字而不是數字的圖論問題都可以用散列表來做。比如某個頂點叫計算機,那麼某個特定的超算中心對應的計算機列表裏有ibm1,ibm2,ibm3這樣的。如果用查找樹來做這個事,那效率就很滑稽了2333 第三種用途是在爲遊戲編制的程序裏。程序搜索遊戲不同的行(row,不是同行那個行)時,根據實時位置計算出一個散列值,然後跟蹤這些值來確定位置。如果同樣的位置再出現,那麼程序會用簡單的移動變換來避免重複計算,因爲重複計算的代價都很大。遊戲程序的這種叫做變換表。

第四個用途就是在線拼寫檢驗程序,比如word裏面的拼寫檢測,把整個詞典預先散列,然後檢測每個單詞拼寫對不對,這隻花費O(1)時間。散列表很適合這項工作,因爲以字典序排列單詞並不重要,我們不關心它的順序,就避免了散列的缺陷。

總而言之,揚長避短地選用不同結構處理工作纔是我們學習數據結構的第一要義。


總結

本篇博文是轉載過來的,篇中說到的其他內容可以根據開頭的鏈接過去品讀。

關於再散列雙散列之前一直混淆,現在理一下:

對於雙散列其實是在解決哈希衝突時的一種探測的方法,其用到兩個哈希函數,這是開放定址法處理衝突的方法,區別於開散列(拉鍊法,也是處理衝突的,只不過前面那個是順序存儲(閉散列),而開散列是鏈式存儲))的一種策略,王道書中也稱這種方法叫做再散列法(這正是自己混淆的所在)。

再散列是一種處理插入時,處理時間長,甚至是插入失敗的問題(哈希表太滿了),這時我們要讓它換到一個2倍的空間裏去,於是用以前的哈希函數去把表中的元素找到,再用新的哈希函數處理到新的表中,當然這個操作只有當快滿的時候做。

實際上,兩種方法都用到兩個哈希函數,但仔細理解可以發現,對於雙散列的兩個哈希函數是嵌套的,共同決定了下一次的探測地址;而再散列的兩個哈希函數是分佈的,只是用來搬運一個小的表到一個大的表的過程。


版權聲明:本文爲博客園博主「儀式黑刃 」的原創文章
原文鏈接:https://www.cnblogs.com/hongshijie/p/9443452.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章