为啥 ArrayDeque 比 LinkedList 好

Posted

技术标签:

【中文标题】为啥 ArrayDeque 比 LinkedList 好【英文标题】:Why is ArrayDeque better than LinkedList为什么 ArrayDeque 比 LinkedList 好 【发布时间】:2011-09-04 00:35:54 【问题描述】:

我试图理解为什么 Java 的 ArrayDeque 比 Java 的 LinkedList 更好,因为它们都实现了 Deque 接口。

我几乎看不到有人在他们的代码中使用 ArrayDeque。如果有人对 ArrayDeque 的实现方式有更多的了解,那将会很有帮助。

如果我理解它,我会更有信心使用它。我无法清楚地理解 JDK 实现管理头尾引用的方式。

【问题讨论】:

看我几天前做的这个问题的答案:***.com/questions/6129805/… 在您安装 jdk 的文件夹中,有一个文件 src.zip。它是带有 java 类源代码的存档。我强烈建议研究这些类的结构和内部结构,以更好地了解 Java 类的工作原理。 还有一点。 ArrayDeque 的默认大小是 16。当 ArrayDeque 满时,它的大小会翻倍。大小翻倍后,元素被复制到新数组中。最好使用初始大小来初始化 ArrayDeque。 还有一点值得一提的是,在 LinkedList 上,您可以使用索引来遍历其元素,而 ArrayDeque 不支持基于索引的访问。 【参考方案1】:

ArrayDeque 是 Java 6 的新功能,这就是为什么很多代码(尤其是试图与早期 Java 版本兼容的项目)不使用它的原因。

在某些情况下它“更好”,因为您没有为要插入的每个项目分配一个节点;取而代之的是,所有元素都存储在一个巨大的数组中,如果它满了就会调整大小。

【讨论】:

【参考方案2】:

链接结构可能是最糟糕的迭代结构,每个元素都会出现缓存未命中。最重要的是,它们会消耗更多的内存。

如果您需要添加/删除两端,ArrayDeque 明显优于链表。对于循环队列,随机访问每个元素也是 O(1)。

链表唯一更好的操作是在迭代期间删除当前元素。

【讨论】:

要记住的另一个区别:LinkedList 支持空元素,而 ArrayDeque 不支持。 另外一个小缺点(对于实时应用程序)是,在推送/添加操作中,当 ArrayDeque 的内部数组已满时,它需要更多的时间,因为它必须加倍它的大小和复制所有数据。 @AndreiI,这只是故事的一方面。即使您排除了实时应用程序的迭代成本和预分配所需容量的能力,GC 也可能需要迭代整个 LinkedList。基本上,您正在将成本(启动成本更高)转移到 GC 中。 @DavidT。 b / c它涉及释放节点的GC成本,分配头部可能还需要卡标记(再次为GC,如果LinkedList已经在tenured gen中)......而且这是额外的间接(缓存- miss) 返回元素并重新链接。 另外,LinkedList 实现了 ListArrayDeque 没有。这意味着LinkedList 具有indexOfremove(int) 之类的方法,而ArrayDeque 没有。有时它可能很重要。【参考方案3】:

虽然ArrayDeque<E>LinkedList<E>都实现了Deque<E>接口,但是ArrayDeque基本使用Object数组 E[] 用于将元素保留在其对象中,因此它通常使用索引来定位头尾元素。

总之,它就像 Deque 一样工作(使用所有 Deque 的方法),但是使用数组的数据结构。至于哪个更好,取决于您使用它们的方式和位置。

【讨论】:

【参考方案4】:

我认为LinkedList 中的主要性能瓶颈是这样一个事实,即每当您推送到双端队列的任何一端时,该实现都会在后台分配一个新的链表节点,这本质上涉及 JVM/OS,而且成本很高.此外,无论何时从任何一端弹出,LinkedList 的内部节点都可以进行垃圾收集,这在幕后需要更多的工作。 另外,由于链表节点被分配在这里和那里,CPU缓存的使用不会提供太多好处。

如果可能感兴趣,我有一个证明,证明向ArrayListArrayDeque 添加(附加)一个元素在摊销常数时间内运行;参考this。

【讨论】:

你能告诉我如何在 Linked 中从头部添加/删除元素比在 Array 中更好吗? LinkedList 不应该有优势,因为只更改了对 prevnext 的引用,而在 ArrayDeque 中需要移动许多元素?【参考方案5】:

ArrayDeque 和 LinkedList 正在实现 Deque 接口,但实现不同。

主要区别:

    ArrayDeque 类是 Deque 接口的可调整大小的数组实现,LinkedList 类是列表实现

    NULL 元素可以添加到 LinkedList 但不能添加到 ArrayDeque

    ArrayDequeLinkedList 更有效地进行两端的添加和删除操作,LinkedList 实现在迭代期间删除当前元素的效率更高 p>

    LinkedList 实现比 ArrayDeque

    消耗更多内存

所以如果你不必支持NULL元素&&寻找更少的内存&&两端添加/删除元素的效率,ArrayDeque是最好的

详情请参阅documentation。

【讨论】:

"在链表中,需要 O(N) 才能找到最后一个元素。"不完全正确。 LinkedList 实现为双向链表,因此您不必遍历列表即可获取最后一个元素 (header.previous.element)。 “内存效率”的说法也可能会受到挑战,因为支持数组的大小总是被调整为 2 的下一个幂。 “找到最后一个元素需要 O(N)”是错误的。链表保留对最后一个节点的引用,LinkedList.descendingIterator() 获取该节点。所以我们得到 O(1) 的性能。请参阅:coffeeorientedprogramming.wordpress.com/2018/04/23/…(因此投反对票)。 当您使用Iterator 访问最后一个元素时,两个类的操作都是 O(N)。当您使用常见的Deque 接口时,访问最后一个元素对于两个类都是 O(1)。无论您采取哪种观点,同时将 O(1) 归因于 ArrayDeque 和 O(N) 归因于 LinkedList,都是错误的。 您的所有建议都被采纳并更正了内容【参考方案6】:

ArrayDeque 访问一个元素的时间复杂度是 O(1),而 LinkList 访问最后一个元素的时间复杂度是 O(N)。 ArrayDeque 不是线程安全的,因此需要手动同步,以便您可以通过多个线程访问它,因此它们更快。

【讨论】:

如果你指的是Java的Collection中的LinkedList,它是双向链接的,并且可以快速访问头部和尾部,因此访问最后一个元素也需要O(1)。 访问 LinkedList 中的最后一个元素不是 O(N)。如果你使用 descendingIterator(),它在 O(1) 中执行。请参阅coffeeorientedprogramming.wordpress.com/2018/04/23/…(因此投反对票)。 这两个类都不是线程安全的。并且句子的开头“必须手动同步”和结尾的“他们更快”之间没有联系。【参考方案7】:

所有批评LinkedList 的人,想想其他在Java 中使用List 的人可能大部分时间都使用ArrayListLinkedList,因为他们在Java 6 之前就已经使用了,因为那些是在大多数书籍中作为开始教授的内容。

但是,这并不意味着,我会盲目地站在LinkedListArrayDeque 一边。如果您想了解,请查看以下基准 done by Brian(已存档)。

测试设置考虑:

每个测试对象都是一个 500 个字符的字符串。每个字符串都是内存中的不同对象。 测试数组的大小将在测试期间发生变化。 对于每个数组大小/队列实现组合,运行 100 次测试并计算每次测试的平均时间。 每个测试都包括用所有对象填充每个队列,然后将它们全部删除。 以毫秒为单位测量时间。

测试结果:

低于 10,000 个元素,LinkedList 和 ArrayDeque 测试的平均时间都在 1 毫秒以下。 随着数据集越来越大,ArrayDeque 和 LinkedList 平均测试时间的差异越来越大。 在 9,900,000 个元素的测试大小下,LinkedList 方法比 ArrayDeque 方法花费的时间长约 165%。

图表:

外卖:

如果您的要求是存储 100 或 200 个元素,则不会 使用任何一个队列都有很大的不同。 但是,如果您在移动设备上进行开发,您可能希望使用 ArrayListArrayDeque 猜测最大容量 由于严格的内存限制,可能需要该列表。 存在大量代码,使用LinkedList 编写,因此在决定使用ArrayDeque 时要小心谨慎,尤其是因为它没有实现List 接口(我认为这是原因足够大)。可能是您的代码库与 List 接口进行了广泛的对话,很可能您决定使用ArrayDeque。将其用于内部实现可能是个好主意...

【讨论】:

这个benchmark如何捕捉链表垃圾造成的GC时间?【参考方案8】:

情况并非总是如此。

例如,根据 leetcode 103,linkedlist 的性能优于 ArrayDeque

/**
 * Definition for a binary tree node.
 * public class TreeNode 
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x)  val = x; 
 * 
 */
class Solution 
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) 
        List<List<Integer>> rs=new ArrayList<>();
        if(root==null)
            return rs;
        // ? here ,linkedlist works better
        Queue<TreeNode> queue=new LinkedList<>();
        queue.add(root);
        boolean left2right=true;
        while(!queue.isEmpty())
        
            int size=queue.size();
            LinkedList<Integer> t=new LinkedList<>();
            while(size-->0)
            
                TreeNode tree=queue.remove();
                if(left2right)  
                    t.add(tree.val);
                else
                    t.addFirst(tree.val);
                if(tree.left!=null)
                
                    queue.add(tree.left);
                
                if(tree.right!=null)
                
                    queue.add(tree.right);
                
            
            rs.add(t);
            left2right=!left2right;
        
        return rs;
    

【讨论】:

【参考方案9】:

我不认为ArrayDequeLinkedList 更好。它们是不同的。

ArrayDeque 平均比LinkedList 快。但是对于添加元素,ArrayDeque 需要分摊的常数时间,LinkedList 需要常数时间。

对于需要所有操作都花费恒定时间的时间敏感型应用程序,应该只使用LinkedList

ArrayDeque 的实现使用数组,需要调整大小,偶尔当数组满了需要添加元素时,调整大小需要线性时间,导致add() 方法需要线性时间。如果应用程序对时间非常敏感,那可能是一场灾难。

有关 Java 实现这两种数据结构的更详细说明,请参阅由 Wayne 和 Sedgewick 教授的普林斯顿大学提供的 Coursera 上的“Algorithms, Part I”课程。该课程免费向公众开放。

详细信息在“第 2 周”的“堆栈和队列”部分的视频“调整数组大小”中进行了说明。

【讨论】:

以上是关于为啥 ArrayDeque 比 LinkedList 好的主要内容,如果未能解决你的问题,请参考以下文章

ArrayDeque部分源码剖析

ArrayDeque 源码分析

死磕 java集合之ArrayDeque源码分析

JDK源码ArrayDeque源码分析

java LinkedLis t的26种使用方法

为啥 ArrayList 没有实现 Queue?