ChatGPT解决这个技术问题 Extra ChatGPT

什么时候应该使用 List 和 LinkedList

什么时候使用 ListLinkedList 更好?

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

p
p.s.w.g

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

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

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


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

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

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

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

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

附加

LinkedList.AddLast(item) 常量时间

List.Add(item) 摊销常数时间,线性最坏情况

前置

LinkedList.AddFirst(item) 常量时间

List.Insert(0, item) 线性时间

插入

LinkedList.AddBefore(node, item) 常量时间

LinkedList.AddAfter(node, item) 常量时间

List.Insert(index, item) 线性时间

移动

LinkedList.Remove(item) 线性时间

LinkedList.Remove(node) 常量时间

List.Remove(item) 线性时间

List.RemoveAt(index) 线性时间

数数

LinkedList.Count 常量时间

List.Count 常数时间

包含

LinkedList.Contains(item) 线性时间

List.Contains(item) 线性时间

清除

LinkedList.Clear() 线性时间

List.Clear() 线性时间

如您所见,它们大多是等价的。在实践中,LinkedList<T> 的 API 使用起来更加麻烦,其内部需求的详细信息会溢出到您的代码中。

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


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

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

更新成员 i-1 中的指针以指向新成员

将新成员中的指针设置为指向成员 i

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


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

编辑

请阅读对此答案的评论。人们声称我没有进行适当的测试。我同意这不应该是一个公认的答案。在我学习的过程中,我做了一些测试,并想分享它们。

原答案...

我发现了有趣的结果:

// 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) 最重要的是,您向链表添加的内容超出了要求。这是一个错误的比较。传播关于链表的错误想法。
对不起,但是这个答案真的很糟糕。请不要听这个答案。简而言之:认为数组支持的列表实现愚蠢到在每次插入时都调整数组大小是完全错误的。在遍历以及在任一端插入时,链表自然比数组支持的列表慢,因为只有它们需要创建新对象,而数组支持的列表使用缓冲区(显然是双向的)。 (做得不好的)基准恰恰表明了这一点。答案完全无法检查链表更可取的情况!
A
Andrew____Pls_Support_Ukraine

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

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

简短的结果:

数组需要使用:尽可能频繁。它速度很快,并且对于相同数量的信息占用最小的 RAM 范围。如果您知道所需的单元格的确切数量如果保存在数组中的数据 < 85000 b(整数数据为 85000/32 = 2656 个元素)如果需要高随机访问速度

尽可能经常。它速度很快,并且对于相同数量的信息占用最小的 RAM 范围。

如果您知道所需细胞的确切数量

如果保存在数组中的数据 < 85000 b(整数数据为 85000/32 = 2656 个元素)

如果需要高随机存取速度

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

如果需要将单元格添加到列表末尾(经常)

如果需要在列表的开头/中间添加单元格(不经常)

如果保存在数组中的数据 < 85000 b(整数数据为 85000/32 = 2656 个元素)

如果需要高随机存取速度

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

如果需要在列表的开头/中间/结尾添加单元格(经常)

如果需要仅顺序访问(向前/向后)

如果您需要保存 LARGE 项目,但项目数量很少。

最好不要用于大量项目,因为它会为链接使用额外的内存。

更多细节:

https://i.stack.imgur.com/iBz6V.png

LinkedList 在内部不是 .NET 中的列表。它甚至没有实现 IList。这就是为什么没有索引和与索引相关的方法的原因。 LinkedList 是基于节点指针的集合。在 .NET 中,它是双向链接的实现。这意味着先前/下一个元素具有指向当前元素的链接。并且数据是碎片化的——不同的列表对象可以位于 RAM 的不同位置。与 List 或 Array 相比,LinkedList 使用的内存也更多。 .Net 中的 List 是 Java 的 ArrayList 的替代品。这意味着这是数组包装器。所以它作为一个连续的数据块在内存中分配。如果分配的数据大小超过 85000 字节,它将被移动到大对象堆。根据大小,这可能会导致堆碎片(一种温和的内存泄漏形式)。但同时如果 size < 85000 字节——这在内存中提供了一个非常紧凑和快速访问的表示。单个连续块对于随机访问性能和内存消耗是首选,但对于需要定期更改大小的集合,通常需要将诸如 Array 之类的结构复制到新位置,而链表只需要管理新插入的内存/删除的节点。


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

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)。如果您需要许多结尾处 notAdd,就会出现严重的问题。 Marc 指出,每次 插入时需要移动 现有数据(不仅仅是在需要调整大小时)是 List 的更大性能成本。
问题是理论上的大 O 符号并不能说明整个故事。在计算机科学中,这是所有人都关心的问题,但在现实世界中要关心的远不止这些。
D
Dr. Alrawi

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


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

使用 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 最终会比其他类型的集合消耗显着更多的内存,并且内存局部性的丢失意味着它的迭代速度会明显变慢,这使得它比 { 2}。
@Servy,请注意@Tom 的答案使用Java。我不确定 Java 中是否有 RemoveAll 等价物。
@ArturoTorresSánchez 好吧,这个问题特别指出它是关于.NET 的,所以这只会让答案变得不那么合适。
@Servy,那么您应该从一开始就提到这一点。
如果 RemoveAll 不适用于 List,您可以执行“压缩”算法,该算法看起来像 Tom 的循环,但有两个索引,并且需要在列表的内部移动项目以一次保留一个大批。效率为 O(n),与 LinkedList 的 Tom 算法相同。在这两个版本中,为字符串计算 HashSet 键的时间占主导地位。这不是什么时候使用 LinkedList 的好例子。
M
Michael Damatov

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


i
iliketocode

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

链表具有更好的插入/删除性能,只要插入/删除不在集合中的最后一个元素上。这是因为数组必须移动插入/删除点之后的所有剩余元素。但是,如果插入/删除位于列表的末尾,则不需要这种移位(尽管如果超出了数组的容量,可能需要调整数组的大小)。

数组具有更好的访问能力。数组可以直接索引(在恒定时间内)。必须遍历链表(线性时间)。


C
Community

这改编自 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) 随机访问。


P
Peter Mortensen

使用 LinkedList<>

你不知道有多少物体正在通过防洪闸门。例如,令牌流。当您只想在末尾删除\插入时。

对于其他一切,最好使用 List<>


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

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

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

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

希望有人会发现这些评论有用。


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

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

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


A
Adam Cox

我问了一个 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 更有效的一般性陈述;仅在特定情况下。)