Under what circumstances are linked lists useful?
大多数时候我看到人们试图使用链表,在我看来这是一个糟糕(或非常糟糕)的选择。也许探究链接列表在什么情况下是或不是数据结构的一个好选择是有用的。
理想情况下,答案将阐述选择数据结构时使用的标准,以及在特定情况下哪些数据结构可能最有效。
编辑:我必须说,我对答案的数量和质量印象深刻。我只能接受一个,但如果没有更好的东西,我不得不说还有两三个值得接受。只有一对夫妇(尤其是我最终接受的那个)指出链接列表提供了真正的优势。我认为史蒂夫·杰索普不仅提出了一个答案,而且提出了三个不同的答案,这是值得尊敬的,我觉得所有这些都非常令人印象深刻。当然,尽管它只是作为一个评论而不是一个答案发布,我认为尼尔的博客条目也很值得阅读——不仅信息丰富,而且非常有趣。
当您需要在任意(编译时未知)长度的列表上执行大量的插入和删除操作,但不需要进行太多的搜索时,链接列表非常有用。
拆分和连接(双向链接)列表非常有效。
还可以组合链接列表-例如,树结构可以实现为"垂直"链接列表(父/子关系),将水平链接列表(同级)连接在一起。
出于这些目的使用基于数组的列表有严重的限制:
- 添加新项意味着必须重新分配数组(或者必须分配比将来增长所需空间更多的空间,并减少重新分配的数量)。
- 删除项目会留下浪费的空间或需要重新分配
- 在除结尾以外的任何位置插入项目都涉及(可能重新分配和)将大量数据复制到一个位置上
它们对于并发数据结构很有用。(现在有一个非并发的实际使用示例,如果@neil没有提到fortran,就不会有这个示例了。;-)
例如,.net 4.0rc中的
EDCOX1×1是一种数据结构,它作为新线程池的基础,(本地的队列)基本上是栈。(另一个主要支撑结构是
新的线程池反过来为新的线程池的工作调度提供了基础。任务并行库。
因此它们肯定是有用的——链表目前是至少一项伟大的新技术的主要支持结构之一。
(在这些情况下,单独的链表可以进行强制的无锁(而不是无等待)选择,因为主操作可以通过单个CA执行(+重试)。在现代的GC-D环境中(如Java和.NET),ABA问题可以很容易避免。只需包装添加到新创建的节点中的项目,不要重用这些节点—让GC完成它的工作。ABA问题上的页面还提供了一个无锁堆栈的实现,它实际上在.NET(和Java)中工作(带有一个(GC-ED)节点保存项目。
编辑:@尼尔:实际上,您提到的fortran提醒我,在.NET中最常用和被滥用的数据结构中也可以找到相同类型的链接列表:纯.NET通用
不是一个,而是多个链表存储在一个数组中。
- 它避免在插入/删除上执行许多小(de)分配。
- 哈希表的初始加载速度非常快,因为数组是按顺序填充的(在CPU缓存中表现非常好)。
- 更不用说链接哈希表在内存方面是昂贵的——而且这种"技巧"将X64上的"指针大小"减少了一半。
实际上,许多链表存储在一个数组中。(每使用一个桶一个。)可重用节点的自由列表在它们之间"交织"(如果有删除)。数组在开始/重新创建时分配,其中保留链节点。还有一个自由指针——数组中的索引——跟随删除。;-)不管你信不信由你,Fortran技术仍然存在。(…除了最常用的.NET数据结构之一;-)。
链接列表是非常灵活的:通过修改一个指针,您可以进行大规模的更改,在数组列表中相同的操作将非常低效。
数组是通常比较链接列表的数据结构。
通常,当您必须对列表本身进行大量修改,而数组在直接元素访问上的性能优于列表时,链接列表很有用。
下面是可以在列表和数组上执行的操作列表,与相对操作成本(n=列表/数组长度)相比:
- 添加元素:
- 在列表中,您只需要为新元素分配内存并重定向指针。O(1)
- 在数组上,必须重新定位数组。o(n)
- 删除元素
- 在列表上,您只需重定向指针。O(1)。
- 在数组上,如果要删除的元素不是数组的第一个或最后一个元素,则需要花费O(n)个时间来重新定位数组;否则,您只需将指针重新定位到数组的开头或缩短数组长度。
- 获取已知位置的元素:
- 在列表中,您必须将列表从第一个元素移动到位于特定位置的元素。最坏情况:O(N)
- 在数组上,您可以立即访问元素。O(1)
这是对这两种流行的和基本的数据结构的一个非常低级的比较,您可以看到,在需要对列表进行大量修改(删除或添加元素)的情况下,列表的性能会更好。另一方面,当必须直接访问数组的元素时,数组的性能比列表好。
从内存分配的角度来看,列表更好,因为不需要将所有元素都放在彼此旁边。另一方面,存储指向下一个(甚至是上一个)元素的指针的开销很小。
了解这些差异对于开发人员在实现中在列表和数组之间进行选择非常重要。
注意,这是列表和数组的比较。这里报告的问题有很好的解决方案(例如:滑雪板、动态阵列等)。在这个答案中,我考虑了每个程序员应该知道的基本数据结构。
双链表是定义哈希图排序的一个很好的选择,它也定义了元素上的顺序(Java中的LinkedHashMap),特别是在上次访问命令的情况下:
当然,与更复杂和可调的缓存相比,您可以首先讨论LRU缓存是否是一个好主意,但是如果您打算使用一个,这是一个相当不错的实现。您不希望在每次读取访问时从中间执行删除操作,并在向量或deque上添加到末尾,但将节点移动到尾部通常很好。
对于单元分配器或对象池中的空闲列表,单链表是一个不错的选择:
当您无法控制数据的存储位置时,链表是一种自然的选择,但是您仍然需要以某种方式从一个对象到下一个对象。
例如,在C++中实现内存跟踪(新的/删除替换)时,您要么需要一些控制数据结构来跟踪哪些指针已经被释放,您完全需要实现自己。另一种方法是过度分配并在每个数据块的开头添加一个链接列表。
因为您总是即时知道,当调用delete时,您在列表中的位置,所以您可以很容易地放弃o(1)中的内存。在O(1)中还添加了一个刚刚被恶意锁定的新块。在这种情况下,很少需要遍历列表,因此这里的O(N)成本不是问题(无论如何,遍历结构是O(N)成本)。
单链表是函数式编程语言中常见"list"数据类型的明显实现:
相比之下,向量或deque通常在任意一端添加都比较慢,要求(至少在我的两个不同的附录示例中)从整个列表(向量)或索引块和附加到(deque)的数据块中获取一个副本。实际上,在大的列表中可能会有一些关于deque的内容需要在尾部添加,因为某些原因,我对函数编程没有足够的了解来判断。
当您需要高速推、弹出和旋转,并且不介意O(N)索引时,它们非常有用。
链表良好使用的一个例子是链表元素非常大,即足够大,只有一个或两个元素可以同时放在CPU缓存中。此时,连续块容器(如用于迭代的向量或数组)的优点或多或少是无效的,并且如果实时发生许多插入和删除,则可能具有性能优势。
根据我的经验,实现稀疏矩阵和斐波那契堆。链接列表使您能够更好地控制此类数据结构的总体结构。虽然我不确定稀疏矩阵是否最好用链表实现-也许有更好的方法,但它确实有助于学习在本科生中使用链表的稀疏矩阵的输入和输出:)
最有用的一个用例的列表中查找相关的工作,在关键领域的绩效目样和图像处理,物理引擎,和光线追踪的是当使用联列表实际上提高了locality参考和对堆分配和使用,有时甚至对存储器的两个相对的straightforward替换。
好。
现在,我似乎像一个完整的矛盾,联列表可能不全是因为他们的臭名昭著的对面或做对,但他们有一个独特的性质,在那狡猾的每个节点有一个固定的大小和对齐的要求,我们可以exploit允许他们两个两个仓库contiguously和删除在常数时间的方式。可变大小的没有的事。
好。
作为一个结果,一个简单的案例,我们想做的analogical当量(储存一个可变长度序列,包含一万套式可变长度的子序列。岩石一混凝土指标以储存一百万的多边形网格(有一些三角形,四边形,有一些五边形、六边形等)和有时是删除从任何地方的多边形的多边形网格和有时是一个rebuilt插入现有的两个顶点的多边形或remove一。在这样的情况下,如果我们的小
好。
"这是一个问题,我会instances万
好。
如果,而不是我们这样做:
好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct FaceVertex { // Points to next vertex in polygon or -1 // if we're at the end of the polygon. int next; ... }; struct Polygon { // Points to first vertex in polygon. int first_vertex; ... }; struct Mesh { // Stores all the face vertices for all polygons. std::vector<FaceVertex> fvs; // Stores all the polygons. std::vector<Polygon> polys; }; |
…………………然后,我们dramatically学院二号堆分配和高速缓存失误。而不是要求一堆分配和潜在的高速缓存失误义务为我们的每一个单一的多边形的访问,我们现在只需要那一堆分配时的二维矢量可存储在全网超过他们的能力(amortized成本)。当这两个跨越鸭从一个顶点,两下一股可能会因为其静止的高速缓存失误,它的静止或更少,如果每一个单一的多边形可存储阵列从一个动态的节点是contiguously仓库和有一neighboring顶点A的概率,可能是accessed之前eviction(特别是多considering极ygons会增加他们的vertices全一次这使得狮子的份额vertices perfectly本体(多边形)。
好。
这里的另一个实例:冰
好。
好。
…网格单元用于加速粒子碰撞,例如,每帧移动1600万个粒子。在那个粒子网格示例中,使用链接列表,我们可以通过更改3个索引将粒子从一个网格单元移动到另一个网格单元。从一个向量中删除并推回另一个向量可能会花费更大的成本,并引入更多的堆分配。链表还将一个单元的内存减少到32位。根据实现的不同,向量可以将其动态数组预分配到一个空向量可以占用32个字节的位置。如果我们有大约一百万个网格单元,那就大不一样了。好的。
…这就是我发现链表最近最有用的地方,我特别发现"索引链表"的变化很有用,因为32位索引将64位机器上链接的内存需求减半,这意味着节点存储在一个数组中是连续的。好的。
通常,我也会将它们与索引自由列表结合起来,以允许在任何地方进行持续时间的删除和插入:好的。
好的。
在这种情况下,如果节点已被删除,则
这是我最近为链表找到的第一个用例。当我们要存储,比如说,一百万个可变长度的子序列,平均每个元素4个(但有时元素被删除并添加到这些子序列中的一个),链表允许我们存储400万个链表节点,而不是100万个容器,每个容器被单独的堆分配:一个巨大的ctor,也就是说,不是一百万个小的。好的。好啊。
考虑到链表在包含与重复互锁的部分的系统的域驱动设计风格实现中可能非常有用。
想到的一个例子可能是,如果你要做一个悬挂链的模型。如果您想知道任何特定链接上的张力是什么,那么您的接口可以包含一个"明显"重量的getter。其中的实现将包括一个链接,询问其下一个链接的表观权重,然后将其自身权重添加到结果中。通过这种方式,可以使用链客户机的单个调用来计算从下到下的整个长度。
作为一个像自然语言一样阅读的代码的支持者,我喜欢这样做能让程序员问一个链环有多重。它还关注在链接实现的边界内计算这些子属性,从而消除了对链权重计算服务的需求"。
有两个互补的操作,它们在列表中是微不足道的O(1),在其他数据结构中很难在O(1)中实现——从任意位置删除和插入元素,假设您需要保持元素的顺序。
哈希映射显然可以在O(1)中执行插入和删除操作,但是您不能按顺序迭代元素。
鉴于上述事实,可以将哈希映射与链接列表结合起来创建一个漂亮的LRU缓存:一个存储固定数量的键值对并删除最近访问次数最少的键以为新键腾出空间的映射。
哈希映射中的条目需要有指向链接列表节点的指针。访问散列图时,链接列表节点将从其当前位置断开链接,并移动到列表的头部(o(1),对于链接列表为yay!)。当需要删除最近使用过的元素时,需要将列表尾部的元素(假设保留指向尾部节点的指针,则为o(1))与关联的哈希映射项一起删除(因此需要从列表到哈希映射的反向链接)。
过去我在C/C++应用程序中使用了链表(甚至是双链表)。这是在.NET甚至STL之前。
我现在可能不会在.NET语言中使用链接列表,因为您需要的所有遍历代码都是通过LINQ扩展方法提供给您的。