何时更适合使用 List 而不是 LinkedList?
何时更适合使用 List 而不是 LinkedList?
List<T>
更有用。当添加/删除列表中间的项目时,LinkedList<T>
的成本较低,而List<T>
只能在列表末尾轻松添加/删除。
LinkedList<T>
仅在访问顺序数据(向前或向后)时达到最高效率——随机访问相对昂贵,因为每次都必须遍历链表(因此它没有索引器)。然而,由于List<T>
本质上只是一个数组(带有包装器),所以随机访问很好。
List<T>
还提供了许多支持方法,例如Find
、ToArray
等。然而,通过.NET 3.5/C# 3.0的扩展方法,这些方法也可用于LinkedList<T>
,因此这不是一个重要因素。List<T>
和 T[]
太过笨重(全部在一个块中),而 LinkedList<T>
则因为每个元素都有一个块而过于细粒度。 - Marc Gravell将链表看作一个列表可能会有点误导。它更像是一条链。事实上,在 .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>
则需要线性时间,因为插入/删除后必须重新排列列表中的额外项。
链表提供了非常快速的插入或删除列表成员的能力。链表中的每个成员都包含指向列表中下一个成员的指针,因此要在位置 i 插入成员:
链表的缺点是无法进行随机访问。访问成员需要遍历列表,直到找到所需成员为止。
请阅读此回答下方的评论。有人声称我没有进行适当的测试。我同意这不应该是一个被接受的答案。在学习过程中,我进行了一些测试并决定分享它们。
我发现了一些有趣的结果:
// 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;
}
}
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;
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<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;
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;
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;
所以只有在您计划插入多个项目并且同时有要插入项目的位置参考时,才使用链表。仅仅因为您需要插入许多项目,并不意味着它会更快,因为查找要插入的位置需要时间。
list.AddLast(a);
?我知道在循环之前添加一次,就像在倒数第二个 LinkedList 中使用 list.AddLast(new Temp(1,1,1,1));
一样,但是在循环本身中似乎会添加两倍于所需的 Temp 对象。(当我用一个测试应用程序进行自我检查时,LinkedList 中确实有两倍数量的对象。) - ruffin我的上一个回答不够准确。
事实上,它是可怕的 :D
但现在我可以发布更有用和正确的答案了。
我进行了一些额外的测试。您可以通过以下链接找到其源代码,并通过自己的环境重新检查它: https://github.com/ukushu/DataStructuresTestsAndOther.git
短期成果:
更多细节:
LinkedList<T>
在.NET中内部不是一个列表。它甚至没有实现IList<T>
。这就是为什么缺少索引和与索引相关的方法。
LinkedList<T>
是基于节点指针的集合,在.NET中它是双向链表实现。这意味着前/后元素有链接到当前元素。数据是分散的--不同的列表对象可以位于RAM的不同位置。此外,LinkedList<T>
使用的内存比List<T>
或数组更多。
List<T>
在.NET中是Java的替代ArrayList<T>
。这意味着这是一个数组包装器。因此,它作为一块连续的数据块在内存中分配。如果分配的数据大小超过85000字节,则会将其移动到大型对象堆。根据大小,这可能导致堆片段(一种轻微的内存泄漏)。但同时,如果大小小于85000字节,则提供了非常紧凑和快速访问内存中的表示。
单个连续块对于随机访问性能和内存消耗的性能更佳,但是对于需要定期更改大小的集合,诸如数组之类的结构通常需要复制到新位置,而链表仅需要管理新插入/删除节点的内存。
List和LinkedList的区别在于它们的底层实现方式。List是基于数组的集合(ArrayList),而LinkedList是基于节点指针的集合(LinkedListNode)。在API级别的使用上,它们都实现了相同的接口,例如ICollection、IEnumerable等。
关键的区别在于性能问题。例如,如果您正在实现具有大量“插入”操作的列表,LinkedList比List更为优越。因为LinkedList可以在O(1)时间内完成,但List可能需要扩展底层数组的大小。如需了解更多信息/详细信息,您可能需要阅读LinkedList和数组数据结构之间算法差异的相关资料。可参考链接:http://en.wikipedia.org/wiki/Linked_list和Array
希望这可以帮助您,
Add
总是在现有数组的末尾的情况。即使不是O(1),List
也足够好。严重的问题出现在需要许多不在末尾的Add
的情况下。Marc指出,每次插入时(不仅在需要调整大小时)需要移动现有数据是List
更实质性的性能成本。 - ToolmakerSteve链表相较于数组的主要优点在于它们提供了链接来有效地重新排列项目的能力。
使用LinkedList的一个常见情况如下:
假设您想从一个包含大量字符串(比如10万个)的字符串列表中删除许多特定的字符串。要删除的字符串可以在HashSet字典中查找,而字符串列表被认为包含30,000到60,000个这样的字符串。
那么用什么类型的List来存储这100,000个字符串呢?答案是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();
}
RemoveAll
简单地从 List
中移除项目而无需大量移动项目,或者使用 LINQ 中的 Where
创建第二个列表。然而,在这里使用 LinkedList
将会消耗显著更多的内存比其他类型的集合,并且由于失去了内存局部性,迭代速度明显变慢,使它比 List
差得多。 - ServyRemoveAll
的相应方法。 - Arturo Torres SánchezList
没有提供RemoveAll
方法,你可以使用“压缩”算法,它看起来像Tom的循环,但有两个索引,并且需要将要保留的项逐个向下移动到列表的内部数组中。效率为O(n),与LinkedList
的Tom算法相同。在两个版本中,计算字符串的HashSet键的时间占主导地位。这不是使用LinkedList
的好例子。 - ToolmakerSteve当您需要内置索引访问、排序(以及在此之后的二分搜索)和“ToArray()”方法时,应使用List。
List<>
是对数组的封装。而LinkedList<>
则是一个链表。所以问题就在于,数组和链表之间有什么区别,何时应该使用数组而不是链表。你决定使用哪个的最重要的两个因素可能是: