何时在数组/数组列表上使用链接列表?


176

我使用了很多列表和数组,但是我还没有遇到这样一种情况:数组列表不能像链接列表一样容易地使用。我希望有人能给我一些有关链表何时明显更好的例子。


在Java中,ArrayList和LinkedList使用与构造函数不同的完全相同的代码。您的“数组列表...比链接列表更容易或更容易使用”是没有意义的。请提供一个ArrayList比LinkedList更“容易”的示例。
S.Lott

2
检查这个问题,以及,stackoverflow.com/questions/322715/...
NONAME


3
S.Lott这是不正确的。Java ArrayList是Array的包装,并添加了一些实用程序功能。显然,链表是链表。 developer.classpath.org/doc/java/util/ArrayList-source.html
kingfrito_5005

Answers:


259

在以下情况下,链表优于数组:

  1. 您需要从列表中进行固定时间的插入/删除操作(例如,在实时计算中,时间可预测性至关重要)

  2. 您不知道列表中有多少个项目。对于数组,如果数组太大,可能需要重新声明并复制内存

  3. 您不需要随机访问任何元素

  4. 您希望能够在列表中间插入项目(例如优先级队列)

在以下情况下最好使用数组:

  1. 您需要对元素进行索引/随机访问

  2. 您可以提前知道数组中元素的数量,以便为数组分配正确的内存量

  3. 依次遍历所有元素时需要速度。您可以在数组上使用指针数学来访问每个元素,而您需要基于指针为链接列表中的每个元素查找节点,这可能会导致页面错误并可能导致性能下降。

  4. 内存是一个问题。填充的数组占用的内存少于链接列表。数组中的每个元素只是数据。每个链接列表节点都需要数据以及指向链接列表中其他元素的一个(或多个)指针。

数组列表(如.Net中的数组列表)为您提供了数组的好处,但可以为您动态分配资源,因此您不必担心列表大小,您可以轻松删除任何索引的项目,而无需付出任何努力或重新改组周围的元素。在性能方面,数组列表要比原始数组慢。


7
良好的开端,但是却忽略了重要的事情:列表支持结构共享,数组更密集且位置更好。
大流士培根

1
实际上,数组列表和数组之间的性能差异可以忽略不计。假设您比较可比较的数据,例如,当您事先知道大小时,就将其告知数组列表。
svick

40
因为LinkedList何时才具有O(1)插入/删除(这就是我说的固定时间插入/删除的意思)?将内容插入到LinkedList的中间总是O(n)
Pacerier 2011年

28
如果您恰好位于插入位置(通过迭代器),则LinkedList确实具有O(1)插入。并非总是如此。
亚当

4
将链接列表用于优先级队列是一个非常愚蠢的想法。支持动态数组的堆允许O(lg n)摊销插入和最坏情况的对数delete-min,并且是最快的实用优先级队列结构。
Fred Foo 2013年

54

数组具有O(1)随机访问权,但向其中添加内容或从中删除内容确实非常昂贵。

链表在任何地方添加或删除项以及进行迭代都非常便宜,但是随机访问为O(n)。


3
从数组末尾删除项目是费时的,从链接列表的任一末端插入/删除项目也是如此。在中间...两者都不是。
乔伊,

1
@Joey不是在链表O(n)的末尾插入/删除吗?除非您已经位于倒数第二个链接上,否则您仍然需要O(n)个步骤来找到最后一个元素,不是吗?
Alex Moore-Niemi

@ AlexMoore-Niemi:是的,对于单链接列表。但是许多链接都有前进和后退的链接,因此始终保持指向两端的指针。
乔伊

2
拥有双链表会导致您进行向前和向后搜索,除非您的LL具有有序的值...而且最坏的情况是O(n)
securecurve

“链接列表在任何地方添加或删除项并进行迭代真的很便宜”并不是完全正确的。如果要删除链接列表中间的项目,则必须从头开始进行迭代,直到到达列表中的该项目为止。它的O(n / 2)时间,其中n =列表中的项目数。从您的答案中听起来,您似乎建议像数组中那样的恒定时间O(1)。现在是从链接列表的头/根节点添加/删除的恒定时间。
Yawar Murtaza

21
Algorithm           ArrayList   LinkedList
seek front            O(1)         O(1)
seek back             O(1)         O(1)
seek to index         O(1)         O(N)
insert at front       O(N)         O(1)
insert at back        O(1)         O(1)
insert after an item  O(N)         O(1)

ArrayLists对于一次写入多次读取或追加程序很有用,但不利于从前端或中间进行添加/删除。


14

为了增加其他答案,大多数数组列表实现在列表末尾保留了额外的容量,以便可以在O(1)时间将新元素添加到列表末尾。当超出数组列表的容量时,将在内部分配一个更大的新数组,并复制所有旧元素。通常,新数组的大小是旧数组的两倍。这意味着,这些实现中,平均而言,将新元素添加到数组列表的末尾是O(1)操作。因此,即使您不预先知道元素的数量,只要您在末尾添加元素,数组列表可能仍比链接列表快得多。显然,在数组列表中的任意位置插入新元素仍然是O(n)操作。

即使访问是顺序的,访问数组列表中的元素也比链接列表要快。这是因为数组元素存储在连续的内存中,并且可以轻松地进行缓存。链接列表节点可能会分散在许多不同的页面上。

如果您知道要在任意位置插入或删除项目,则建议仅使用链接列表。数组列表几乎可以处理其他所有内容。


1
此外,您还可以使用动态数组来实现链接列表(在抽象数据类型意义上)。这样,您可以利用计算机缓存,同时在列表的开头进行固定时间的插入和删除的摊销,并且在拥有元素的索引之后必须在列表的中间进行固定时间的插入和删除的摊销。完成或删除元素的索引(无需移位/取消移位)。CLRS 10.3是一个很好的参考。
Domenico De Felice 2014年

7

如果您需要在中间插入项目,而又不想开始调整数组的大小并四处移动,则可以看到列表的优点。

您是正确的,通常情况并非如此。我有一些这样的非常具体的案例,但是没有太多。


当您在中间进行求逆时,实际发生的就是数组的移位和调整大小。如果您未达到摊销范围,则只需要转移而无需调整大小。
securecurve

3

这完全取决于您在迭代时执行的操作类型,所有数据结构都在时间和内存之间进行权衡,并且根据我们的需求,我们应该选择正确的DS。因此,在某些情况下,LinkedList比array更快,反之亦然。考虑数据结构的三个基本操作。

  • 正在搜寻

由于数组是基于索引的数据结构,搜索array.get(index)将花费O(1)时间,而链表不是索引DS,因此您需要遍历index,其中index <= n,n是链表的大小,因此,当元素具有随机访问权限时,数组会更快地链接列表。

问:那么这背后的美丽是什么?

由于数组是连续的内存块,因此它们的大块将在首次访问时加载到高速缓存中,这使得访问数组的其余元素相对较快,因为我们在引用数组中访问元素的位置也增加了,因此捕获较少未命中,高速缓存局部性是指操作在高速缓存中,因此与在内存中的执行相比,执行起来要快得多。基本上,在数组中,我们最大化了顺序元素访问在高速缓存中的机会。尽管链接列表不一定位于连续的内存块中,但不能保证列表中顺序出现的项实际上在内存中彼此排列得很近,这意味着更少的缓存命中率,例如

  • 插入

在LinkedList中这很容易并且快速,因为与数组相比,在LinkedList中插入O(1)操作(在Java中)(考虑数组的情况),如果数组已满,则需要将内容复制到新数组中,这使得插入在最坏的情况下,将元素插入O(n)的ArrayList中,而如果在数组末尾插入任何内容,则ArrayList还需要更新其索引,如果是链表,我们不需要调整其大小,则只需更新指针。

  • 删除中

它像插入一样工作,并且在LinkedList中比数组更好。


2

这些是Collection的最常用实现。

数组列表:

  • 通常在末尾插入/删除O(1)最坏情况O(n)

  • 在中间O(n)中插入/删除

  • 检索任何位置O(1)

链表:

  • 在O(1)的任何位置插入/删除(请注意是否引用了该元素)

  • 在中间O(n)中检索

  • 检索第一个或最后一个元素O(1)

向量:请勿使用。这是一个与ArrayList类似的旧实现,但是所有方法都已同步。对于多线程环境中的共享列表,这不是正确的方法。

哈希图

通过O(1)中的键插入/删除/检索

TreeSet 插入/删除/包含O(log N)

HashSet 在O(1)中插入/删除/包含/大小


1

实际上,内存局部在实际处理中具有巨大的性能影响。

与“随机访问”相比,“大数据”处理中越来越多地使用磁盘流,这表明在此基础上对应用程序进行结构化可以如何极大地提高性能。

如果有任何顺序访问数组的方法,到目前为止,这是性能最好的。如果性能很重要,则至少应考虑以此为目标进行设计。


0

嗯,我可以在以下情况下使用Arraylist:

  1. 您不确定会出现多少个元素
  2. 但是您需要通过索引随机访问所有元素

例如,您需要导入和访问联系人列表中的所有元素(大小未知)


0

将链接列表用于对数组进行基数排序和多项式运算。


0

1)如上所述,与ArrayList(O(n))相比,插入和删除操作在LinkedList中具有良好的性能(O(1))。因此,如果需要在应用程序中频繁添加和删除,那么LinkedList是最佳选择。

2)搜索(获取方法)操作在Arraylist(O(1))中很快,但在LinkedList(O(n))中却不快,因此,如果添加和删除操作较少,而对搜索操作的需求更多,则ArrayList将是您最好的选择。


0

我认为主要区别在于您是否经常需要从列表顶部插入或删除内容。

对于数组,如果从列表顶部删除某些内容,则复杂度为o(n),因为数组元素的所有索引都必须移动。

对于链接列表,它为o(1),因为您只需要创建节点,重新分配头并将对下一个的引用分配为前一个头。

当在列表的末尾频繁插入或删除时,数组是可取的,因为复杂度为o(1),不需要重新索引,但是对于链接列表,它将为o(n),因为您需要从头开始到最后一个节点。

我认为在链接列表和数组中搜索都将是o(log n),因为您可能正在使用二进制搜索。


0

我做了一些基准测试,发现对于随机插入,列表类实际上比LinkedList快:

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            int count = 20000;
            Random rand = new Random(12345);

            Stopwatch watch = Stopwatch.StartNew();
            LinkedList<int> ll = new LinkedList<int>();
            ll.AddLast(0);
            for (int i = 1; i < count; i++)
            {
                ll.AddBefore(ll.Find(rand.Next(i)),i);

            }
            Console.WriteLine("LinkedList/Random Add: {0}ms", watch.ElapsedMilliseconds);

            watch = Stopwatch.StartNew();
            List<int> list = new List<int>();
            list.Add(0);
            for (int i = 1; i < count; i++)
            {
                list.Insert(list.IndexOf(rand.Next(i)), i);

            }
            Console.WriteLine("List/Random Add: {0}ms", watch.ElapsedMilliseconds);

            Console.ReadLine();
        }
    }
}

链接列表需要900毫秒,列表类需要100毫秒。

它创建后续整数的列表。每个新整数都插入列表中已经存在的随机数之后。也许List类使用的不仅仅是数组。


列表是一个接口,而不是一个类
borgmater19年

0

到目前为止,数组是使用最广泛的数据结构。但是,链表以其独特的方式被证明很有用,在这种情况下,数组显得笨拙-或至少可以说是昂贵的。

链接列表对于在大小可能有所变化的情况下实现堆栈和队列很有用。可以推送或弹出链接列表中的每个节点,而不会打扰大多数节点。在中间某处插入/删除节点也是如此。但是,在数组中,所有元素都必须移位,就执行时间而言,这是一项昂贵的工作。

二进制树和二进制搜索树,哈希表和尝试是其中的一些数据结构-至少在C语言中,您需要链表作为构建它们的基本要素。

但是,在希望能够通过其索引调用任意元素的情况下,应避免使用链表。


0

可以使用以下几点来简单地回答该问题:

  1. 当需要收集相似类型的数据元素时,将使用数组。而链接列表是混合类型数据链接元素(称为节点)的集合。

  2. 在数组中,可以在O(1)时间内访问任何元素。而在链表中,我们需要从头到需要的节点遍历整个链表,所需时间为O(n)。

  3. 对于数组,最初需要声明特定大小。但是链接列表的大小是动态的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.