当给定要删除的节点时,为什么在单链表和双链表中都没有删除O(1)?

时间:2018-11-08 07:18:21

标签: algorithm data-structures linked-list nodes

我非常清楚,当我们要删除“链表”中的一个节点时(无论是双链还是单链),而我们必须搜索该节点,此任务的时间复杂度为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),那么看来我的解决方案是可行的。尽管在这一点上,仅使用双向链接列表可能值得:)

7 个答案:

答案 0 :(得分:2)

的确,假设将数据也从i.next复制到i,然后再从i复制数据,则删除O(1)就是O(1)。 / p>

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

关于您的评论:

  

我想我的不满意来自于这样一个事实,即教科书和基本上每个在线资源都引用了双向链接列表的第一大优点是删除-这似乎有点不诚实。这是删除的一个非常具体的情况-在尾部删除!如果您只想进行有效的删除,则似乎不保证使用双链表而不是单链表(由于指针数量加倍需要所有开销)。只需存储对列表中倒数第二个节点的引用,就可以了!

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

请记住,大的O表示的是最坏的情况,因此,即使单个情况为O(n),整个算法也是O(n) < / p>

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

大型O并没有谈论预期的性能或平均性能,它是一种出色的理论工具,但是在决定使用什么时,您需要考虑特定的用例。

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

答案 1 :(得分:0)

对于列表中间的节点,您需要修改上一个节点(因此其“下一个”指针指向已删除的节点“下一个”)。

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

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

答案 2 :(得分:0)

  

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

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

答案 3 :(得分:0)

此方法的问题在于它会使错误的引用无效。删除该节点只会使对该那个节点的引用无效,而对该任何其他节点的引用将保持有效。

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

答案 4 :(得分:0)

很好的问题。

简单答案:对于单链列表,建议的替代解决方案并不完整,并且在为您提供最后一个删除节点时会失败。您无法使前一个节点到最后一个节点都为空。

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

答案 5 :(得分:0)

删除单个链接列表

假定总共有6个节点。并且第一个节点指示Head。

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

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

如果要删除最后一个节点,则复杂度为O(n),因为必须迭代所有节点。

答案 6 :(得分:0)

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

假设您必须像通常使用数组和列表那样查找节点来查找值,则只能沿一个方向行进,并且在双链表和单链表中需要O ^ n次才能到达该节点并在内存中检索它的地址。

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

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

即使我们必须移动到末端节点,如果前一个节点仍保存在临时变量中,我们也可以返回以将其下一个指针分配为空。但是,仍然是双向链表完成了我存储邻居的地址的任务,然后没有任何事情进入搜索和删除的临时状态。

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

对此有一些想法。希望自己能提供更详尽的解释。 维基百科:https://en.wikipedia.org/wiki/Doubly_linked_list