簡體   English   中英

Hashtable與雙鏈表?

[英]Hashtable with doubly linked lists?

算法簡介 (CLRS)聲明使用雙向鏈表的哈希表能夠比單鏈表更快地刪除項。 誰能告訴我在Hashtable實現中使用雙鏈表而不是單鏈表進行刪除有什么好處?

這里的混淆是由於CLRS中的符號。 為了與真正的問題保持一致,我在這個答案中使用了CLRS表示法。

我們使用哈希表來存儲鍵值對。 CLRS偽代碼中未提及值部分,而密鑰部分定義為k

在我的CLR副本中(我在這里處理第一版),列出帶鏈接的哈希的例程是插入,搜索和刪除(書中有更詳細的名稱)。 insert和delete例程采用參數x這是與key[x]相關聯的鏈表元素 搜索例程采用參數k ,它是鍵值對的關鍵部分。 我相信混淆是你已經將刪除例程解釋為使用鍵而不是鏈表元素。

由於x是鏈表元素, 如果它是雙鏈表 ,則單獨使用它就足以從哈希表的h(key[x])槽中的鏈表中進行O(1)刪除。 但是,如果它是單鏈表,則x不夠。 在這種情況下,您需要從表的插槽h(key[x])中的鏈表的頭部開始並遍歷列表,直到最后點擊x以獲取其前任。 只有當你擁有x的前身時才能刪除,這就是為什么本書說明單鏈接的情況導致搜索和刪除的運行時間相同。

補充討論

雖然CLRS說你可以在O(1)時間內進行刪除,假設有一個雙向鏈表,它還要求你在調用delete時有x 關鍵在於:他們定義了搜索例程以返回元素x 該搜索不是任意密鑰k恆定時間。 一旦從搜索例程中獲得x ,就可以避免在使用雙向鏈接列表時在刪除調用中產生另一次搜索的成本。

如果向用戶提供哈希表接口,則偽代碼例程的級別低於您使用的級別。 例如,缺少以鍵k作為參數的刪除例程。 如果將該刪除暴露給用戶,您可能只會堅持使用單鏈接列表並使用特殊版本的搜索來同時查找與k及其前任元素關聯的x

我可以想到一個原因,但這不是一個很好的原因。 假設我們有一個大小為100的哈希表。現在假設值A和G都添加到表中。 也許是哈希到75位。現在假設G也哈希到75,我們的沖突解決策略是以80的恆定步長向前跳。所以我們嘗試跳到(75 + 80)%100 = 55.現在,我們可以從當前節點開始並向后遍歷20,而不是從列表的前面開始並向前遍歷85,這樣會更快。 當我們到達G所在的節點時,我們可以將其標記為刪除它的墓碑。

不過,我建議在實現哈希表時使用數組。

Hashtable通常作為列表向量實現。 向量中的索引是鍵(哈希)。
如果每個鍵沒有多個值,並且您對這些值的任何邏輯不感興趣,則單個鏈表就足夠了。 選擇其中一個值時更復雜/特定的設計可能需要雙鏈表。

讓我們設計一個緩存代理的數據結構。 我們需要一個從URL到內容的地圖; 讓我們使用哈希表。 我們還需要一種方法來查找要逐出的頁面; 讓我們使用FIFO隊列來跟蹤上次訪問URL的順序,以便我們可以實現LRU驅逐。 在C中,數據結構可能看起來像

struct node {
    struct node *queueprev, *queuenext;
    struct node **hashbucketprev, *hashbucketnext;
    const char *url;
    const void *content;
    size_t contentlength;
};
struct node *queuehead;  /* circular doubly-linked list */
struct node **hashbucket;

一個微妙之處:為了避免特殊情況並在散列桶中浪費空間, x->hashbucketprev指向指向x的指針。 如果x是桶中的第一個,它指向hashbucket ; 否則,它指向另一個節點。 我們可以用它從桶中刪除x

x->hashbucketnext->hashbucketprev = x->hashbucketprev;
*(x->hashbucketprev) = x->hashbucketnext;

在驅逐時,我們通過queuehead指針迭代最近訪問的最少節點。 如果沒有hashbucketprev ,我們需要散列每個節點並使用線性搜索找到它的前任,因為我們沒有通過hashbucketnext到達它。 (這是否真的很糟糕是值得商榷的,因為哈希應該很便宜而且鏈條應該很短。我懷疑你所詢問的評論基本上是一次性的。)

如果散列表中的項目存儲在“侵入式”列表中,則他們可以知道它們所屬的鏈接列表。 因此,如果侵入列表也是雙重鏈接的,則可以快速從表中刪除項目。

(請注意,“侵入性”可被視為違反抽象原則......)

例如:在面向對象的上下文中,侵入式列表可能需要從基類派生所有項。

class BaseListItem {
  BaseListItem *prev, *next;

  ...

public: // list operations
  insertAfter(BaseListItem*);
  insertBefore(BaseListItem*);
  removeFromList();
};

性能優勢是任何項目都可以從其雙向鏈接列表中快速刪除,而無需定位或遍歷列表的其余部分。

不幸的是,我的CLRS副本現在在另一個國家,所以我不能用它作為參考。 但是,我認為這是在說:

基本上,雙向鏈表支持O(1)刪除,因為如果您知道項的地址,您可以執行以下操作:

x.left.right = x.right;
x.right.left = x.left;

從鏈表中刪除對象,而在鏈表中,即使您有地址,也需要搜索鏈表以查找其前任:

pred.next = x.next

因此,當你從哈希表中刪除一個項目時,你會查找它,由於哈希表的屬性,它是O(1),然后在O(1)中刪除它,因為你現在有了地址。

如果這是一個單鏈表,你需要找到你想要刪除的對象的前身,這將需要O(n)。


然而:

由於查找的工作原理,我對鏈式哈希表的這種斷言也略感困惑。 在鏈式哈希表中,如果存在沖突,則您需要遍歷鏈接的值列表以查找所需的項目,因此還需要找到其前任。

但是,語句的措辭方式給出了澄清:“如果哈希表支持刪除,那么它的鏈表應該雙重鏈接,以便我們可以快速刪除項目。如果列表只是單鏈接,那么要刪除元素x,我們首先必須在列表T [h(x.key)]中找到x,以便我們可以更新x的前任的下一個屬性。“

這就是說你已經有元素x,這意味着你可以用上面的方式刪除它。 如果你使用單鏈表,即使你已經有元素x,你仍然需要找到它的前身才能刪除它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM