雙散列和再散列暨散列表總結


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

 

雙散列

我們來考察最后一個沖突解決方法,雙散列(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),不過慶幸的是實際情況里並不會經常需要我們再散列,都是等快填滿了才做一次,所以還沒那么差。得說明一下,這種技術是對程序員友好而對用戶不友好的。因為如果我們把這種結構應用於某個程序,那並不會有什么顯著的效果,另一方面,如果再散列作為交互系統的一部分運行,可能使用戶感到系統變慢。所以到底用不用還是要權衡一番的,運行速度不敏感的場景就可以用,方便自己,因為這個技術把程序員從對表規模的擔心中解放出來了。

 

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

  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)的平均時間完成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)時間。散列表很適合這項工作,因為以字典序排列單詞並不重要,我們不關心它的順序,就避免了散列的缺陷。

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM