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

什么时候使用列表LinkedList更好?

281054 次浏览

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

在大多数情况下,List<T>更有用。在列表的中间添加/删除项时,LinkedList<T>的开销较小,而List<T>只能在列表的结束处添加/删除项。

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

List<T>也提供了很多支持方法——FindToArray,等等;然而,这些也可以通过扩展方法用于。net 3.5/ c# 3.0的LinkedList<T> -所以这不是一个因素。

链表提供了快速插入或删除列表成员的功能。链表中的每个成员都包含指向链表中下一个成员的指针,因此要在位置i插入一个成员:

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

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

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

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

希望这能有所帮助,

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

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

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

它们也有不同的表现特征:

附加

  • LinkedList<T>.AddLast(item) < em >常数时间< / em >
  • List<T>.Add(item) 平摊常数时间,线性最坏情况

预谋

  • LinkedList<T>.AddFirst(item) < em >常数时间< / em >
  • List<T>.Insert(0, item) 线性时间

插入

  • LinkedList<T>.AddBefore(node, item) < em >常数时间< / em >
  • LinkedList<T>.AddAfter(node, item) < em >常数时间< / em >
  • List<T>.Insert(index, item) 线性时间

删除

  • LinkedList<T>.Remove(item) 线性时间
  • LinkedList<T>.Remove(node) < em >常数时间< / em >
  • 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>提供线性时间,因为列表中的额外项必须在插入/移除之后重新排列。

编辑

请阅读这个答案的评论。人们说我没有 适当的测试。我同意这不应该是一个可以接受的答案。就像我一样 我做了一些测试,想要分享。

最初的回答…

我发现了有趣的结果:

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

因此,只有当你计划插入几个项目,并且你在某处有你计划插入项目的引用时,才使用链表。只是因为你必须插入很多项,这并不会使它更快,因为搜索你想要插入的位置需要时间。

使用LinkedList<>

  1. 你不知道有多少东西会通过防洪闸门。例如,Token Stream
  2. 当你只想在结尾删除\插入。

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

链表相对于数组的主要优点是,链接为我们提供了有效地重新排列项的能力。 Sedgewick, p. 91

这是改编自Tono南的接受的答案纠正了一些错误的测量。

测试:

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)随机访问。

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

假设您想要从一个字符串列表中删除许多特定的字符串,这些字符串的大小很大,比如100,000。要删除的字符串可以在HashSet dic中查找,字符串列表中应该包含30,000到60,000个这样的需要删除的字符串。

那么用于存储100,000个字符串的列表的最佳类型是什么?答案是LinkedList。如果它们存储在数组列表中,则遍历它并删除匹配的字符串将占用 到数十亿次操作,而使用迭代器和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();
}
我之前的回答不够准确。 D是正确答案 但是现在我可以发布更有用和正确的答案了

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

短的结果:

  • 数组需要使用:

    • 越频繁越好。它速度快,对相同数量的信息占用最小的RAM范围。
    • 如果你知道所需细胞的确切数量
    • 如果数据保存在数组<85000 b(对于整数数据,85000/32 = 2656个元素)
    • 如果需要高随机访问速度
    • 李< / ul > < / >
    • 列表需要使用:

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

        • 如果需要在列表的开始/中间/结尾添加单元格(通常)
        • 如果只需要顺序访问(向前/向后)
        • 如果你需要保存大型物品,但物品数量低。
        • 最好不要用于大量的项目,因为它会使用额外的内存链接。
        • 李< / ul > < / >

        更多的细节:

        < a href = " https://i.stack.imgur.com/iBz6V.png " rel = " noreferrer " > < img src = " https://i.stack.imgur.com/iBz6V.png " alt = "введитесюдаописаниеизображения”> 有趣的是:

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

        2. LinkedList<T>是基于节点指针的集合。在。net中是双向链接实现。这意味着前一个/下一个元素与当前元素有链接。而且数据是碎片化的——不同的列表对象可以位于RAM的不同位置。而且,LinkedList<T>将比List<T>或Array使用更多的内存。

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

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

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

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

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

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

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

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

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

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

为什么要使用List? 我会说这要看情况。如果没有指定的元素,List将创建4个元素。当您超过这个限制时,它将内容复制到一个新数组,将旧数组留给垃圾回收器。然后它的大小翻倍。在本例中,它创建了一个包含8个元素的新数组。想象一下,有一个包含100万个元素的列表,你再添加1个元素。它会创建一个全新的数组,大小是你需要的两倍。新的阵列将具有2Mil容量,然而,您只需要1Mil和1。本质上是把东西留在GEN2中给垃圾回收器等等。所以它最终会成为一个巨大的瓶颈。