何时应使用 List 与 LinkedList

When should I use a List vs a LinkedList

提问人:Jonathan Allen 提问时间:10/4/2008 最后编辑:Colonel PanicJonathan Allen 更新时间:10/10/2023 访问量:297762

问:

什么时候使用 ListLinkedList 更好?

C# .NET vb.net 数据结构 链接列表

评论

3赞 nawfal 7/3/2014
Java q,应该不会有太大的不同。
2赞 Xpleria 8/9/2018
@jonathan-allen,请考虑更改已接受的答案。目前的版本不准确且极具误导性。
0赞 Leonardo 8/13/2021
正如 Xpleria 所说,请考虑更改当前接受的答案。电流具有误导性。

答:

2赞 Michael Damatov 10/4/2008 #1

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

331赞 Marc Gravell 10/4/2008 #2

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

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

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

评论

8赞 RenniePet 12/19/2014
List<> 与 LinkedList 相比<>我从未想过的一个优势是微处理器如何实现内存缓存。虽然我不完全理解,但这篇博文的作者谈了很多关于“引用位置”的问题,这使得遍历数组比遍历链表快得多,至少在链表在内存中变得有些碎片的情况下是这样。kjellkod.wordpress.com/2012/02/25/......
0赞 Casey 4/2/2015
@RenniePet List 是用动态数组实现的,数组是连续的内存块。
3赞 Cardin 7/27/2015
由于 List 是一个动态数组,这就是为什么有时最好在构造函数中指定 List 的容量(如果您事先知道的话)。
0赞 Philm 4/19/2017
对于一个非常重要的情况,all、array、List<T> 和 LinkedList<T> 的 C# 实现是否有可能有些次优: 你需要一个非常大的列表,追加(AddLast)和顺序遍历(在一个方向上)是完全可以的: 我不希望调整数组大小来获得连续块(是否保证每个数组, 甚至 20 GB 数组?),而且我事先不知道大小,但我可以提前猜到块大小,例如每次提前预留 100 MB。这将是一个很好的实现。还是数组/列表与此类似,而我错过了一点?
1赞 Marc Gravell 4/19/2017
@Philm在这种情况下,您在选择的区块策略上编写自己的填充码; 并且会因为太厚实(所有一块板)而失败,会因为太细(每个元素的板)而哀嚎。List<T>T[]LinkedList<T>
120赞 b3. 10/4/2008 #3

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

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

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

评论

6赞 paulecoyote 7/23/2009
我要补充一点,链表通过引用上一个和下一个节点的 LinkedListNode 存储的每个项目都有开销。这样做的回报是,与基于数组的列表不同,存储列表不需要连续的内存块。
3赞 Jonathan Allen 2/6/2010
通常不是连续的内存块吗?
7赞 jpierson 3/17/2010
是的,连续块是随机访问性能和内存消耗的首选,但对于需要定期更改大小的集合,通常需要将数组等结构复制到新位置,而链表只需要管理新插入/删除节点的内存。
6赞 Andrew 5/4/2011
如果您曾经不得不使用非常大的数组或列表(列表只是包装一个数组),即使您的计算机上似乎有足够的可用内存,您也会开始遇到内存问题。该列表在其基础数组中分配新空间时使用加倍策略。因此,一个 1000000 elemnt 已满的数组将被复制到一个包含 2000000 个元素的新数组中。这个新数组需要在足够大的连续内存空间中创建,以容纳它。
1赞 Peter 10/27/2011
我有一个特定的案例,我所做的只是添加和删除,然后一个接一个地循环......在这里,链表远远优于普通列表。
21赞 user23117 10/4/2008 #4

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

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

希望这有帮助,

评论

4赞 Marc Gravell 10/4/2008
List<T> 是基于数组 (T[]) 的,而不是基于 ArrayList。重新插入:数组大小调整不是问题(加倍算法意味着大多数时候它不必这样做):问题是它必须首先阻止复制所有现有数据,这需要一点时间。
2赞 Ilya Ryzhenkov 10/4/2008
@Marc,“倍增算法”只能使它成为 O(logN),但它仍然比 O(1) 差
2赞 Marc Gravell 10/4/2008
我的观点是,造成痛苦的不是调整大小,而是闪电。所以最坏的情况是,如果我们每次都添加第一个(第零个)元素,那么 blit 每次都必须移动所有内容。
1赞 ToolmakerSteve 11/21/2018
@IlyaRyzhenkov - 您正在考虑始终位于现有数组末尾的情况。 在这一点上“足够好”,即使不是 O(1)。如果您需要许多不在末尾的 s,则会出现严重的问题。Marc 指出,每次插入时(不仅仅是在需要调整大小时)都需要移动现有数据,这是 的更可观的性能成本。AddListAddList
0赞 MattE 2/8/2019
问题是理论上的大O符号并不能说明整个故事。在计算机科学中,这是任何人都关心的,但在现实世界中,需要关注的远不止于此。
255赞 Drew Noakes 10/15/2011 #5

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

链表可以是单链接的,也可以是双链接的。这是指链中的每个元素是仅链接到下一个元素(单独链接)还是同时链接到前一个/下一个元素(双重链接)。 是双重联系的。LinkedList<T>

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

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

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

评论

4赞 Iain Ballard 11/4/2011
count linkedlist 是常数吗?我以为那会是线性的?
13赞 Drew Noakes 11/5/2011
@Iain,计数缓存在两个列表类中。
3赞 aStranger 9/16/2012
你写了“List<T>。Add(item) logarithmic time“,但是,如果列表容量可以存储新项目,则实际上是”常量“,如果列表没有足够的空间和新空间需要重新分配,则为”线性”。
1赞 Philm 4/19/2017
我在一些结论中看到了一个矛盾:鉴于我只关心 Append 的速度,什么是最好的?我想用几百万行文本(或任何其他流)填充容器,但我不关心 RAM:我只需要关心速度 Append (.添加到列表的末尾)。这是最重要的(规范)情况,中间的插入是另一回事: ----- 使用 LinkedList<T> 或 List<T> 更好吗?
1赞 Drew Noakes 4/19/2017
@Philm,你可能应该开始一个新问题,你不会说一旦构建完你将如何使用这个数据结构,但如果你说的是一百万行,你可能喜欢某种混合(数组块链表或类似)来减少堆碎片,减少内存开销,并避免LOH上的单个大对象。
112赞 Tono Nam 9/18/2012 #6

编辑

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

原答案...

我发现了有趣的结果:

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

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

评论

110赞 JerKimball 9/20/2012
与 List 相比,LinkedList 有一个好处(这是特定于 .net的):由于 List 由内部数组支持,因此它被分配在一个连续的块中。如果分配的块大小超过 85000 字节,它将被分配到大型对象堆上,这是一个不可压缩的生成。根据大小的不同,这可能会导致堆碎片,这是一种轻微的内存泄漏形式。
37赞 cHao 9/21/2012
请注意,如果你在前面预置了很多(就像你在上一个示例中所做的那样)或删除第一个条目,那么链表几乎总是会快得多,因为没有搜索或移动/复制要做。列表需要将所有内容向上移动一个位置以容纳新项目,从而使前置 O(N) 操作。
6赞 ruffin 4/28/2013
为什么在最后两个 LinkedList 示例中出现 in-loop?我在循环之前做过一次,就像在倒数第二个 LinkedList 中一样,但看起来(对我来说)您在循环本身中添加了两倍的 Temp 对象。(当我使用测试应用程序仔细检查自己时,果然,LinkedList 中的数量是其两倍。list.AddLast(a);list.AddLast(new Temp(1,1,1,1));
7赞 nawfal 7/3/2014
我对这个答案投了反对票。1)正如您后来的帖子所揭示的那样,您的一般建议是有缺陷的。您可能需要对其进行编辑。2) 你的时机是什么?实例化、加法和枚举一步到位?大多数情况下,实例化和枚举不是 ppl 所担心的,这些是一次性步骤。具体来说,插入和添加的时间会给出一个更好的主意。3)最重要的是,你向链接列表添加了超过要求的内容。这是一个错误的比较。散布关于链接列表的错误想法。I say never use a linkedList.
52赞 mafu 11/19/2014
对不起,但这个答案真的很糟糕。请不要听这个答案。简而言之:认为数组支持的列表实现愚蠢到在每次插入时调整数组大小是完全有缺陷的。链表在遍历和插入两端时自然比数组支持的列表慢,因为只有它们需要创建新对象,而数组支持的列表使用缓冲区(显然是双向的)。(做得不好的)基准恰恰表明了这一点。答案完全无法检查链表更可取的情况!
0赞 Antony Thomas 11/24/2012 #7

使用场合LinkedList<>

  1. 你不知道有多少物体从闸门进来。例如。Token Stream
  2. 当您只想在末尾删除\插入时。

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

评论

10赞 Drew Noakes 12/26/2012
我不明白为什么第 2 点有意义。当您在整个列表中进行多次插入/删除时,链表非常有用。
0赞 Antony Thomas 12/27/2012
由于 LinkedLists 不是基于索引的,因此您确实必须扫描整个列表以查找插入或删除,这会导致 O(n) 损失。另一方面<>列表受到数组大小调整的影响,但与 LinkedLists 相比,IMO 仍然是一个更好的选择。
1赞 Drew Noakes 12/27/2012
如果跟踪代码中的对象,则不必扫描列表中的插入/删除。如果你能做到这一点,那么它比使用 要好得多,特别是对于插入/删除频繁的很长的列表。LinkedListNode<T>List<T>
0赞 Antony Thomas 12/27/2012
你是说通过哈希表?如果是这样的话,这将是典型的空间\时间权衡,每个计算机程序员都应该根据问题域:)做出选择但是,是的,这将使它更快。
2赞 ToolmakerSteve 11/21/2018
@AntonyThomas - 不,他的意思是传递对节点的引用,而不是传递对元素的引用。如果你只有一个元素,那么 List 和 LinkedList 的性能都很差,因为你必须搜索。如果你认为“但是使用List,我可以只传入一个索引”:这只有在你从不将新元素插入List的中间时才有效。LinkedList 没有此限制,如果您保留一个节点(并在您需要原始元素时使用)。因此,您可以重写算法以使用节点,而不是原始值。node.Value
15赞 Dr. Alrawi 11/25/2012 #8

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

评论

1赞 RBaarda 7/13/2016
IMO 这应该是答案。LinkedList 用于保证订单很重要的情况。
1赞 Philm 4/19/2017
@RBaarda:我不同意。这取决于我们谈论的水平。算法级别与机器实现级别不同。出于速度考虑,您也需要后者。正如所指出的,数组被实现为内存的“一个块”,这是一个限制,因为这可能导致调整大小和内存重组,特别是对于非常大的数组。经过一段时间的思考,一个特殊的自己的数据结构,数组链表将是一个想法,可以更好地控制线性填充和访问非常大的数据结构的速度。
1赞 ToolmakerSteve 11/21/2018
@Philm - 我赞成您的评论,但我想指出您正在描述不同的要求。答案是,链表对于涉及大量项目重新排列的算法具有性能优势。鉴于此,我将 RBaarda 的评论解释为需要添加/删除项目,同时持续保持给定的顺序(排序标准)。因此,不仅仅是“线性填充”。鉴于此,List 会失败,因为索引是无用的(每次在末尾以外的任何地方添加元素时都会更改)。
0赞 Farid 10/27/2022
@RBaarda 不,这不应该是答案,它只是复制粘贴了一个没有示例或他们自己的输入的参考。最好的办法是评论,甚至不是答案
1赞 nawfal 7/3/2014 #9

这是根据 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();
        }
    }
}

您可以看到结果与此处记录的其他人的理论性能一致。很清楚 - 在插入的情况下获得大量时间。我还没有测试是否从列表中间删除,但结果应该是相同的。当然,还有其他方面,它的表现要好得多,比如 O(1) 随机访问。LinkedList<T>List<T>

4赞 Tom 8/19/2014 #10

使用 LinkedList 的常见情况如下:

假设您要从大尺寸字符串列表中删除许多特定的字符串,例如 100,000。要删除的字符串可以在 HashSet dic 中查找,据信字符串列表包含 30,000 到 60,000 个要删除的字符串。

那么存储 100,000 个字符串的最佳列表类型是什么?答案是 LinkedList。如果它们存储在 ArrayList 中,则遍历它并删除匹配的字符串 whould 会占用 到数十亿次操作,而使用迭代器和 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赞 Servy 10/21/2014
您可以简单地用于从 中删除项,而无需移动大量项,或者从 LINQ 创建第二个列表。然而,使用 here 最终会比其他类型的集合消耗更多的内存,并且内存局部性的丢失意味着迭代速度会明显变慢,这使得它比 .RemoveAllListWhereLinkedListList
0赞 Arturo Torres Sánchez 11/21/2014
@Servy,请注意,@Tom的答案使用的是 Java。我不确定 Java 中是否有等效的。RemoveAll
3赞 Servy 11/24/2014
@ArturoTorresSánchez 好吧,这个问题特别指出它与 .NET 有关,所以这只会让答案变得不那么合适。
0赞 Arturo Torres Sánchez 11/24/2014
@Servy,那么你应该从一开始就提到这一点。
0赞 ToolmakerSteve 11/21/2018
如果 不可用,您可以执行“压缩”算法,该算法看起来像 Tom 的循环,但有两个索引,并且需要在列表的内部数组中一次移动一个要保留的项目。效率为 O(n),与 的 Tom 算法相同。在这两个版本中,计算字符串的 HashSet 键的时间占主导地位。这不是何时使用 .RemoveAllListLinkedListLinkedList
44赞 Andrew_STOP_RU_WAR_IN_UA 3/26/2015 #11

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


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

简短结果:

  • 阵列需要使用:

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

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

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

更多详情:

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

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

  2. LinkedList<T>是基于节点指针的集合。在 .NET 中,它采用双链接实现。这意味着 prior/next 元素链接到当前元素。而且数据是碎片化的——不同的列表对象可以位于 RAM 的不同位置。此外,用于的内存将多于 for 或 Array。LinkedList<T>List<T>

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

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

评论

1赞 Philm 4/19/2017
问题:对于“保存在数组 < 或 85.000 字节>的数据”,您的意思是每个数组/列表 ELEMENT 的数据,是吗?可以理解为你的意思是整个数组的数据大小。
0赞 Andrew_STOP_RU_WAR_IN_UA 1/15/2019
数组元素按顺序位于内存中。所以每个数组。我知道表中的错误,稍后我会:)修复它(我希望....
0赞 Rob 5/14/2019
由于列表在插入时速度很慢,如果列表有很多周转(大量插入/删除),则删除的空间是否占用了内存,如果是这样,这是否使“重新”插入更快?
0赞 olegz 11/9/2022
“如果数据保存在数组中 < 85000 b(85000/32 = 整数数据为 2656 个元素)” 你的意思是 85000 位还是字节?我假设 Integer 需要 4 个字节,而不是 32 个字节
-3赞 Adam Cox 8/13/2017 #12

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

评论

2赞 ToolmakerSteve 11/21/2018
回复您的声明,该语句“类似于链表,但性能有所提高”。对于您的特定代码,请限定该语句:的性能比 更好。通过您的链接,我看到两天后您从 Ivan Stoev 那里了解到,这不是 LinkedList 的低效,而是代码的低效。(即使 LinkedList 效率低下,也不能证明 Deque 更有效率的一般性声明是合理的;只有在特定情况下。DequeDequeLinkedList
2赞 John Smith 12/8/2017 #13

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

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

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

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

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

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

评论

1赞 Jonathan Allen 4/19/2018
请注意,此建议适用于 .NET,而不是 Java。在 Java 的链表实现中,你没有“当前节点”的概念,所以你必须遍历每个插入的列表。
0赞 ToolmakerSteve 11/21/2018
这个答案只是部分正确:2)如果元素很大,那么将元素类型设置为类而不是结构,这样List就只保存一个引用。然后元素大小变得无关紧要。3) 如果您使用 List 作为“循环缓冲区”,而不是在开始时执行插入或删除则可以在 List 中有效地完成 Deque 和 Queue。 斯蒂芬·克利里(StephenCleary)的德克。4)部分正确:当许多对象时,LL的优点是不需要巨大的连续内存;缺点是节点指针的额外内存。
-2赞 Ivailo Manolov 7/1/2021 #15

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

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

0赞 Raildex 10/10/2023 #16

除了其他答案之外,我发现在大型数据集和内存分配方面更好。LinkedList<T>

虽然需要连续的空间,但不需要。 当您拥有 GB 大小的数据时,这非常有用。 因为单个 Node 条目可以适合任何有足够空间的地方。 在这种情况下,使用常规或您需要 GB 的连续空间。List<T>LinkedList<T>List<T>T[]

想象一下,您有 2,000,000 个项目: 当您插入其中并且它需要重新分配(因为已满)时,您需要为 4,000,000 个项目提供连续空间(假设增长因子为 2.0 倍)。实际上甚至更多,因为新数组和旧数组需要同时存在才能重新分配。List<T>Capacity

有了 a,项目分散在内存中任何适合它们的地方,添加项目无非是为单个和节点的数据分配内存(+运行时需要的任何其他东西)LinkedList<T>T

评论

0赞 Cristian Diaconescu 11/28/2023
您还必须考虑数据结构的固有内存使用量。让我们举个例子,假设 64 位机器:具有 2^10 个元素的 A 将消耗 1M * sizeof(int) == 1M * 4 字节 ->单个 4 MB 的连续内存块(数组)加上最小开销:8 字节用于引用数组 + 4 字节用于内部。A 将分配 1M 个对象!每个节点都有 3 个引用(原始列表、上一个节点、下一个节点)加上有效负载:每个节点 3*8 + 4 = 28 字节。总计:28 * 1M = 28 MB!(与 List<> 的 4MB 相比)List<int>countLinkedList<int>LinkedListNode<int>int