【问题标题】:Why isn't deletion O(1) in both Singly Linked Lists and Doubly Linked Lists, when given the node to delete?当给定要删除的节点时,为什么在单链表和双链表中都没有删除 O(1)?
【发布时间】:2019-04-11 16:22:44
【问题描述】:

我非常清楚,当我们想要删除链表中的一个节点(无论是双链还是单链),并且我们必须搜索这个节点时,这个任务的时间复杂度是 O(n),因为我们必须在最坏的情况下遍历整个列表来识别节点。同样,如果我们要删除第 k 个节点,也是 O(k),而且我们还没有对该节点的引用。

人们普遍认为,使用双向链表而不是单链表的好处之一是,当我们有一个要删除的节点的引用时,删除的时间是 O(1)。即,如果要删除节点 i,只需执行以下操作: i.prev.next = i.next 和 i.next.prev = i.prev

据说,只有在您对要删除的节点之前的节点有引用时,单链表中的删除操作是 O(1)。但是,我认为情况不一定如此。如果你想删除节点 i(并且你有对节点 i 的引用),为什么不能只复制 i.next 中的数据,然后设置 i.next = i.next.next?这也将是 O(1),就像在双向链表的情况下一样,这意味着在任何情况下,在双向链表中删除都不会更有效,就 Big-O 而言。当然,如果你要删除的节点是链表中的最后一个节点,这个想法就行不通了。

在比较单链表和双链表时,没有人记得这一点,这让我很烦恼。我错过了什么?

澄清:我在单链接案例中的建议是用下一个节点的数据覆盖您要删除的节点处的数据,然后删除下一个节点。这与删除节点 i 具有相同的预期效果,尽管它本身并不是您正在做的事情。

编辑

我学到了什么:

看来我在某种程度上是正确的。首先,很多人提到我的解决方案并不完整,因为删除最后一个元素是一个问题,所以我的算法是 O(n)(根据 Big-O 的定义)。我天真地建议通过跟踪列表中的“倒数第二个节点”来解决这个问题 - 当然,一旦列表中的最后一个节点第一次被删除,这就会导致问题。一个建议并且似乎确实有效的解决方案是用 NullNode 之类的东西来划分列表的末尾,我喜欢这种方法。

出现的其他问题是引用完整性,以及与从下一个节点复制数据本身相关的时间(即,可能需要昂贵的深度复制)。如果您可以假设您没有其他对象使用您正在复制的节点,并且复制任务本身就是 O(1),那么我的解决方案似乎有效。虽然,在这一点上,使用双向链表也许是值得的:)

【问题讨论】:

  • 我的看法是,将“i 的内容设置为 i.next 的内容”可以完成 如果 a) 内容不'之后不要改变 b)有自动垃圾收集。您不能只使用free(i.next),因为可能有“其他人”知道i.next 并在i(概念上)被删除时使用它。同样,复制内容会在发生更改时创建一个不同步的别名。
  • 仅当节点具有相同的类型/大小时,覆盖才有效(想想:struct hack,或者更好:VLA)。此外,payload 可以包含指向其他(动态分配的)对象的指针。
  • 您可以在列表中添加一个额外的节点来表示列表的结尾而不是空指针让我们将其称为 NullNode,然后您可以检查指针是否等于 NullNode 而不是检查 null .那么对于尾节点,您可以检查 i.next == NullNode 然后简单地说 NullNode = i;并删除 i.next;这样你就可以绕过特殊情况。
  • 我喜欢 kingW3 的 NullNode 命题,它似乎解决了“跟踪第 2 个到最后一个节点”解决方案的问题,这根本不好

标签: algorithm data-structures linked-list nodes


【解决方案1】:

对于列表中间的节点,您需要修改 previous 节点(因此它的“next”指针指向已删除的节点“next”)。

使用双链表很简单,因为要删除的节点包含指向前一个节点的指针。这对于 s 单链表是不可能的,您需要遍历列表,直到找到一个节点,其“下一个”指针是要删除的节点。

因此,在双链表中删除一个节点是 O(1)。对于单链表,它是 O(n),其中 n 是要删除的节点之前的节点数。

【讨论】:

  • 您没有考虑我的方法来规避在单链接情况下需要上一个节点:只需用下一个节点的数据覆盖您要删除的节点中的数据,然后删除下一个节点。这与删除原始节点的效果相同。
  • 好的。但是您的解决方案不适用于最后一个节点,因为没有“下一个节点”。
【解决方案2】:

确实,将数据从i.next复制到i然后删除i将是O(1),假设复制数据也是O(1)

但是即使使用这个算法,由于删除最后一个元素是O(n),并且用大O表示法对函数的描述只提供了函数增长率的上限,这意味着你的算法仍然O(n).

关于您的评论:

我想我的不满来自于教科书和基本上所有在线资源都引用双向链表的 #1 最大优势是删除 - 这似乎有点虚伪。这是一个非常具体的删除案例——尾部删除!如果您只想进行高效删除,那么似乎这不保证使用双重而不是单链表(由于指针数量翻倍所需的所有开销)。只需在列表中存储对倒数第二个节点的引用,就可以开始了!

您当然可以存储对倒数第二个节点的引用并删除最后一个节点O(1),但这只会在您第一次删除最后一个节点时出现。您可以更新对它之前的节点的引用,但发现它将是O(n)。如果您保留对倒数第二个元素的引用,则可以解决此问题,依此类推。至此,您已经推断出双向链表的方式,其主要优点是删除,并且由于您已经拥有指向先前节点的指针,因此您实际上不需要移动值。

请记住,大符号 O 表示最坏的情况,因此即使单个情况是 O(n),那么您的整个算法也是 O(n)

当您说解决方案是O(n) 时,您基本上是在说“在最坏的情况下,该算法将随着n 的增长而增长”

Big O 没有谈论预期或平均性能,它是一个很好的理论工具,但在决定使用什么时,您需要考虑您的特定用例。

此外,如果您需要保持引用完整性,您不会希望将值从一个节点移动到另一个节点,即如果您添加对节点 i+1 的引用并删除节点 i,您不会期望您的引用是无效的,因此在删除元素时,更强大的选项是删除节点本身。

【讨论】:

  • 我想这确实有道理。我想我的不满来自于教科书和基本上所有在线资源都引用双向链表的 #1 最大优势是删除这一事实 - 这似乎有点虚伪。这是一个非常具体的删除案例——尾部删除!如果您只想进行高效删除,那么似乎这不保证使用双重而不是单链表(由于指针数量翻倍所需的所有开销)。只需在列表中存储对倒数第二个节点的引用,就可以开始了!
  • 你说得很有道理,这是一个非常好的问题。可能存在移动值太昂贵的情况(尽管如果您使用指针,它应该是微不足道的)。请记住,如果您存储对倒数第二个项目的引用,则只会在第一次删除尾部时加快速度。
  • 更新了我的答案,对此做出了回应,我希望这是有道理的!
  • 感谢卢卡斯卡罗的澄清! “保持对倒数第二个节点的引用”当然不是要走的路。您如何看待 kingW3 提供的“NullNode”方式?似乎确实可以解决问题,并在单链接案例中真正删除 O(1)。
  • 只要您不需要保持参照完整性,听起来就可以工作,尽管在这一点上,双向链表似乎比必须管理特殊情况更容易使用和实现 :)跨度>
【解决方案3】:

据说删除是 O(1) 在一个单链表中只有当你 在要删除的节点之前引用该节点。 但是,我认为情况不一定如此。如果你想 删除节点 i(并且您有对节点 i 的引用),您为什么不能只是 从 i.next 复制数据,并设置 i.next = i.next.next?

因为它是前一个节点的“下一个”成员,您希望在删除 i 之前将其设置为等于 i.next 指向的内容。查找前一个节点是单链表的 O(N) 操作,如果您没有对它的引用。对于双链表,查找前一个节点是 O(1) 操作,因为它应该是 i.prev

【讨论】:

  • 也许我不清楚,但我知道这是删除操作中通常所做的。我的建议是用下一个节点中的数据覆盖您要删除的节点中的数据,然后将您的 i.next 设置为指向下一个节点,这与删除节点 i 的效果相同 -虽然被删除的是节点 i.next,并且它的数据被复制到节点 i。
  • 好的。但是您的解决方案不适用于最后一个节点,因为没有“下一个节点”。
  • 这确实回答了 Big O 的问题。但是,就像我在另一条评论中提到的那样,这可以通过简单地存储对倒数第二个节点的引用来解决。 (当然要考虑 1 个节点的情况)似乎是一种简单的方法,可以在没有不必要的开销的情况下在单链表中进行 O(1) 删除。
  • 但是现在如果您对单链表进行任何操作,您必须维护对倒数第二个节点的引用(如果您进行插入或删除,则更新它)加上如果您的单链表做一些事情列表有 0 到 1 个节点。我不喜欢额外的复杂性和特殊情况处理。与删除单个链表项的经典方式仅更新指针相比,对于非常大的项,数据的覆盖可能会很​​慢。
  • 这很公平,我同意。我想这一切都归结为我的烦恼,因为在比较单链表和双链表的文本中没有足够详细地讨论这个问题。它比你想象的要微妙一些。似乎如果您要说有效删除是双向链表的一大特点(与单链表相比),还要提到这不一定总是如此。
【解决方案4】:

这种方法的问题在于它使错误的引用无效。删除节点只会使对那个节点的引用失效,而对任何其他节点的引用仍然有效。

只要您不持有对列表的任何引用,这种方法就可以工作。否则很容易失败。

【讨论】:

    【解决方案5】:

    好问题。

    简单的答案:您为单链表建议的替代解决方案不完整,并且在您获得最后一个要删除的节点时失败。没有办法让前一个到最后一个节点指向 null。

    因此,对于一个有效的解决方案,单链表中删除的复杂度是O(n)。

    【讨论】:

      【解决方案6】:

      删除单个链接列表

      假设总共有 6 个节点。第一个节点表示Head。

      如果您想删除第一个节点,那么复杂度将为 O(1),因为您只需要 1 次迭代。

      如果要删除第 4 个节点,那么复杂度将 O(n)

      如果你想删除最后一个节点,那么复杂度将是 O(n),因为你必须迭代所有节点。

      【讨论】:

        【解决方案7】:

        我一直在寻找这种方法来解释它并获取博客文章的参考。

        假设您必须查找节点,就像我们经常使用数组和列表来查找值一样,您只能在一个方向上移动,并且在双链表和单链表中需要 O^n 次才能到达节点并在内存中检索它的地址。

        在双链表中,一旦有了节点位置,就可以根据需要设置前一个节点和下一个节点的指针,而无需临时存储任何数据。我认为您的想法会奏效,无论最后一个节点问题如何,如果在遍历查找需要删除的有问题的节点时,跟踪前一个节点的临时值。

        我认为真正的问题是在单个链表中,当您遍历分配新指针时,您必须将节点地址保存在临时变量中。在每个节点上,我们必须将当前节点存储为前一个节点,并将下一个节点存储为下一个节点,以便可以完成指针重新分配,这本质上是双链表在创建时所做的。

        即使我们必须到达结束节点,如果前一个节点保存在一个临时变量中,我们可以返回为它的下一个指针分配一个 none。但是,这仍然是双向链表完成我为它的邻居存储地址的工作,然后什么都不需要进入临时状态以进行搜索和删除。

        还要考虑 O^n 可能不是好处,但不必放置临时数据来进行删除。在节点的位置,我们可以访问双向链表中的邻居,而在单链表中,我们必须在每次迭代时临时存储数据,以便找到值。数据总是有可能不在列表中。在双向链表中,无需存储临时信息即可进行遍历。如果存在并行进程并且在指针交换发生之前临时数据已更改怎么办?如果在新分配之前删除该临时数据会怎样?

        只是一些想法。希望自己能得到更彻底的解释。 维基百科:https://en.wikipedia.org/wiki/Doubly_linked_list

        【讨论】:

          猜你喜欢
          • 2012-12-12
          • 2013-10-14
          • 2015-04-15
          • 2012-01-10
          • 2010-12-26
          • 1970-01-01
          • 2023-03-08
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多