我什么时候应该使用List vs LinkedList

时间:2008-10-04 08:23:25

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

何时使用ListLinkedList比较好?

15 个答案:

答案 0 :(得分:258)

在大多数情况下,List<T>更有用。在列表中间添加/删除项目时,LinkedList<T>的费用会降低,而List<T>只能在列表的 end 上便宜地添加/删除。

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

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

答案 1 :(得分:200)

将链表视为列表可能会有点误导。它更像是一个链条。事实上,在.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>提供线性时间,因为列表中的额外项目必须在插入/移除后随机播放。

答案 2 :(得分:114)

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

位置插入成员
  • 更新成员i-1中的指针以指向新成员
  • 将指针置于新成员中以指向成员i

链表的缺点是随机访问是不可能的。访问成员需要遍历列表,直到找到所需的成员。

答案 3 :(得分:97)

修改

  

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

原始答案......

我发现了有趣的结果:

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

即使您只访问数据本质上也要慢得多!! 我说从不使用链接列表。




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

链接列表(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;

因此,只有当您计划插入多个项目并且某处有您计划插入项目的位置时,请使用链接列表。仅仅因为你必须插入很多项目它不会使它更快,因为搜索你想要插入它的位置需要时间。

答案 4 :(得分:17)

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

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

希望得到这个帮助,

答案 5 :(得分:15)

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


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

结果短:

  • 需要使用数组:

    • 尽可能经常。它的速度很快,占用最小的RAM范围以获得相同数量的信息。
    • 如果您知道所需的确切细胞数
    • 如果数据保存在数组中&lt; 85000 b
    • 如果需要高随机访问速度
  • 列表需要使用:

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

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

更多详情:

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

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

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

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

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

答案 6 :(得分:11)

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

答案 7 :(得分:2)

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

答案 8 :(得分:2)

使用LinkedList的常见情况如下:

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

那么存储100,000个字符串的最佳List类型是什么?答案是LinkedList。如果它们存储在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();
}

答案 9 :(得分: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)随机访问一样表现得更好。

答案 10 :(得分:1)

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

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

答案 11 :(得分:0)

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

但是,我只想补充一点,有很多实例,其中LinkedList是比List更好的选择,以提高效率。

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

答案 12 :(得分:0)

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

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

答案 13 :(得分:-1)

时使用LinkedList<>
  1. 您不知道有多少物体通过防洪闸门。例如,Token Stream
  2. 当你只想在末尾删除\ insert时。
  3. 对于其他所有内容,最好使用List<>

答案 14 :(得分:-1)

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

某些链接列表实现使用预先分配的节点的底层块。如果他们不这样做,那么恒定时间/线性时间就不那么重要了,因为内存性能会很差,缓存性能会更差。

时使用链接列表

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

2)如果您有一个像结构一样的大型队列,并希望一直移除或添加任何地方,但最终。 &gt;存在100K列表但不常见。