双链表何时比单链表更有效?

Posted

技术标签:

【中文标题】双链表何时比单链表更有效?【英文标题】:When is doubly linked list more efficient than singly linked list? 【发布时间】:2013-03-11 21:05:27 【问题描述】:

在今天的一次采访中,我被问到了这个问题。

除了回答反转列表以及向前和向后遍历之外,面试官一直在强调其中的一些“基本”内容。我放弃了,当然在面试后做了一些研究。似乎双链表中的插入和删除比单链表更有效。我不太确定双向链表如何更有效,因为很明显需要更改更多引用。 有人能解释一下背后的秘密吗?老实说,我做了很多研究,但未能理解我的主要问题是双链表仍然需要 O(n) 搜索。

【问题讨论】:

【参考方案1】:

插入显然在单链表中的工作量较少,只要您满足于始终在头部或某个已知元素之后插入。 (也就是说,您不能在已知元素之前插入,但请参见下文。)

另一方面,删除比较棘手,因为您需要在要删除的元素之前知道该元素。

这样做的一种方法是使删除 API 与要删除的元素的前身一起工作。这反映了插入 API,它采用将成为新元素前身的元素,但它不是很方便,而且很难记录。不过,这通常是可能的。一般来说,你通过遍历列表到达列表中的一个元素。

当然,你可以从头开始搜索列表,找到要删除的元素,这样你就知道它的前身是什么了。这假设删除 API 包括列表的头部,这也很不方便。此外,搜索速度非常慢。

几乎没有人使用但实际上非常有效的方法是将单链表迭代器定义为指向迭代器当前目标之前的元素的指针。这很简单,只比直接使用指向元素的指针慢一个间接,并且使插入和删除都快速。缺点是删除一个元素可能会使其他迭代器无效,这很烦人。 (它不会使被删除元素的迭代器无效,这对于删除一些元素的遍历来说是很好的,但这并没有多大的补偿。)

如果删除不重要,也许是因为数据结构是不可变的,单链表提供了另一个非常有用的属性:它们允许结构共享。单链表可以很高兴地成为多个头的尾部,这对于双链表来说是不可能的。出于这个原因,单链表传统上一直是函数式语言选择的简单数据结构。

【讨论】:

其他答案也不错,但我选择了这个,因为它给了我更多信息。 +1 对于@rici 的非常详细但易于理解的解释。另一件事 - 在双链表中搜索更容易。当您将索引传递给单个链表中的元素时,您必须遍历所有元素直到您要查找的那个(除非它是结束节点,它通常作为引用存储在列表对象/结构中) .使用双链表,您可以计算(非常容易)您的元素是否更靠近列表的开头或结尾,并相应地开始向前/向后遍历,这在许多情况下可以节省您的计算时间。 @rici 很抱歉回复晚了。但是关于结构共享的事情——你能想到的任何例子(现实生活中的应用程序)?谢谢! 我也想知道一个示例结构共享或多个头到一个尾! 关于“几乎没有人使用的方式”......拥有一个迭代器管理器应该是微不足道的,它可以在发生删除时更新其他迭代器,以免它们无效,对吗?前提是迭代器的数量相当低,也就是说,为了保持性能敏锐。【参考方案2】:

这里有一些代码让我更清楚......有:

class Node
      Node next;
      Node prev;

删除单链表中的节点 -O(n)-

你不知道哪个是前面的节点,所以你必须遍历列表直到找到它:

deleteNode(Node node)
    prevNode = tmpNode;
    tmpNode = prevNode.next;

    while (tmpNode != null) 
        if (tmpNode == node) 
            prevNode.next = tmpNode.next;
        
        prevNode = tmpNode;
        tmpNode = prevNode.next;
    

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

您可以像这样简单地更新链接:

deleteNode(Node node)
    node.prev.next = node.next;
    node.next.prev = node.prev;

【讨论】:

node.next.prev 呢? 用户代码如何处理node?用户代码正在传递node。故障几率 这个例子的目的是为了说明双链表和单链表在删除上的效率差异,不是企业实现。但是感谢您提出这一点!【参考方案3】:

以下是我对双向链表的看法:

您可以随时访问\插入两端。

它可以同时作为队列和堆栈工作。

节点删除不需要额外的指针。

您可以应用 Hill-Climb 遍历,因为您已经可以访问两端。

如果您存储的是数值,并且您的列表已排序,您可以为中位数保留一个指针/变量,然后使用统计方法可以高度优化搜索操作。

【讨论】:

【参考方案4】:

如果要删除链表中的元素,则需要将前一个元素链接到下一个元素。使用双向链表,您可以随时访问这两个元素,因为您有两个元素的链接。

这假设您已经有一个指向您需要删除的元素的指针,并且不涉及搜索。

【讨论】:

我想如果你已经知道尾巴那么你可以在末尾插入元素。【参考方案5】:

'除了回答反转列表以及向前和向后遍历之外,还有一些“基本”'。

似乎没有人提到:在双向链表中,只需使用指向已删除元素的指针即可重新插入已删除元素。参见 Knuth 的 Dancing Links 论文。我认为这是非常基本的。

【讨论】:

【参考方案6】:

因为双向链表可以立即访问前端和后端 在列表中,他们可以在 O(1) 的任一侧插入数据,也可以在 O(1) 的任一侧删除数据。因为双向链表可以在 O(1) 时间内在末尾插入数据并在 O(1) 时间内从前面删除数据,它们为队列提供了完美的底层数据结构。 Queeus 是项目列表 其中数据只能在末尾插入并从开头删除。 队列是抽象数据类型的一个例子,并且 我们能够使用数组在后台实现它们。 现在,由于队列在末尾插入并从开头删除,因此数组 仅与底层数据结构一样好。虽然数组是 O(1) 最后插入,从头开始删除是 O(N)。 另一方面,双向链表在末尾都插入 O(1) 并从头开始删除。这就是使它非常适合的原因 作为队列的底层数据结构。

在 LRU 缓存设计中使用双向链表,因为我们需要经常删除最近最少的项目。删除操作更快。

DLL 用于需要前后导航的导航系统。浏览器也使用它来实现访问网页的后退和前进导航,即后退和前进按钮。

【讨论】:

【参考方案7】:
Doubly Linked list is more effective than the Singly linked list when the location of the element to be deleted is given.Because it is required to operate on "4" pointers only & "2" when the element to be deleted is at the first node or at the last node.


struct  Node 

       int  Value;
       struct Node  *Fwd;
       struct Node  *Bwd;
);


Only the below line of code will be enough to delete the element ,If the element to be deleted is not in the first or last node.

X->Bwd->Fwd = X->Fwd; X->Fwd->Bwd = X->Bwd ;

【讨论】:

【参考方案8】:

单链表 vs 双链表 vs 动态数组

在比较三种主要数据结构时,在查看时间复杂度时,双向链表在所有主要任务和操作中都是最有效的。对于双向链表,除了仅按索引访问外,它在所有操作的恒定时间运行,在线性时间 (n) 运行,因为它需要遍历每个节点以获取所需的索引。在插入、删除、First、Last、连接和计数方面,双向链表以恒定时间运行,而动态数组以线性时间 (n) 运行。

就空间复杂度而言,动态数组只存储元素,因此时间复杂度恒定,单链表存储每个元素的后继,因此线性空间复杂度 (n),最糟糕的是,双向链表存储每个元素的前任和后继元素,因此也是线性空间复杂度,但 (2*n)。

除非您的资源/空间非常有限,否则动态数组或单链表可能会更好,但是,如今,空间和资源越来越丰富,因此双链表要好得多,但需要更多空间。

【讨论】:

以上是关于双链表何时比单链表更有效?的主要内容,如果未能解决你的问题,请参考以下文章

双链表与单链表的比较

从单链表扩展创建双链表 - 获取空指针

数据结构初阶第四篇——双链表(实现+图解)

(王道408考研数据结构)第二章线性表-第三节3:循环单链表和循环双链表

数据结构[双链表的实现,以及双链表和单链表之间的比较,链表和顺序表的优劣]

深度解析数组单链表和双链表