为什么使用双向链表删除哈希表的元素是O(1)?

时间:2011-11-12 16:45:36

标签: algorithm hashtable doubly-linked-list

在CLRS的教科书“算法简介”中,pg上有这样的段落。 258。

如果列表是双重链接的,我们可以在O(1)时删除一个元素。 (注意,CHAINED-HASH-DELETE将元素x作为输入,而不是其键k,因此我们不必首先搜索x。如果哈希表支持删除,那么它的链表应该双重链接,以便我们可以快速删除一个项目。如果列表只是单独链接,那么要删除元素x,我们首先必须在列表中找到x,以便我们可以更新x的前任的 next 属性。对于单链表,删除和搜索都具有相同的渐近运行时间。)

这是什么让我感到困惑的是,我无法理解它的逻辑。有了双链表,还是要找到x才能删除它,这与单链表有什么不同?请帮助我理解它!

8 个答案:

答案 0 :(得分:28)

这里提出的问题是:考虑到你正在查看哈希表的特定元素。删除它的成本有多高?

假设您有一个简单的链接列表:

v ----> w ----> x ----> y ----> z
                |
            you're here

现在,如果您删除x,则需要将wy关联以保持列表关联。您需要访问w并告诉它指向y(您希望拥有w ----> y)。但是您无法从w访问x,因为它只是简单链接!因此,您必须遍历所有列表才能在O(n)操作中找到w,然后告诉它链接到y。那很糟糕。

然后,假设你是双重联系的:

v <---> w <---> x <---> y <---> z
                |
            you're here

很酷,你可以从这里访问w和y,这样你就可以在O(1)操作中连接两个(w <---> y)!

答案 1 :(得分:2)

一般来说,你是对的 - 你发布的算法虽然输入了元素作为输入而不仅仅是它的键:

  

请注意,CHAINED-HASH-DELETE 将元素x作为输入,而不是它   键k,这样我们就不必先搜索x

你有元素x - 因为它是一个双链表,你有指向前任和后继的指针,所以你可以在O(1)中修复这些元素 - 用一个链表只有后继可用,所以你必须在O(n)中搜索前任。

答案 2 :(得分:2)

在我看来,哈希表部分主要是红鲱鱼。真正的问题是:“我们可以在固定时间内从链接列表中删除当前元素,如果是,如何删除?”

答案是:它有点棘手,但实际上是的,我们可以 - 至少通常。我们(通常)必须遍历整个链表才能找到前一个元素。相反,我们可以在当前元素和下一个元素之间交换数据,然后删除下一个元素。

一个例外是当/我们需要/想要删除列表中的 last 项目时。在这种情况下, 没有要交换的下一个元素。如果你真的必须这样做,那么就没有办法避免找到前一个元素。但是,通常有一些方法可以避免这种情况 - 一种方法是使用sentinel而不是空指针来终止列表。在这种情况下,由于我们从不删除具有sentinel值的节点,因此我们永远不必处理删除列表中的最后一项。这给我们留下了相对简单的代码,如下所示:

template <class key, class data>
struct node {
    key k;
    data d;
    node *next;
};

void delete_node(node *item) {
    node *temp = item->next;
    swap(item->key, temp->key);
    swap(item->data, temp->data);
    item ->next = temp->next;
    delete temp;
}

答案 3 :(得分:1)

假设您要删除元素x,通过使用双向链接列表,您可以轻松地将x的前一个元素连接到x的下一个元素。所以不需要查看所有列表,它将在O(1)中。

答案 4 :(得分:0)

对于链式哈希表,

Find(x)通常是O(1) - 无论您是否使用单链表或双链表,都无关紧要。它们的性能相同。

如果在运行Find(x)之后,您决定要删除返回的对象,您会发现,在内部,哈希表可能必须再次搜索您的对象。它通常仍然是O(1)并不是什么大不了的事,但你发现你删除了很多,你可以做得更好。不是直接返回用户的元素,而是返回指向底层哈希节点的指针。然后,您可以利用一些内部结构。因此,如果在这种情况下,您选择双向链表作为表达链接的方式,那么在删除过程中,无需重新计算哈希值并再次搜索集合 - 您可以省略此步骤。您有足够的信息可以从您所在的位置执行删除操作。如果您提交的节点是头节点,则必须进一步小心,因此如果节点是链接列表的头部,则可以使用整数标记节点在原始数组中的位置。

权衡是额外指针占用的保证空间与可能更快的删除(以及稍微复杂的代码)。对于现代桌面,空间通常非常便宜,所以这可能是一个合理的权衡。

答案 5 :(得分:0)

编码观点: 可以在c ++中使用unordered_map来实现它。

unordered_map<value,node*>mp;

其中node*是指向存储键,左右指针的结构的指针!

使用方法:

如果您有一个值v,并且您想删除该节点,请执行以下操作:

  1. 访问节点的值如mp[v]

  2. 现在只需将其左指针指向右侧的节点即可。

  3. 瞧,你已经完成了。

    (提醒一下,在C ++中unordered_map需要一个平均O(1)才能访问存储的特定值。)

答案 6 :(得分:0)

在阅读教科书时,我也对同一个主题感到困惑(“ x”是指向元素还是元素本身的指针),然后最终落入了这个问题。但是经过上述讨论并再次参考教科书之后,我认为在书中“ x”被隐式假定为“节点”,并且其可能的属性为“键”,“下一个”。

教科书上有几行。

1)CHAINED-HASH-INSERT(T,x)  在列表T [h( x.key )]

的开头插入x

2)如果仅将列表链接在一起,则转到 删除元素x,我们首先必须在列表T [h( x.key )]中找到x,以便我们 可以更新x的前身的 next属性

因此,我们可以假定给出了元素的指针,而且我认为Fezvez对所提出的问题给出了很好的解释。

答案 7 :(得分:-3)

教科书错了。列表的第一个成员没有可用的“上一个”指针,因此如果它恰好是链中的第一个,则需要额外的代码来查找和取消链接元素(通常30%的元素是其链的头部,如果N = M,(当将N个项映射到M个时隙中时;每个时隙具有单独的链。)

编辑:

使用反向链接的更好方法是使用指针指向指向我们的链接(通常是列表中上一个节点的 - &gt;下一个链接)

struct node {
   struct node **pppar;
   struct node *nxt;
   ...
   }

删除然后变为:

*(p->pppar) = p->nxt;

这个方法的一个很好的特性是它对链上的第一个节点同样有效(其pppar指针指向节点的一部分指针。

更新2011-11-11

因为人们没有看到我的观点,我会试着说明一下。作为一个例子,有一个哈希表table(基本上是一个指针数组) 以及一大堆节点onetwothree其中一个必须删除。

    struct node *table[123];
    struct node *one, *two,*three;
    /* Initial situation: the chain {one,two,three}
    ** is located at slot#31 of the array */
    table[31] = one, one->next = two , two-next = three, three->next = NULL;
                one->prev = NULL, two->prev = one, three->prev = two;


    /* How to delete element one :*/
    if (one->prev == NULL) {
            table[31] = one->next;
            }
    else    {
            one->prev->next = one->next
            }
    if (one->next) {
            one->next->prev = one->prev;
            }

现在很明显,obove代码是O(1),但有一些令人讨厌的东西:它仍然需要array,而索引31,所以在大多数一个节点是“自包含”的情况,指向节点的指针足以将其从链中删除,除了,当它恰好是其链中的第一个节点时;然后,需要其他信息才能找到table31

接下来,考虑使用指向指针作为反向链接的等效结构。

    struct node {
            struct node *next;
            struct node **ppp;
            char payload[43];
            };

    struct node *table[123];
    struct node *one, *two,*three;
    /* Initial situation: the chain {one,two,three}
    ** is located at slot#31 of the array */
    table[31] = one, one-next = two , two-next = three, three->next = NULL;
                one->ppp = &table[31], two->ppp = &one->next, three->ppp = &two-next;

    /* How to delete element one */
    *(one->ppp) = one->next;
    if (one->next) one->next->ppp = one->ppp;

注意:没有特殊情况,也不需要知道父表。 (考虑存在多个哈希表但具有相同节点类型的情况:删除操作仍然需要知道应从哪个表中删除该节点)。

通常,在{prev,next}场景中,通过在双链表的开头添加虚节点来避免特殊情况;但这也需要分配和初始化。