这是一个更高效的链表实现

Which is a more efficient implementation of a linked list?

本文关键字:高效 链表 实现 一个      更新时间:2023-10-16

至少有两种方法可以表示链表:

1.)使用链表的基于数组的表示,其中我们保留类型的结构的std::vector

struct {
    <whatever-type-you-want> item ;
     int   nextitem; 
   }

在这里插入到列表中,就是对向量执行push_back()并给出下一项的适当值。

2) 在其中在整个RAM中都有一组结构。此处插入完成C++运算符CCD_ 2。

正确的说法是,第一种方法更有效,因为所有项目都在内存中的连续位置,因此可以扩大链表到比第二种方法大得多的尺寸

在第二种方法中,可能存在具有巨大链表的内存碎片,因为其中一个链表可能更早出现分段错误。

我会反对这里的其他人,说,是的,第一种方法可能会更有效。在第二种方法中,您在堆上分配内存O(N)次——N是列表中的节点数。如果您使用的是一个向量,那么您只进行了O(logN)数量的堆分配。

此外,如果您在64位机器上,如果您处理大量小项目,则在每个节点中保存指针的开销可能有点太大。使用向量,您可以使用较小的nextItem,例如,32位而不是64位,如果您正在制作一个包含32位int的列表,则这将使内存使用率提高1.5。

另一种可能的优化是,如果你提前知道你将处理很多元素,你可以保留一个大向量,并在很长一段时间内分配一个堆。

我最近上了一门关于自动机应用的课程,讲师正在为相当大的数据集实现一些算法。他告诉我们的技术之一正是你表示链表的第一种方法。我有一门课程,我尝试用两种方式实现(用指针、向量和nextItem之类的东西),向量一的效果要好得多(它也有其他优化,但向量肯定有效果)。

其他注意事项

我认为@smilingbuddha所问的更像是一个链表的集合——或者至少这就是我使用它的目的。例如,使用邻居列表保存图形时。您需要每个节点的所有邻居的链表(或数组,或其他)。因此,您不需要保留一个链表数组或向量向量,只需保留指向每个节点最后插入的邻居的索引数组。

用向量实现列表是错误的


我来解释。容器通常是为实现某一组目标而设计的,并且底层实现是基于这些目标来选择的。

向量非常好,因为它有连续的内存,并且可以通过指针运算到达任何单元格。不幸的是,在向量中心插入或删除元素时,向量的性能非常糟糕。

列出清单的目的恰恰相反。导航到列表中的一个点很耗时,因为它不连续,所以必须遵循链接。但是列表的主要目的是允许快速插入、删除、重新排序、拼接、反转等。


因此,将向量视为列表的实现基础(虽然可以做到)实际上并不是看待这一问题的方法。用向量实现列表基本上意味着你没有任何优势让你首先选择列表


编辑

正如其他人在下面的评论中指出的那样,如果您正在考虑更复杂的实现,那么您肯定可以从中获得性能优势。

例如,如果您维护一个引用了所有指针的向量,并努力保持该引用向量的顺序,则您可以获得指针算术访问的好处,同时仍然具有相对快速的删除/插入等。此外,由于引用向量只保存指向动态分配对象的指针,操作引用向量的成本并不高,而且您仍然不必使用大量的连续内存区域(在您的体系结构中,向量只需要NumElements*sizeof(指针))。

您应该看看std::deque实现以获得一些乐趣它们在由指针链接的连续内存区域之间有一些有趣的相互作用,以加快插入/删除/其他操作。

恰恰相反;使用第一种方法,从链表中删除项的效率很低,因为您"丢失"了存储该项的向量中的槽,并且必须以垃圾收集风格遍历整个列表,以发现哪些槽没有被使用。

关于内存碎片,拥有大量的小分配通常不是问题;事实上,由于向量需要是连续的,因此分配内存将导致碎片,因为您需要越来越大的连续内存块。此外,每次调整向量的大小时,都会导致复制大块内存。

事实上,您的第一个答案是僭越内存分配器和内存管理单元的工作。内存分配器的工作是分配小块内存;MMU(以及其他)的工作是确保存储器块之间的指针即使在物理存储器中移动时也继续指向相同的逻辑存储器。您的nextitemint成员本质上起到了指针的作用。除非您有非常专业的需求,否则硬件、内核和malloc可以比您做得更好。

您的逻辑完全是向后的。第一种方法要求内存是连续的,一旦可用的连续内存不足,就会出现故障。第二种方法可以使用内存,无论是否连续,并且将继续工作,直到完全没有内存。

您的第一种方法似乎混合了两种算法,因此,我认为效率较低。

链表的优点之一是可以很容易地插入和删除项目。然而,使用您的方法,它们需要转换数据。您还可以使用一个简单的可调整大小的数组。

此外,数组要求内存是连续的。在某些情况下,在处理大量数据时,您会比使用真正的链表更快地耗尽内存,因为有时可能会有一定数量的内存可用,但不是连续可用。

如果在案例#1中从列表中删除一个元素,那么剩余的大部分元素的nextitem索引可能会出错。因此,#2是通常的方法,如果实现得当,不会导致任何内存问题,除非你试图在列表或任何其他容器中插入数量惊人的元素。