为啥在单个链表中删除 O(1)?

Posted

技术标签:

【中文标题】为啥在单个链表中删除 O(1)?【英文标题】:Why is deleting in a single linked list O(1)?为什么在单个链表中删除 O(1)? 【发布时间】:2012-12-12 10:58:57 【问题描述】:

我不太明白为什么像wikipedia article 所说的那样,在单个链表末尾删除需要花费 O(1) 时间。

单个链表由节点组成。一个节点包含某种数据,以及对下一个节点的引用。链表中最后一个节点的引用为空。

--------------    --------------           --------------
| data | ref | -> | data | ref | -> ... -> | data | ref |
--------------    --------------           --------------

我可以删除 O(1) 中的最后一个节点。但在这种情况下,您不会将新的最后一个节点(前一个节点)的引用设置为 null,因为它仍然包含对已删除的最后一个节点的引用。所以我想知道,他们在运行时间分析中是否忽略了这一点?还是认为您不必更改它,因为引用只是指向任何内容,而这被视为空值?

因为如果不忽略它,我会认为删除是 O(n)。由于您必须遍历整个列表才能到达新的最后一个节点并将其引用也设置为 null。只有在双链表中才会真正是 O(1)。

-编辑- 或许这种观点能更清楚一些。但我将“删除节点”视为成功删除节点本身并将先前的引用设置为 null。

【问题讨论】:

我在 Wikipedia 文章中看到了两个对 O(1) 的引用,但它们都没有声明删除单链表中的最后一个节点是 O(1) 操作。据推测,您在尝试删除时已经持有指向前一个节点的指针,这是删除除第一个节点以外的任何节点所必需的。 【参考方案1】:

我不确定我是否在 Wikipedia 文章中看到它说可以在 O(1) 时间内删除单链表的最后一个条目,但在大多数情况下该信息是不正确的。给定链表中的任何单个节点,始终可以通过围绕该新节点重新连接列表,在 O(1) 时间内删除该节点之后的节点。因此,如果给你一个指向链表中倒数第二个节点的指针,那么你可以在 O(1) 时间内删除链表的最后一个元素。

但是,如果除了头指针之外没有任何额外的指向列表的指针,那么您不能在不扫描到列表末尾的情况下删除列表的最后一个元素,这需要 Θ(n)时间,正如你所指出的。您绝对正确,仅删除最后一个节点而不首先更改指向它的指针将是一个非常糟糕的主意,因为如果您要这样做,现有列表将包含指向已释放对象的指针。

更一般地说 - 在单链表中进行插入或删除的成本是 O(1),假设您在要插入或删除的节点之前有一个指向节点的指针。但是,您可能需要做额外的工作(最多 Θ(n))才能找到该节点。

希望这会有所帮助!

【讨论】:

这应该是批准的答案。更好的解释。【参考方案2】:

ANY 位置添加/删除ANY 节点是O(1)。代码只是使用固定成本(很少的指针计算和 malloc/frees)来添加/删除节点。对于任何特定情况,此算术成本都是固定的。

但是,到达(索引)所需节点的成本是 O(n)。

文章只是列出了多个子类中的添加/删除(添加在中间,开始,结束),以表明中间添加的成本与开始/结束添加的成本不同(但各自的成本仍然是固定的) .

【讨论】:

我仍然不太确定我是否理解正确。您认为删除只是删除节点 O(1),而不是将前一个节点的引用设置为 null,O(n),“到达所需节点的成本”?我将删除视为删除节点本身并将前一个节点的引用设置为空。 我不明白你为什么批准这个答案,它几乎没有解释太多。如果我将要删除的节点设为空并且它是指向下一个节点的指针,那么我会留下一个损坏的列表。我可以说我的名单一分为二。如何正确删除?除非它假定删除是删除下一个节点。我需要一个详细的解释。索引是 O(n),但如果删除需要索引,它应该是用例的一部分。也就是说,如果我在单链表上调用方法 Delete,我必须期望 O(n)。【参考方案3】:

如果您包括修复悬空节点的成本,您仍然可以在 O(1) 中完成,最后使用哨兵节点(也在该页面上描述)。

您的“空”列表以单个哨兵开始

Head -> [Sentinel]

添加一些东西

Head -> 1 -> 2 -> 3 -> [Sentinel] 

现在通过将节点 3 标记为无效来删除尾部 (3),然后删除指向旧哨兵的链接,并为其释放内存:

Head -> 1 -> 2 -> 3 -> [Sentinel] 
Head -> 1 -> 2 -> [Sentinel] -> [Sentinel] 
Head -> 1 -> 2 -> [Sentinel]

【讨论】:

我喜欢这个。它会使时间复杂度 O(1),同时引发空间交易,尽管它会保持空间复杂度 O(n)。显然,正如您所说,您有时需要执行 O(n) 操作来清理列表,否则您可能会遇到内存问题。 当您的列表较长时,您可能会发现算法不起作用。如果你有 Head -> 1 -> 2 -> 3 -> 4 -> 5 -> sentinel 然后删除节点 3,你能正确执行此操作吗?【参考方案4】:

O(1) 仅表示“恒定成本”。这并不意味着 1 次操作。这意味着“最多 C”操作,C 是固定的,而不管其他参数如何变化(例如列表大小)。事实上,在有时令人困惑的 big-Oh 世界中:O(1) == O(22)。

相比之下,删除整个列表的成本为 O(n),因为成本随列表的大小 (n) 而变化。

【讨论】:

删除整个列表是 O(1)。清空整个列表是 O(n)。【参考方案5】:

为了将来参考,我必须说,经过一些研究后,我发现为回答这个问题而提供的所有论点都不相关。答案是我们简单地决定堆栈的顶部是链表的头部(而不是尾部)。这将在 push 例程中引入轻微的变化,但 pop 和 push 都将保持 o(1)。

【讨论】:

@morgul 请阅读您正在查看的帖子。这答案。【参考方案6】:

如果你在单个链表中给出了一个指向要删除的节点的指针,那么你可以通过简单地将下一个节点复制到要删除的节点上来在恒定时间内删除这个节点。

M pointer to node to delete
M.data=M.next.data
M.next=M.next.next

否则,如果您需要搜索节点,那么您不能做得比 O(n) 更好

【讨论】:

【参考方案7】:

是的,即使您没有维护指向“前一个元素”的指针,您也可以在 O(1) 时间内完成此操作。

假设您有这个列表,其中“head”和“tail”是静态指针,“End”是标记为结束的节点。您可以正常使用“next == NULL”,但在这种情况下,您必须忽略该值:

head -> 1 -> 2 -> 3 -> 4 -> 5 -> End <- tail

现在,你得到了一个指向节点 3 的指针,但不是它的前一个节点。你也有头和尾指针。这是一些 python 风格的代码,假设是 SingleLinkedList 类。

class SingleLinkedList:

    # ... other methods

    def del_node(self, node):  # We "trust" that we're only ever given nodes that are in this list.
        if node is self.head:
            # Simple case of deleting the start
            self.head = node.next
            # Free up 'node', which is automatic in python
        elif node.next is self.tail
            # Simple case of deleting the end. This node becomes the sentinel
            node.value = None
            node.next = None
            # Free up 'self.tail'
            self.tail = node
        else:
            # Other cases, we move the head's value here, and then act
            # as if we're deleting the head.
            node.value = self.head.value
            self.head = self.head.next
            new_head = self.head.next
            # Free up 'self.head'
            self.head = new_head

唉,这会重新排序列表,并且移动值,这对您的应用程序可能合适,也可能不合适。

【讨论】:

【参考方案8】:

例如,您可以在 last 之前(“倒数第二个”)和删除时有一个指向元素的指针: 1. 删除此“倒数第二个”元素的 *next。 2. 将此“倒数第二个” *next 设置为 NULL

【讨论】:

除此之外,如果您保留一个指向“倒数第二个”的指针,您现在必须更新该指针。

以上是关于为啥在单个链表中删除 O(1)?的主要内容,如果未能解决你的问题,请参考以下文章

剑指Offer之在O时间删除链表节点

刷题--删除链表中重复的节点

c_cpp 删除O(1)中链表中的节点

《剑指Offer——在O时间删除链表结点,链表中倒数第k个结点》代码

当指向前一个节点的指针不可用时,从单个链表中删除中间节点

剑指offer18删除链表的(重复)节点