Hashtable与双链表?

时间:2011-07-28 07:29:58

标签: algorithm hashtable

Introduction to Algorithms(CLRS)声明使用双向链表的哈希表能够比单链表更快地删除项。任何人都可以告诉我在Hashtable实现中使用双链表而不是单链表进行删除有什么好处?

6 个答案:

答案 0 :(得分:9)

这里的混淆是由于CLRS中的符号。为了与真正的问题保持一致,我在这个答案中使用了CLRS表示法。

我们使用哈希表来存储键值对。 CLRS伪代码中未提及值部分,而关键部分定义为k

在我的CLR副本中(我正在处理第一版),列出带链接的哈希的例程是插入,搜索和删除(书中有更详细的名称)。插入和删除例程采用参数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作为参数的删除例程。如果该删除向用户公开,您可能只会坚持使用单链接列表并使用特殊版本的搜索来同时查找与x及其前任元素关联的k。 / p>

答案 1 :(得分:0)

我能想到一个原因,但这不是一个很好的原因。假设我们有一个大小为100的哈希表。现在假设值A和G都添加到表中。也许是哈希到75位。现在假设G也哈希到75,我们的冲突解决策略是以80的恒定步长向前跳。所以我们尝试跳到(75 + 80)%100 = 55.现在,我们可以从当前节点开始并向后遍历20,而不是从列表的前面开始并向前遍历85,这样会更快。当我们到达G所在的节点时,我们可以将其标记为删除它的墓碑。

但是,我建议在实现哈希表时使用数组。

答案 2 :(得分:0)

Hashtable通常作为列表向量实现。向量中的索引是键(哈希) 如果每个键没有多个值,并且您对这些值的任何逻辑不感兴趣,则单个链表就足够了。选择其中一个值时更复杂/特定的设计可能需要双链表。

答案 3 :(得分:0)

让我们设计一个缓存代理的数据结构。我们需要一个从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到达它。 (这是否真的很糟糕是值得商榷的,因为哈希应该很便宜而链条应该很短。我怀疑你所询问的评论基本上是一次性的。)

答案 4 :(得分:0)

如果散列表中的项目存储在“侵入式”列表中,他们可以知道他们所属的链接列表。因此,如果侵入列表也是双重链接的,则可以快速从表中删除项目。

(但请注意,“侵扰性”可被视为违反抽象原则......)

示例:在面向对象的上下文中,侵入式列表可能要求所有项都从基类派生。

class BaseListItem {
  BaseListItem *prev, *next;

  ...

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

性能优势是任何项目都可以从其双向链接列表中快速删除,而无需查找或遍历列表的其余部分。

答案 5 :(得分:0)

不幸的是,我的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,你仍然需要找到它的前任才能删除它。