【问题标题】:When should I use a List vs a LinkedList什么时候应该使用 List 和 LinkedList
【发布时间】:2010-09-15 06:14:30
【问题描述】:

什么时候使用ListLinkedList 比较好?

【问题讨论】:

  • Java q,应该不会有太大的不同。
  • @jonathan-allen,请考虑更改接受的答案。当前的版本不准确且极具误导性。
  • 正如 Xpleria 所说,请考虑更改当前接受的答案。电流具有误导性。

标签: c# .net vb.net data-structures linked-list


【解决方案1】:

在大多数情况下,List<T> 更有用。 LinkedList<T> 在列表中间添加/删除项目时成本较低,而 List<T> 只能在列表的 end 廉价添加/删除。

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

List<T>还提供了很多支持方式——FindToArray等;但是,这些也可通过扩展方法用于带有 .NET 3.5/C# 3.0 的 LinkedList<T> - 所以这不是一个因素。

【讨论】:

  • List 与 LinkedList 的一个优势,我从未想过微处理器如何实现内存缓存。虽然我没有完全理解,但是这篇博文的作者讲了很多关于“引用的局部性”,这使得遍历数组比遍历链表快得多,至少如果链表列表在内存中变得有些碎片化。 kjellkod.wordpress.com/2012/02/25/…
  • @RenniePet List 是用动态数组实现的,数组是连续的内存块。
  • 由于 List 是一个动态数组,这就是为什么如果事先知道的话,有时最好在构造函数中指定 List 的容量。
  • 对于一个非常重要的情况,all、array、List 和 LinkedList 的 C# 实现是否可能不是最理想的:您需要一个非常大的列表,追加 (AddLast) 和顺序遍历(在一个方向上)完全没问题:我不希望调整数组大小来获得连续块(是否保证每个数组,甚至 20 GB 数组?),我事先不知道大小,但我可以猜到提前一个块大小,例如每次提前预留 100 MB。这将是一个很好的实现。还是array/List类似,我漏了点?
  • @Philm 在这种情况下,您可以在选择的块策略上编写自己的 shim; List<T>T[] 会因为太粗(都是一块板)而失败,LinkedList<T> 会因为太细(每个元素的板)而哀号。
【解决方案2】:

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

链表可以是单链的,也可以是双链的。这是指链中的每个元素是否仅与下一个元素(单链接)或前一个/下一个元素(双链接)有链接。 LinkedList<T> 是双向链接的。

在内部,List<T> 由数组支持。这在内存中提供了非常紧凑的表示。相反,LinkedList<T> 涉及额外的内存来存储连续元素之间的双向链接。所以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> 的 API 使用起来更加麻烦,其内部需求的细节会溢出到您的代码中。

但是,如果您需要从列表中进行多次插入/删除,它会提供恒定的时间。 List<T> 提供线性时间,因为列表中的额外项目必须在插入/删除后随机排列。

【讨论】:

  • 计数链表是常数吗?我以为那是线性的?
  • @Iain,计数缓存在两个列表类中。
  • 你写的是“List.Add(item) logarithmic time”,但是如果列表容量可以存储新项目,它实际上是“常数”,如果列表容量是“线性”没有足够的空间和新的重新分配。
  • 我在一些结论中看到了一个矛盾:鉴于我只关心 Append 的速度,什么是最好的?我想用几百万个文本行(或任何其他流)填充容器,但我不关心 RAM:我只需要关心速度 Append(.Add 到列表的末尾)。这是最重要(规范)的情况,中间的插入是别的东西: ----- 使用 LinkedList 或 List 更好吗??
  • @Philm,您可能应该开始一个新问题,并且您没有说构建后将如何使用此数据结构,但是如果您谈论的是一百万行,您可能会喜欢某种混合(数组块或类似的链表)来减少堆碎片,减少内存开销,并避免 LOH 上的单个巨大对象。
【解决方案3】:

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

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

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

【讨论】:

  • 我要补充一点,链接列表通过 LinkedListNode 引用前一个节点和下一个节点在上面存储的每个项目都有开销。这样做的好处是不需要连续的内存块来存储列表,这与基于数组的列表不同。
  • 通常不应该使用连续的内存块吗?
  • 是的,连续块对于随机访问性能和内存消耗是首选,但对于需要定期更改大小的集合,通常需要将结构(如数组)复制到新位置,而链表只需要管理新插入/删除节点的内存。
  • 如果您曾经不得不使用非常大的数组或列表(列表只是包装了一个数组),即使您的机器上似乎有足够的可用内存,您也会开始遇到内存问题.该列表在其底层数组中分配新空间时使用加倍策略。因此,一个 1000000 个已满的元素数组将被复制到一个具有 2000000 个元素的新数组中。这个新数组需要在一个足够大的连续内存空间中创建。
  • 我有一个特殊情况,我所做的只是添加和删除,然后一个接一个地循环......这里的链表远远优于普通列表......
【解决方案4】:

编辑

请阅读该答案的 cmets。人们声称我没有做 适当的测试。我同意这不应该是一个公认的答案。像我以前一样 学习我做了一些测试,并想分享它们。

原答案...

我发现了有趣的结果:

// 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;

链接列表具有插入位置的参考(0.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;

因此,只有当您计划插入多个项目并且您在某处有您计划插入项目的位置的参考时,才使用链表。仅仅因为您必须插入很多项目并不能使其更快,因为搜索您要插入的位置需要时间。

【讨论】:

  • LinkedList 比 List 有一个好处(这是 .net 特有的):由于 List 由内部数组支持,因此它被分配在一个连续的块中。如果该分配块的大小超过 85000 字节,它将被分配到大对象堆上,这是一个不可压缩的代。根据大小,这可能会导致堆碎片,这是一种温和的内存泄漏形式。
  • 请注意,如果您在前面添加很多内容(就像您在最后一个示例中所做的那样)或删除第一个条目,则链接列表几乎总是会明显更快,因为没有搜索或移动/复制做。列表需要将所有内容向上移动以容纳新项目,从而预先进行 O(N) 操作。
  • 为什么最后两个 LinkedList 示例中的循环内 list.AddLast(a);?我在循环之前做了一次,就像在最后一个 LinkedList 旁边的 list.AddLast(new Temp(1,1,1,1)); 一样,但它看起来(对我来说)就像你在循环本身中添加了两倍的 Temp 对象。 (而当我double-check myself with a test app 时,果然是LinkedList 中的两倍。)
  • 我否决了这个答案。 1) 你的一般建议I say never use a linkedList. 是有缺陷的,正如你后面的帖子所揭示的那样。您可能想要编辑它。 2)你的时间是什么?一步完成实例化、加法和枚举?大多数情况下,实例化和枚举并不是人们所担心的,它们只是一个时间步骤。具体定时插入和添加会给出一个更好的主意。 3) 最重要的是,您向链表添加的内容超出了要求。这是一个错误的比较。传播关于链表的错误观念。
  • 对不起,但是 这个答案真的很糟糕。请不要听这个答案。 简而言之:认为数组支持的列表实现愚蠢到在每次插入时调整数组大小是完全错误的。在遍历以及在任一端插入时,链表自然比数组支持的列表慢,因为只有它们需要创建新对象,而数组支持的列表使用缓冲区(显然是双向的)。 (做得不好的)基准恰恰表明了这一点。答案完全无法检查链表更可取的情况!
【解决方案5】:

我之前的回答不够准确。 真的很可怕:D 但现在我可以发布更多有用和正确的答案。


我做了一些额外的测试。您可以通过以下链接找到它的源代码,并自行在您的环境中重新检查:https://github.com/ukushu/DataStructuresTestsAndOther.git

短结果:

  • 数组需要使用:

    • 尽可能频繁。它速度很快,并且对于相同数量的信息占用最小的 RAM 范围。
    • 如果您知道所需细胞的准确计数
    • 如果保存在数组中的数据
    • 如果需要高随机访问速度
  • 列表需要使用:

    • 如果需要将单元格添加到列表末尾(经常)
    • 如果需要在列表的开头/中间添加单元格(不经常)
    • 如果保存在数组中的数据
    • 如果需要高随机访问速度
  • LinkedList需要使用:

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

更多详情:

有兴趣了解:

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

  2. LinkedList&lt;T&gt; 是基于节点指针的集合。在 .NET 中,它是双向链接的实现。这意味着先前/下一个元素具有指向当前元素的链接。并且数据是碎片化的——不同的列表对象可以位于 RAM 的不同位置。与List&lt;T&gt; 或数组相比,LinkedList&lt;T&gt; 使用的内存也会更多。

  3. .Net 中的List&lt;T&gt; 是Java 的ArrayList&lt;T&gt; 的替代品。这意味着这是数组包装器。所以它作为一个连续的数据块在内存中分配。如果分配的数据大小超过 85000 字节,它将被移动到大对象堆。根据大小,这可能会导致堆碎片(一种温和的内存泄漏形式)。但同时如果 size

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

【讨论】:

  • 问题:对于“保存在数组中的数据 85.000 字节”,您的意思是每个数组/列表元素的数据是吗?可以理解为你的意思是整个数组的datasize..
  • 在内存中按顺序排列的数组元素。所以每个数组。我知道表中的错误,稍后我会修复它:)(我希望....)
  • 由于列表在插入时速度很慢,如果列表有很多周转(大量插入/删除),则内存被保留的已删除空间占用,如果是这样,这是否会使“重新”插入更快?
【解决方案6】:

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

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

希望对您有所帮助,

【讨论】:

  • List 是基于数组 (T[]),而不是基于 ArrayList。重新插入:数组调整大小不是问题(加倍算法意味着大多数时候它不必这样做):问题是它必须首先块复制所有现有数据,这需要一点时间时间。
  • @Marc,“加倍算法”仅使其 O(logN),但仍然比 O(1) 差
  • 我的意思是,导致痛苦的不是调整大小 - 而是 blit。所以最坏的情况是,如果我们每次都添加第一个(第零个)元素,那么 blit 每次都必须移动所有内容。
  • @IlyaRyzhenkov - 您正在考虑Add 始终位于现有数组末尾的情况。 List 在这方面“足够好”,即使不是 O(1)。如果您需要许多末尾 notAdds,就会出现严重的问题。 Marc 指出,移动 现有数据每次 插入(不仅仅是在需要调整大小时)的需要是List 的更大性能成本。跨度>
  • 问题是理论上的大 O 符号并不能说明全部情况。在计算机科学中,这是所有人都关心的问题,但在现实世界中,需要关心的远不止这些。
【解决方案7】:

与数组相比,链表的主要优点是链接为我们提供了有效地重新排列项目的能力。 塞奇威克,p。 91

【讨论】:

  • IMO 这应该是答案。当保证顺序很重要时,使用 LinkedList。
  • @RBaarda:我不同意。这取决于我们谈论的水平。算法级别不同于机器实现级别。出于速度考虑,您也需要后者。正如所指出的,数组被实现为“一块”内存,这是一个限制,因为这可能导致调整大小和内存重组,特别是对于非常大的数组。经过一段时间的思考,一个特殊的自己的数据结构,一个数组的链表将是一个更好地控制线性填充速度和访问非常大的数据结构的想法。
  • @Philm - 我赞成您的评论,但我想指出您描述的是不同的要求。答案是链表对于涉及大量重新排列项的算法具有性能优势。鉴于此,我将 RBaarda 的评论解释为需要添加/删除项目,同时持续保持给定的排序(排序标准)。所以不仅仅是“线性填充”。鉴于此,List 失败了,因为索引是无用的(每次添加元素时都会更改,但在尾部)。
【解决方案8】:

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

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

那么存储 100,000 个字符串的最佳 List 类型是什么?答案是链表。如果它们存储在 ArrayList 中,则对其进行迭代并删除匹配的字符串 到数十亿次操作,而使用迭代器和 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();
}

【讨论】:

  • 您可以简单地使用RemoveAllList 中删除项目而无需移动大量项目,或者使用来自LINQ 的Where 创建第二个列表。然而,在此处使用LinkedList 最终会比其他类型的集合消耗更多的内存显着,并且内存局部性的丢失意味着它的迭代速度会明显变慢,这使得它比@ 更糟糕987654326@.
  • @Servy,请注意@Tom 的回答使用 Java。我不确定 Java 中是否有 RemoveAll 等价物。
  • @ArturoTorresSánchez 这个问题明确指出它是关于 .NET 的,所以这只是让答案不太合适。
  • @Servy,那么你应该从一开始就提到这一点。
  • 如果RemoveAll 不适用于List,您可以执行“压缩”算法,这看起来像汤姆的循环,但有两个索引并且需要移动项目以保留一个一次在列表的内部数组中。效率为 O(n),与 LinkedList 的 Tom 算法相同。在这两个版本中,为字符串计算 HashSet 键的时间占主导地位。这不是什么时候使用LinkedList 的好例子。
【解决方案9】:

当您需要内置索引访问、排序(以及在此二进制搜索之后)和“ToArray()”方法时,您应该使用 List。

【讨论】:

    【解决方案10】:

    本质上,.NET 中的List&lt;&gt;数组 的包装器。 LinkedList&lt;&gt; 是一个链表。所以问题归结为,数组和链表有什么区别,什么时候应该使用数组而不是链表。您决定使用哪个最重要的因素可能归结为:

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

    【讨论】:

      【解决方案11】:

      这是改编自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&lt;T&gt; 在插入的情况下获得大量时间。我还没有测试从列表中间删除,但结果应该是一样的。当然List&lt;T&gt; 在其他方面表现更好,比如 O(1) 随机访问。

      【讨论】:

        【解决方案12】:

        当使用LinkedList&lt;&gt;

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

        对于其他一切,最好使用List&lt;&gt;

        【讨论】:

        • 我不明白为什么第 2 点有意义。当您在整个列表中进行多次插入/删除时,链表非常有用。
        • 由于 LinkedLists 不是基于索引的事实,您确实必须扫描整个列表以进行插入或删除,这会导致 O(n) 损失。另一方面,List 会受到数组大小调整的影响,但与 LinkedLists 相比,IMO 仍然是一个更好的选择。
        • 如果您跟踪代码中的 LinkedListNode&lt;T&gt; 对象,则不必扫描列表中的插入/删除。如果你能做到这一点,那么它比使用List&lt;T&gt; 好得多,尤其是对于插入/删除频繁的非常长的列表。
        • 你的意思是通过哈希表?如果是这样的话,这将是每个计算机程序员应该根据问题域做出选择的典型空间\时间权衡:) 但是,是的,这会使其更快。
        • @AntonyThomas - 不,他的意思是传递对节点的引用,而不是传递对元素的引用。如果你只有一个元素,那么 List 和LinkedList 的性能都很差,因为你必须搜索。如果您认为“但是使用 List 我可以只传递一个索引”:这仅在您从不将新元素插入到 List 中间时才有效。 LinkedList 没有这个限制,if 你持有一个 node (并且只要你想要原始元素就使用node.Value)。所以你重写算法来处理节点,而不是原始值。
        【解决方案13】:

        我同意上述大部分观点。而且我也同意 List 在大多数情况下看起来是一个更明显的选择。

        但是,我只想补充一点,在很多情况下,LinkedList 比 List 要好得多,效率更高。

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

        希望有人会发现这些 cmets 有用。

        【讨论】:

        • 请注意,此建议适用于 .NET,而非 Java。在 Java 的链表实现中,您没有“当前节点”的概念,因此您必须遍历每个插入的列表。
        • 这个答案只是部分正确:2)如果元素很大,则将元素类型设置为 Class 而不是 Struct,以便 List 简单地保存一个引用。然后元素大小变得无关紧要。 3) 如果您使用 List 作为“循环缓冲区”,而不是在开始时插入或删除,则可以在 List 中有效地完成 Deque 和队列 StephenCleary's Deque。 4)部分正确:当许多个对象时,LL的pro不需要巨大的连续内存;缺点是节点指针的额外内存。
        【解决方案14】:

        在 .NET 中,列表表示为数组。因此,与 LinkedList 相比,使用普通 List 会更快。这就是上面的人看到他们看到的结果的原因。

        为什么要使用列表? 我会说这取决于。如果您没有指定任何元素,List 会创建 4 个元素。当你超过这个限制时,它会将内容复制到一个新数组中,将旧数组留在垃圾收集器的手中。然后它的大小加倍。在这种情况下,它会创建一个包含 8 个元素的新数组。想象一下有一个包含 100 万个元素的列表,然后再添加 1 个。它本质上将创建一个全新的数组,其大小是您需要的两倍。新阵列将具有 2Mil 容量,但是您只需要 1Mil 和 1。基本上将 GEN2 中的东西留给垃圾收集器等等。所以它实际上最终可能成为一个巨大的瓶颈。你应该注意这一点。

        【讨论】:

          【解决方案15】:

          我问了similar question related to performance of the LinkedList collection,发现Steven Cleary's C# implement of Deque 是一个解决方案。与 Queue 集合不同,Deque 允许前后移动项目。它类似于链表,但性能有所提高。

          【讨论】:

          • 重新声明Deque “类似于链表,但性能有所提高”。请限定该声明:Deque 的性能优于 LinkedList对于您的特定代码。按照您的链接,我看到两天后您从 Ivan Stoev 那里得知这不是 LinkedList 的低效率,而是您的代码效率低下。 (即使它是 LinkedList 的低效率,也不能证明 Deque 更有效的一般性陈述;仅在特定情况下。)
          猜你喜欢
          • 2010-10-18
          • 2015-12-05
          • 2023-04-02
          • 2011-04-15
          • 2017-04-10
          • 2012-03-19
          • 2018-05-12
          • 2018-12-11
          相关资源
          最近更新 更多