用于实现堆栈的链表与动态数组

Posted

技术标签:

【中文标题】用于实现堆栈的链表与动态数组【英文标题】:Linked list vs. dynamic array for implementing a stack 【发布时间】:2011-11-16 14:36:03 【问题描述】:

在我最后一年的学校开始之前,我已经开始复习数据结构和算法,以确保我掌握了一切。一个审查问题是“使用链表或动态数组实现堆栈并解释为什么你做出了最佳选择”。

对我来说,使用带有尾指针的列表来实现堆栈似乎更直观,因为它可能需要经常调整大小。似乎对于大量数据,列表是更好的选择,因为动态数组重新调整大小是一项昂贵的操作。此外,使用列表,您无需分配比实际需要更多的空间,因此空间效率更高。

但是,动态数组肯定可以更快地添加数据(除非需要调整大小)。但是,我不确定使用数组是否整体上更快,或者仅在不需要调整大小的情况下。

这本书的解决方案说“对于存储非常大的对象,列表是一个更好的实现”,但我不明白为什么。

哪种方式最好?应该使用哪些因素来确定哪种实现是“最好的”?另外,我的逻辑有问题吗?

【问题讨论】:

顺便说一句——列表实现不需要尾部指针:您是否在 头部 处插入和删除。 【参考方案1】:

如果您的实现设计得当,调整动态数组的大小不会是一项昂贵的任务。

例如,要扩大数组,如果它已满,则创建一个两倍大小的新数组,然后复制项目。

添加 N 项最终的摊销成本约为 3N。

【讨论】:

【参考方案2】:

我想你自己回答了这个问题。对于具有大量项目的堆栈,当简单地将额外项目添加到堆栈顶部时,动态数组将具有过多的开销成本(复制开销)。使用列表,它是一个简单的指针切换。

【讨论】:

【参考方案3】:

重要的是在运行任务的过程中调用 malloc() 的次数。获得一块内存可能需要数百到数千条指令。 (free() 或 GC 中的时间应该与此成正比。)另外,保持透视感。这可能是总时间的 99%,或者只有 1%,这取决于发生了什么。

【讨论】:

【参考方案4】:

这里涉及到许多权衡,我认为这个问题没有“正确”的答案。

如果您使用带有尾指针的链表来实现堆栈,那么最坏情况下的 push、pop 或 peek 运行时间是 O(1)。但是,每个元素都会有一些与之相关的额外开销(即指针),这意味着结构总是存在 O(n) 开销。此外,根据内存分配器的速度,为堆栈分配新节点的成本可能会很明显。此外,如果您要不断地从堆栈中弹出所有元素,则可能会因局部性差而导致性能下降,因为无法保证链表单元格将连续存储在内存中。

如果您使用动态数组实现堆栈,则推送或弹出的 摊销 运行时为 O(1),而最坏情况下的偷看成本为 O(1)。这意味着如果您关心堆栈中任何单个操作的成本,这可能不是最好的方法。也就是说,分配并不频繁,因此添加或删除 n 个元素的总成本可能比基于链表的方法中的相应成本更快。此外,这种方法的内存开销通常比链表的内存开销要好。如果您的动态数组只存储指向元素的指针,那么最坏情况下的内存开销会在填充一半元素时发生,在这种情况下会有 n 个额外的指针(与使用链接的情况相同)列表),并且在动态数组已满的最佳情况下,没有空单元格,额外开销为 O(1)。另一方面,如果您的动态数组直接包含元素,那么在最坏的情况下内存开销可能会更糟。最后,由于元素是连续存储的,如果您想连续地从堆栈中压入或弹出元素,则有更好的局部性,因为所有元素在内存中都彼此相邻。

简而言之:

链表方法对每个操作都有最坏情况 O(1) 保证;动态数组具有摊销 O(1) 保证。 链表的局部性不如动态数组的局部性。 假设两个都存储指向其元素的指针,动态数组的总开销可能小于链表的总开销。 如果直接存储元素,动态数组的总开销很可能大于链表。

这些结构中没有一个明显比另一个“更好”。这实际上取决于您的用例。找出哪个更快的最好方法是对两者都计时,看看哪个性能更好。

希望这会有所帮助!

【讨论】:

很高兴提到局部性,我认为这是支持动态数组的胜利论据。 The total overhead of the dynamic array is likely to be greater than that of the linked list if the elements are stored directly.“直接存储”是什么意思? @Leonid Vasilyev 我正在设想一个 C 或 C++ 样式的数组,其中数组元素直接存储在数组中,而不是一个 Java 或 Python 数组,其中数组包含对实际的引用元素。 @LeonidVasilyev 假设您存储了 n 个元素。假设您有一个动态数组,其中后备数组的大小为 2n(也许您最近刚刚将数组的大小翻了一番。)您需要的总内存大约是一个指针(知道数组在哪里),两个整数 (知道大小和容量),以及 2n * sizeof(entry) 个字节用于这些项目的存储空间。总计为 2n * sizeof(entry) + 3 * sizeof(pointer)。对于链表,元素有 n * sizeof(entry) 个字节,指针有 n * sizeof(pointer) 个字节。 Totaly 是 n * (sizeof(entry) + sizeof(pointer)。 @LeonidVasilyev 这里的区别是链表只在需要时为实际条目分配空间。这意味着如果你有一个非常大的对象的链表并将它与非常大的对象的动态数组进行比较,你会发现 n * (sizeof(entry) + sizeof(pointer)) 【参考方案5】:

好吧,对于小对象与大对象的问题,如果堆栈上有小对象,请考虑为链表使用多少额外空间。然后考虑如果您的堆栈上有一堆对象,您需要多少额外空间。

接下来,考虑相同的问题,但使用基于动态数组的实现。

【讨论】:

因此,对于一个动态数组,您必须留出额外空间以避免经常调整大小,当使用大对象时,这意味着需要留下大量额外空间。另一方面,使用列表,您不必留下任何额外的空间,如果对象非常大,这使得列表成为更好的选择。看起来对吗? 好吧,如果您使用链表,则需要 一些 额外空间,因为每个链接都占用空间。

以上是关于用于实现堆栈的链表与动态数组的主要内容,如果未能解决你的问题,请参考以下文章

链表06-开发可用链表(根据索引取得数据)

链表的实现(Linked List)

静态链表和动态链表的区别

链表与数组的比较

数据结构与算法--必知必会

浅谈单链表与双链表的区别