什么时候应该使用列表与LinkedList


Answers:


107

编辑

请阅读对此答案的评论。人们声称我没有做适当的测试。我同意这不是一个可以接受的答案。在学习的过程中,我做了一些测试,觉得很喜欢分享。

原始答案...

我发现了有趣的结果:

// Temporary class to show the example
class Temp
{
    public decimal A, B, C, D;

    public Temp(decimal a, decimal b, decimal c, decimal d)
    {
        A = a;            B = b;            C = c;            D = d;
    }
}

链接列表(3.9秒)

        LinkedList<Temp> list = new LinkedList<Temp>();

        for (var i = 0; i < 12345678; i++)
        {
            var a = new Temp(i, i, i, i);
            list.AddLast(a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

清单(2.4秒)

        List<Temp> list = new List<Temp>(); // 2.4 seconds

        for (var i = 0; i < 12345678; i++)
        {
            var a = new Temp(i, i, i, i);
            list.Add(a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

即使您仅本质上访问数据也要慢得多!我说永远不要使用linkedList。




这是另一个执行大量插入操作的比较(我们计划在列表的中间插入一个项目)

链接列表(51秒)

        LinkedList<Temp> list = new LinkedList<Temp>();

        for (var i = 0; i < 123456; i++)
        {
            var a = new Temp(i, i, i, i);

            list.AddLast(a);
            var curNode = list.First;

            for (var k = 0; k < i/2; k++) // In order to insert a node at the middle of the list we need to find it
                curNode = curNode.Next;

            list.AddAfter(curNode, a); // Insert it after
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

清单(7.26秒)

        List<Temp> list = new List<Temp>();

        for (var i = 0; i < 123456; i++)
        {
            var a = new Temp(i, i, i, i);

            list.Insert(i / 2, a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

链接列表具有插入位置的引用(.04秒)

        list.AddLast(new Temp(1,1,1,1));
        var referenceNode = list.First;

        for (var i = 0; i < 123456; i++)
        {
            var a = new Temp(i, i, i, i);

            list.AddLast(a);
            list.AddBefore(referenceNode, a);
        }

        decimal sum = 0;
        foreach (var item in list)
            sum += item.A;

所以,只有当您打算将几个项目,你什么地方有,你打算插入的项目,然后用链表的参考。仅仅因为您必须插入很多项目,并不能因此而使其更快,因为在您想要插入的位置进行搜索需要花费时间。


99
与List相比,LinkedList有一个好处(特定于.net):由于List由内部数组支持,因此将其分配在一个连续的块中。如果该分配的块大小超过85000字节,它将在不可压缩的大对象堆上分配。根据大小,这可能导致堆碎片化,这是一种轻微的内存泄漏形式。
JerKimball 2012年

35
请注意,如果您要添加大量内容(如您在上一个示例中的基本操作)或删除第一个条目,则链接列表几乎总是会更快,因为无需进行搜索或移动/复制。列表需要将所有内容移到某个位置以容纳新项目,因此要先进行O(N)操作。
cHao 2012年

6
为什么在list.AddLast(a);最后两个LinkedList示例中进入循环?我在循环之前执行了一次,就像list.AddLast(new Temp(1,1,1,1));在最后一个LinkedList旁边一样,但是(在我看来)您在循环本身中添加了两倍的Temp对象。(当我用一个测试应用程序仔细检查自己时,肯定是LinkedList中的两倍。)
ruffin

7
我拒绝了这个答案。1)您的一般建议I say never use a linkedList.是有缺陷的,因为您以后的帖子显示。您可能要编辑它。2)您打算什么时间?实例化,加法和枚举总共需要一步?通常,实例化和枚举并不是ppl所担心的,那只是一步之遥。专门安排插入和添加的时间会更好。3)最重要的是,您向链表添加的内容超出了要求。这是一个错误的比较。传播关于链表的错误想法。
nawfal 2014年

47
抱歉,这个答案真的不好。请不要听这个答案。简而言之:认为数组支持的列表实现愚蠢到足以在每次插入时调整数组大小是完全有缺陷的。遍历以及在两端插入时,链表自然比后备列表慢,因为链表仅需要创建新对象,而后备列表使用缓冲区(显然是双向的)。(做得不好)基准测试准确地表明了这一点。答案完全无法检查首选链表的情况!
mafu 2014年

276

在大多数情况下,List<T>它更为有用。LinkedList<T>在列表中间添加/删除项目时,其成本会降低,而List<T>只能在列表末尾廉价添加/删除。

LinkedList<T>仅在访问顺序数据(正向或反向)时才最有效-随机访问相对昂贵,因为随机访问每次都必须遍历整个链(因此为什么它没有索引器)。但是,因为a List<T>本质上只是一个数组(带有包装器),所以随机访问是可以的。

List<T>还提供了很多的支持方法- FindToArray等; 但是,这些也可以LinkedList<T>通过扩展方法与.NET 3.5 / C#3.0一起使用-因此影响较小。


4
我从没想过List <> vs. LinkedList <>的优点之一就是微处理器如何实现内存缓存。尽管我不太了解,但是这篇博客文章的作者谈论了很多“引用的局部性”,这使得遍历数组比遍历链表快得多,至少在链表在内存中变得零散的情况下更是如此。 。kjellkod.wordpress.com/2012/02/25/...
RenniePet

@RenniePet列表是通过动态数组实现的,并且数组是连续的内存块。
Casey 2015年

2
由于List是一个动态数组,因此如果事先知道List的容量,那么有时最好在构造函数中指定List的容量。
Cardin Lee JH 2015年

在一个非常重要的情况下,所有数组,List <T>和LinkedList <T>的C#实现是否有可能不是次优的:您需要一个非常大的列表,append(AddLast)和顺序遍历(在一个方向)是完全没问题:我不希望调整数组大小来获得连续的块(是否保证每个数组,甚至20 GB的数组?),我事先不知道大小,但是我可以预先猜测一个块的大小,例如100 MB可以提前预约。这将是一个很好的实现。还是array / List与此相似,我错过了一点?
Philm

1
@Philm就是这种情况,您可以针对所选的块策略编写自己的垫片;会因为太粗大List<T>T[]失败(全部为一个平板),LinkedList<T>会因为太细而失败(每个元素都为平板)。
马克·格雷韦尔

212

将链接列表视为列表可能会产生误导。它更像一条链。实际上,在.NET中,LinkedList<T>甚至没有实现IList<T>。链表中没有索引的真正概念,即使看起来确实存在。当然,该类上提供的所有方法都不接受索引。

链接列表可以是单链接,也可以是双链接。这指的是链中的每个元素都仅具有指向下一个元素的链接(单链接),还是与先前/后一个元素的链接(双链接)。 LinkedList<T>是双重链接。

在内部,List<T>由数组支持。这在内存中提供了非常紧凑的表示形式。相反,LinkedList<T>涉及额外的内存来存储连续元素之间的双向链接。因此,a的内存占用空间LinkedList<T>通常会大于内存占用空间List<T>(需要注意的是,List<T>可以使用未使用的内部数组元素来提高追加操作期间的性能)。

它们也具有不同的性能特征:

附加

  • LinkedList<T>.AddLast(item) 恒定时间
  • List<T>.Add(item) 摊销固定时间,线性最坏情况

前置

  • LinkedList<T>.AddFirst(item) 恒定时间
  • List<T>.Insert(0, item) 线性时间

插入

  • LinkedList<T>.AddBefore(node, item) 恒定时间
  • LinkedList<T>.AddAfter(node, item) 恒定时间
  • List<T>.Insert(index, item) 线性时间

清除

  • LinkedList<T>.Remove(item) 线性时间
  • LinkedList<T>.Remove(node) 恒定时间
  • List<T>.Remove(item) 线性时间
  • List<T>.RemoveAt(index) 线性时间

计数

  • LinkedList<T>.Count 恒定时间
  • List<T>.Count 恒定时间

包含

  • LinkedList<T>.Contains(item) 线性时间
  • List<T>.Contains(item) 线性时间

明确

  • LinkedList<T>.Clear() 线性时间
  • List<T>.Clear() 线性时间

如您所见,它们几乎是等效的。实际上,LinkedList<T>使用起来比较麻烦,并且其内部需求的详细信息会溢出到您的代码中。

但是,如果您需要从列表中进行多次插入/删除操作,它将提供恒定的时间。 List<T>提供线性时间,因为列表中的其他项目在插入/移除后必须重新排列。


2
链表计数是否恒定?我以为那是线性的?
伊恩·巴拉德

10
@Iain,计数被缓存在两个列表类中。
Drew Noakes

3
您写了“ List <T> .Add(item)对数时间”,但是,如果列表容量可以存储新项目,则实际上是“恒定”,如果列表没有足够的空间和新空间,则实际上是“线性”重新分配。
aStranger 2012年

@aStranger,您当然是对的。不知道上面我在想什么-也许摊销的正常案件时间是对数的,但事实并非如此。实际上,摊销时间是恒定的。为了进行简单比较,我没有涉及最佳/最差的操作情况。我认为添加操作足以提供此详细信息。将编辑答案。谢谢。
德鲁·诺阿克斯

1
@Philm,您可能应该提出一个新的问题,并且不必说一旦建立就将如何使用此数据结构,但是如果要说的是一百万行,则可能需要某种混合(链接列表)数组块或类似的块)以减少堆碎片,减少内存开销并避免LOH上的单个大对象。
Drew Noakes

118

链接列表可以非常快速地插入或删除列表成员。链接列表中的每个成员都包含一个指向列表中下一个成员的指针,以便在位置i处插入一个成员:

  • 更新成员i-1中的指针以指向新成员
  • 将新成员中的指针设置为指向成员i

链表的缺点是无法进行随机访问。访问成员需要遍历列表,直到找到所需的成员。


6
我要添加的是,链表在上面引用了上一个和下一个节点的LinkedListNode隐含了每个项目的开销。与基于数组的列表不同,存储列表不需要连续的内存块。
paulecoyote

3
通常不是连续的内存块吗?
乔纳森·艾伦,2010年

7
是的,连续块是随机访问性能和内存消耗的首选,但对于需要定期更改大小的集合,通常需要将诸如Array之类的结构复制到新位置,而链表仅需要管理该对象的内存。新插入/删除的节点。
jpierson

6
如果必须使用非常大的数组或列表(列表只是包装一个数组),即使计算机上似乎有足够的可用内存,您也将开始遇到内存问题。当列表在其基础数组中分配新空间时,它会使用一种加倍策略。因此,已满的1000000个元素数组将被复制到具有2000000个元素的新数组中。需要在足够大的连续内存空间中创建该新数组,以容纳它。
安德鲁

1
我有一个特定的案例,我所做的就是添加和删除,以及一个接一个地循环...在这里,链表比普通列表要优越得多
彼得

26

我以前的答案不够准确。确实如此,这很可怕:D但现在我可以发布更多有用且正确的答案。


我做了一些额外的测试。您可以通过以下链接找到其来源,然后通过自己的环境在其上重新检查它:https : //github.com/ukushu/DataStructuresTestsAndOther.git

结果简短:

  • 数组需要使用:

    • 尽可能经常。它速度快,并且占用最小的RAM范围即可获得相同数量的信息。
    • 如果您知道所需的确切细胞数
    • 如果数据保存在数组<85000 b中(85000/32 = 2656个元素用于整数数据)
    • 如果需要,则随机访问速度较高
  • 清单需要使用:

    • 如果需要将单元格添加到列表的末尾(通常)
    • 如果需要在列表的开头/中间添加单元格(不要经常)
    • 如果数据保存在数组<85000 b中(85000/32 = 2656个元素用于整数数据)
    • 如果需要,则随机访问速度较高
  • LinkedList需要使用:

    • 如果需要在列表的开头/中间/结尾添加单元格(通常)
    • 如果需要,仅顺序访问(向前/向后)
    • 如果您需要保存大项目,但项目数量很少。
    • 最好不要用于大量项目,因为它会为链接使用额外的内存。

更多细节:

введитесюдаописаниеизображения 有趣的是:

  1. LinkedList<T>内部不是.NET中的列表。它甚至没有实现IList<T>。这就是为什么缺少索引和与索引相关的方法的原因。

  2. LinkedList<T>是基于节点指针的集合。在.NET中,它处于双链接实现中。这意味着先前/下一个元素具有到当前元素的链接。而且数据是零散的-不同的列表对象可以位于RAM的不同位置。此外,用于的内存LinkedList<T>将比用于List<T>或的更多。

  3. List<T>.Net中的Java是Java的替代ArrayList<T>。这意味着这是数组包装器。因此,它作为一个连续的数据块在内存中分配。如果分配的数据大小超过85000字节,它将被移至大对象堆。根据大小,这可能导致堆碎片(一种轻微的内存泄漏形式)。但是同时如果大小<85000字节-这将在内存中提供非常紧凑和快速访问的表示形式。

  4. 单个连续块是随机访问性能和内存消耗的首选,但对于需要定期更改大小的集合,通常需要将诸如Array之类的结构复制到新位置,而链表仅需要管理新插入的内存/删除的节点。


1
问题:“将数据保存在数组<或> 85.000字节中”是指每个数组/列表的数据是否是ELEMENT?可以理解,您的意思是整个数组的数据
大小。– Philm

数组元素顺序位于内存中。所以每个数组。我知道表中的错误,稍后我将修复它:)(我希望....)
Andrew

如果列表的插入速度很慢,则如果列表的周转时间很多(大量插入/删除)会保留已删除空间所占用的内存,如果这样,“重新”插入会更快吗?
罗布

18

List和LinkedList之间的区别在于它们的基础实现。List是基于数组的集合(ArrayList)。LinkedList是基于节点指针的集合(LinkedListNode)。在API级别的用法上,它们两者几乎相同,因为它们都实现了同一组接口,例如ICollection,IEnumerable等。

关键区别在于性能。例如,如果要实现具有大量“ INSERT”操作的列表,则LinkedList的性能将优于List。由于LinkedList可以在O(1)时间内完成,因此List可能需要扩展基础数组的大小。有关更多信息/详细信息,您可能需要阅读LinkedList和数组数据结构之间的算法差异。http://zh.wikipedia.org/wiki/链接列表数组

希望有帮助,


4
List <T>是基于数组(T []),而不是基于ArrayList。重新插入:数组调整大小不是问题(加倍算法意味着大多数时候不必这样做):问题是必须首先块复制所有现有数据,这需要一点时间时间。
马克·格雷韦尔

2
@Marc,“加倍算法”仅使其变为O(logN),但仍比O(1)差
Ilya Ryzhenkov

2
我的观点是,调整大小不是造成疼痛的原因,而是出血。所以最坏的情况是,如果我们每次都添加第一个(第零个)元素,那么blit必须每次都移动所有内容。
马克·Gravell

@IlyaRyzhenkov-您正在考虑Add总是在现有数组末尾的情况。List即使不是O(1),也“足够好”。如果你需要很多会出现严重的问题,AddS中的不是底。Marc指出,每次插入时都需要移动现有数据(而不仅是在需要调整大小时),这是一个更大的性能成本。List
制造商史蒂夫

问题在于,理论上的大O符号不能说明全部情况。在计算机科学中,任何人都在乎,但是在现实世界中,有更多的事情要关注。
MattE

11

链接列表相对于数组的主要优点是,链接使我们能够高效地重新排列项目。塞奇威克 91


1
海事组织,这应该是答案。当保证顺序很重要时,将使用LinkedList。
RBaarda '16

1
@RBaarda:我不同意。这取决于我们所谈论的水平。算法级别与机器实现级别不同。考虑到速度,您也需要后者。如所指出的那样,将数组实现为内存的“一个块”,这是一个限制,因为这可能导致调整大小和内存重组,特别是对于非常大的数组。考虑了一会儿之后,一个特殊的自己的数据结构,一个数组的链表将是一个更好地控制线性填充速度和访问非常大的数据结构的想法。
Philm

1
@Philm-我赞成您的评论,但我想指出您在描述一个不同的要求。答案是说,链表对于涉及大量项目重新排列的算法具有性能优势。鉴于此,我将RBaarda的评论解释为需要添加/删除项目,同时持续保持给定的排序(排序标准)。因此,不仅是“线性填充”。在这种情况下,List会失败,因为索引是无用的(每次在末尾添加元素时都需要更改)。
制造商史蒂夫

4

使用LinkedList的常见情况是这样的:

假设您要从一个较大的字符串列表中删除许多特定的字符串,例如100,000。可以在HashSet dic中查找要删除的字符串,并且认为字符串列表包含30,000至60,000之间的此类字符串。

那么,存储100,000个字符串的最佳列表类型是什么?答案是LinkedList。如果它们存储在ArrayList中,则对其进行迭代并删除匹配的String,这些String最多需要执行数十亿次操作,而使用迭代器和remove()方法仅需要进行大约100,000次操作。

LinkedList<String> strings = readStrings();
HashSet<String> dic = readDic();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
    String string = iterator.next();
    if (dic.contains(string))
    iterator.remove();
}

6
您可以简单地RemoveAll从中删除项目List而不用移动很多项目,也可以使用WhereLINQ从中创建第二个列表。使用LinkedList此然而最终消费显着高于其他类型的集合和内存局部性手段损失更多的内存,这将是明显慢于迭代,使它颇有几分比一个更坏List
2014年

@Servy,请注意,@ Tom的答案使用Java。我不确定RemoveAllJava 是否有等效功能。
Arturo TorresSánchez2014年

3
@ArturoTorresSánchez这个问题专门指出它是关于.NET的,因此这使得答案不太合适。
Servy 2014年

@Servy,那么您应该从一开始就提到它。
Arturo TorresSánchez2014年

如果RemoveAll不适用于List,则可以执行“压缩”算法,该算法看起来像汤姆的循环,但是有两个索引,并且需要将项目一次移动到列表的内部数组中。效率为O(n),与Tom的的算法相同LinkedList。在这两个版本中,为字符串计算HashSet键的时间均占主导。这不是何时使用的好例子LinkedList
制造商史蒂夫


2

本质上,List<>.NET中的a是数组的包装。A LinkedList<> 是一个链表。所以问题归结为,数组和链表之间有什么区别,何时应使用数组代替链表。在决定使用哪个最重要的因素时,可能归结为:

  • 链接列表具有更好的插入/删除性能,只要插入/删除不在集合中的最后一个元素上即可。这是因为数组必须移位插入/删除点之后的所有剩余元素。但是,如果插入/删除在列表的末尾,则不需要此移位(尽管如果超出其容量,则可能需要调整阵列的大小)。
  • 阵列具有更好的访问功能。数组可以直接索引(恒定时间)。必须遍历链接列表(线性时间)。

1

改编自Tono Nam接受的答案,其中纠正了一些错误的度量。

考试:

static void Main()
{
    LinkedListPerformance.AddFirst_List(); // 12028 ms
    LinkedListPerformance.AddFirst_LinkedList(); // 33 ms

    LinkedListPerformance.AddLast_List(); // 33 ms
    LinkedListPerformance.AddLast_LinkedList(); // 32 ms

    LinkedListPerformance.Enumerate_List(); // 1.08 ms
    LinkedListPerformance.Enumerate_LinkedList(); // 3.4 ms

    //I tried below as fun exercise - not very meaningful, see code
    //sort of equivalent to insertion when having the reference to middle node

    LinkedListPerformance.AddMiddle_List(); // 5724 ms
    LinkedListPerformance.AddMiddle_LinkedList1(); // 36 ms
    LinkedListPerformance.AddMiddle_LinkedList2(); // 32 ms
    LinkedListPerformance.AddMiddle_LinkedList3(); // 454 ms

    Environment.Exit(-1);
}

和代码:

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

namespace stackoverflow
{
    static class LinkedListPerformance
    {
        class Temp
        {
            public decimal A, B, C, D;

            public Temp(decimal a, decimal b, decimal c, decimal d)
            {
                A = a; B = b; C = c; D = d;
            }
        }



        static readonly int start = 0;
        static readonly int end = 123456;
        static readonly IEnumerable<Temp> query = Enumerable.Range(start, end - start).Select(temp);

        static Temp temp(int i)
        {
            return new Temp(i, i, i, i);
        }

        static void StopAndPrint(this Stopwatch watch)
        {
            watch.Stop();
            Console.WriteLine(watch.Elapsed.TotalMilliseconds);
        }

        public static void AddFirst_List()
        {
            var list = new List<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
                list.Insert(0, temp(i));

            watch.StopAndPrint();
        }

        public static void AddFirst_LinkedList()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (int i = start; i < end; i++)
                list.AddFirst(temp(i));

            watch.StopAndPrint();
        }

        public static void AddLast_List()
        {
            var list = new List<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
                list.Add(temp(i));

            watch.StopAndPrint();
        }

        public static void AddLast_LinkedList()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (int i = start; i < end; i++)
                list.AddLast(temp(i));

            watch.StopAndPrint();
        }

        public static void Enumerate_List()
        {
            var list = new List<Temp>(query);
            var watch = Stopwatch.StartNew();

            foreach (var item in list)
            {

            }

            watch.StopAndPrint();
        }

        public static void Enumerate_LinkedList()
        {
            var list = new LinkedList<Temp>(query);
            var watch = Stopwatch.StartNew();

            foreach (var item in list)
            {

            }

            watch.StopAndPrint();
        }

        //for the fun of it, I tried to time inserting to the middle of 
        //linked list - this is by no means a realistic scenario! or may be 
        //these make sense if you assume you have the reference to middle node

        //insertion to the middle of list
        public static void AddMiddle_List()
        {
            var list = new List<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
                list.Insert(list.Count / 2, temp(i));

            watch.StopAndPrint();
        }

        //insertion in linked list in such a fashion that 
        //it has the same effect as inserting into the middle of list
        public static void AddMiddle_LinkedList1()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            LinkedListNode<Temp> evenNode = null, oddNode = null;
            for (int i = start; i < end; i++)
            {
                if (list.Count == 0)
                    oddNode = evenNode = list.AddLast(temp(i));
                else
                    if (list.Count % 2 == 1)
                        oddNode = list.AddBefore(evenNode, temp(i));
                    else
                        evenNode = list.AddAfter(oddNode, temp(i));
            }

            watch.StopAndPrint();
        }

        //another hacky way
        public static void AddMiddle_LinkedList2()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start + 1; i < end; i += 2)
                list.AddLast(temp(i));
            for (int i = end - 2; i >= 0; i -= 2)
                list.AddLast(temp(i));

            watch.StopAndPrint();
        }

        //OP's original more sensible approach, but I tried to filter out
        //the intermediate iteration cost in finding the middle node.
        public static void AddMiddle_LinkedList3()
        {
            var list = new LinkedList<Temp>();
            var watch = Stopwatch.StartNew();

            for (var i = start; i < end; i++)
            {
                if (list.Count == 0)
                    list.AddLast(temp(i));
                else
                {
                    watch.Stop();
                    var curNode = list.First;
                    for (var j = 0; j < list.Count / 2; j++)
                        curNode = curNode.Next;
                    watch.Start();

                    list.AddBefore(curNode, temp(i));
                }
            }

            watch.StopAndPrint();
        }
    }
}

您可以看到结果与此处记录的其他理论性能相符。很清楚- LinkedList<T>插入时可节省大量时间。我还没有测试从列表中间删除,但是结果应该是相同的。当然,还有List<T>其他一些区域的性能更好,例如O(1)随机访问。


0

使用LinkedList<>

  1. 您不知道有多少物体通过水闸。例如,Token Stream
  2. 当您只想在末尾删除\插入时。

对于其他所有内容,最好使用List<>


6
我不明白为什么第二点有意义。当您在整个列表中进行多次插入/删除操作时,链接列表非常有用。
德鲁·诺阿克斯

由于LinkedList不是基于索引的事实,您实际上必须扫描整个列表以查找插入或删除,这会导致O(n)损失。另一方面,List <>受数组大小调整的困扰,但与LinkedLists相比,IMO是更好的选择。
安东尼·托马斯

1
如果您跟踪LinkedListNode<T>代码中的对象,则不必扫描列表中的插入/删除操作。如果可以这样做,那将比使用更好List<T>,特别是对于很频繁插入/删除的很长的列表。
德鲁·诺阿克斯

你是说通过哈希表?如果是这样,那将是每个计算机程序员都应根据问题域进行选择的典型时空\时间权衡:)但是,这样可以使其更快。
安东尼·托马斯

1
@AntonyThomas-不,他的意思是通过传递对节点的引用而不是传递对element的引用。如果你已经是一个元素,那么这两个名单和LinkedList有不好的表现,因为你要搜索。如果您认为“但可以使用List,但我只能传递索引”:仅当您从未在List的中间插入新元素时才有效。如果您坚持一个节点(并node.Value在需要原始元素的任何时候使用),LinkedList没有此限制。因此,您重写算法以使用节点,而不是原始值。
制造商史蒂夫

0

我确实同意以上大部分观点。我也同意,在大多数情况下,List似乎是一个更明显的选择。

但是,我只想补充一下,在很多情况下,LinkedList比List更好的选择是为了提高效率。

  1. 假设您正在遍历元素,并且想要执行很多插入/删除操作;LinkedList在线性O(n)时间中完成,而List在二次O(n ^ 2)时间中完成。
  2. 假设您想一次又一次地访问更大的对象,LinkedList变得非常有用。
  3. 使用LinkedList可以更好地实现Deque()和queue()。
  4. 一旦处理许多更大的对象,增加LinkedList的大小就容易得多,而且效果更好。

希望有人会觉得这些评论有用。


请注意,此建议适用于.NET,而不适用于Java。在Java的链表实现中,您没有“当前节点”的概念,因此必须遍历每个插入的列表。
乔纳森·艾伦

这个答案仅部分正确:2)如果元素很大,则使元素类型为Class而不是Struct,以便List仅保存引用。然后,元素大小变得无关紧要。3)如果将List用作“循环缓冲区”,而不是在开始时进行插入或删除,可以在List中有效地完成双端队列和队列 斯蒂芬·克莱里(StevenCleary)的双双。4)部分正确:当对象很多时,LL的pro不需要巨大的连续内存;缺点是节点指针的额外内存。
制造商史蒂夫

-2

这里有很多平均答案...

一些链表实现使用预分配节点的基础块。如果他们不这样做,那么固定时间/线性时间就没有那么重要了,因为内存性能会很差,缓存性能会更差。

在以下情况下使用链接列表

1)您想要线程安全。您可以构建更好的线程安全算法。锁定成本将主导并发样式列表。

2)如果您的队列很长,例如结构,并且想要一直删除或添加除末尾之外的所有内容。存在超过10万个列表,但并不常见。


3
这个问题是关于两个C#实现的,而不是一般的链表。
乔纳森·艾伦,

每种语言都一样
user1496062

-2

我问了一个与LinkedList集合的性能有关类似问题,发现Steven Cleary的Deque的C#实现是一种解决方案。与队列集合不同,Deque允许前后移动项目。它类似于链表,但性能有所提高。


1
重新您的声明Deque“类似于链表,但是具有更高的性能”。请有资格的发言:Deque是不是更好的性能LinkedList为您的特定代码。在链接之后,我看到两天后,您从Ivan Stoev那里了解到这不是LinkedList的低效率,而是代码中的低效率。(即使是LinkedList的效率低下,也不能证明Deque更有效;这只是在特定情况下的一般说法。)
ToolmakerSteve18年
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.