繁体   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