什么时候使用List而不是LinkedList更好?
什么时候使用List而不是LinkedList更好?
Answers:
请阅读对此答案的评论。人们声称我没有做适当的测试。我同意这不是一个可以接受的答案。在学习的过程中,我做了一些测试,觉得很喜欢分享。
我发现了有趣的结果:
// 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。
这是另一个执行大量插入操作的比较(我们计划在列表的中间插入一个项目)
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));
在最后一个LinkedList旁边一样,但是(在我看来)您在循环本身中添加了两倍的Temp对象。(当我用一个测试应用程序仔细检查自己时,肯定是LinkedList中的两倍。)
I say never use a linkedList.
是有缺陷的,因为您以后的帖子显示。您可能要编辑它。2)您打算什么时间?实例化,加法和枚举总共需要一步?通常,实例化和枚举并不是ppl所担心的,那只是一步之遥。专门安排插入和添加的时间会更好。3)最重要的是,您向链表添加的内容超出了要求。这是一个错误的比较。传播关于链表的错误想法。
在大多数情况下,List<T>
它更为有用。LinkedList<T>
在列表中间添加/删除项目时,其成本会降低,而List<T>
只能在列表末尾廉价添加/删除。
LinkedList<T>
仅在访问顺序数据(正向或反向)时才最有效-随机访问相对昂贵,因为随机访问每次都必须遍历整个链(因此为什么它没有索引器)。但是,因为a List<T>
本质上只是一个数组(带有包装器),所以随机访问是可以的。
List<T>
还提供了很多的支持方法- Find
,ToArray
等; 但是,这些也可以LinkedList<T>
通过扩展方法与.NET 3.5 / C#3.0一起使用-因此影响较小。
List<T>
而T[]
失败(全部为一个平板),LinkedList<T>
会因为太细而失败(每个元素都为平板)。
将链接列表视为列表可能会产生误导。它更像一条链。实际上,在.NET中,LinkedList<T>
甚至没有实现IList<T>
。链表中没有索引的真正概念,即使看起来确实存在。当然,该类上提供的所有方法都不接受索引。
链接列表可以是单链接,也可以是双链接。这指的是链中的每个元素都仅具有指向下一个元素的链接(单链接),还是与先前/后一个元素的链接(双链接)。 LinkedList<T>
是双重链接。
在内部,List<T>
由数组支持。这在内存中提供了非常紧凑的表示形式。相反,LinkedList<T>
涉及额外的内存来存储连续元素之间的双向链接。因此,a的内存占用空间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>
使用起来比较麻烦,并且其内部需求的详细信息会溢出到您的代码中。
但是,如果您需要从列表中进行多次插入/删除操作,它将提供恒定的时间。 List<T>
提供线性时间,因为列表中的其他项目在插入/移除后必须重新排列。
链接列表可以非常快速地插入或删除列表成员。链接列表中的每个成员都包含一个指向列表中下一个成员的指针,以便在位置i处插入一个成员:
链表的缺点是无法进行随机访问。访问成员需要遍历列表,直到找到所需的成员。
我以前的答案不够准确。确实如此,这很可怕:D但现在我可以发布更多有用且正确的答案。
我做了一些额外的测试。您可以通过以下链接找到其来源,然后通过自己的环境在其上重新检查它:https : //github.com/ukushu/DataStructuresTestsAndOther.git
结果简短:
数组需要使用:
清单需要使用:
LinkedList需要使用:
更多细节:
LinkedList<T>
内部不是.NET中的列表。它甚至没有实现IList<T>
。这就是为什么缺少索引和与索引相关的方法的原因。
LinkedList<T>
是基于节点指针的集合。在.NET中,它处于双链接实现中。这意味着先前/下一个元素具有到当前元素的链接。而且数据是零散的-不同的列表对象可以位于RAM的不同位置。此外,用于的内存LinkedList<T>
将比用于List<T>
或的更多。
List<T>
.Net中的Java是Java的替代ArrayList<T>
。这意味着这是数组包装器。因此,它作为一个连续的数据块在内存中分配。如果分配的数据大小超过85000字节,它将被移至大对象堆。根据大小,这可能导致堆碎片(一种轻微的内存泄漏形式)。但是同时如果大小<85000字节-这将在内存中提供非常紧凑和快速访问的表示形式。
单个连续块是随机访问性能和内存消耗的首选,但对于需要定期更改大小的集合,通常需要将诸如Array之类的结构复制到新位置,而链表仅需要管理新插入的内存/删除的节点。
List和LinkedList之间的区别在于它们的基础实现。List是基于数组的集合(ArrayList)。LinkedList是基于节点指针的集合(LinkedListNode)。在API级别的用法上,它们两者几乎相同,因为它们都实现了同一组接口,例如ICollection,IEnumerable等。
关键区别在于性能。例如,如果要实现具有大量“ INSERT”操作的列表,则LinkedList的性能将优于List。由于LinkedList可以在O(1)时间内完成,因此List可能需要扩展基础数组的大小。有关更多信息/详细信息,您可能需要阅读LinkedList和数组数据结构之间的算法差异。http://zh.wikipedia.org/wiki/链接列表和数组
希望有帮助,
Add
总是在现有数组末尾的情况。List
即使不是O(1),也“足够好”。如果你需要很多会出现严重的问题,Add
S中的不是底。Marc指出,每次插入时都需要移动现有数据(而不仅是在需要调整大小时),这是一个更大的性能成本。List
链接列表相对于数组的主要优点是,链接使我们能够高效地重新排列项目。塞奇威克 91
使用LinkedList的常见情况是这样的:
假设您要从一个较大的字符串列表中删除许多特定的字符串,例如100,000。可以在HashSet dic中查找要删除的字符串,并且认为字符串列表包含30,000至60,000之间的此类字符串。
那么,存储100,000个字符串的最佳列表类型是什么?答案是LinkedList。如果它们存储在ArrayList中,则对其进行迭代并删除匹配的String,这些String最多需要执行数十亿次操作,而使用迭代器和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
而不用移动很多项目,也可以使用Where
LINQ从中创建第二个列表。使用LinkedList
此然而最终消费显着高于其他类型的集合和内存局部性手段损失更多的内存,这将是明显慢于迭代,使它颇有几分比一个更坏List
。
RemoveAll
Java 是否有等效功能。
RemoveAll
不适用于List
,则可以执行“压缩”算法,该算法看起来像汤姆的循环,但是有两个索引,并且需要将项目一次移动到列表的内部数组中。效率为O(n),与Tom的的算法相同LinkedList
。在这两个版本中,为字符串计算HashSet键的时间均占主导。这不是何时使用的好例子LinkedList
。
当您需要内置索引访问,排序(以及此二进制搜索之后)和“ ToArray()”方法时,应使用List。
改编自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)随机访问。
使用LinkedList<>
时
Token Stream
。对于其他所有内容,最好使用List<>
。
LinkedListNode<T>
代码中的对象,则不必扫描列表中的插入/删除操作。如果可以这样做,那将比使用更好List<T>
,特别是对于很频繁插入/删除的很长的列表。
node.Value
在需要原始元素的任何时候使用),LinkedList没有此限制。因此,您重写算法以使用节点,而不是原始值。
我确实同意以上大部分观点。我也同意,在大多数情况下,List似乎是一个更明显的选择。
但是,我只想补充一下,在很多情况下,LinkedList比List更好的选择是为了提高效率。
希望有人会觉得这些评论有用。
这里有很多平均答案...
一些链表实现使用预分配节点的基础块。如果他们不这样做,那么固定时间/线性时间就没有那么重要了,因为内存性能会很差,缓存性能会更差。
在以下情况下使用链接列表
1)您想要线程安全。您可以构建更好的线程安全算法。锁定成本将主导并发样式列表。
2)如果您的队列很长,例如结构,并且想要一直删除或添加除末尾之外的所有内容。存在超过10万个列表,但并不常见。
我问了一个与LinkedList集合的性能有关的类似问题,发现Steven Cleary的Deque的C#实现是一种解决方案。与队列集合不同,Deque允许前后移动项目。它类似于链表,但性能有所提高。
Deque
是“类似于链表,但是具有更高的性能”。请有资格的发言:Deque
是不是更好的性能LinkedList
,为您的特定代码。在链接之后,我看到两天后,您从Ivan Stoev那里了解到这不是LinkedList的低效率,而是代码中的低效率。(即使是LinkedList的效率低下,也不能证明Deque更有效;这只是在特定情况下的一般说法。)