【问题标题】:Array-Based vs List-Based Stacks and Queues基于数组与基于列表的堆栈和队列
【发布时间】:2011-11-20 14:07:11
【问题描述】:

我试图比较堆栈和队列操作在同时作为数组和链表实现时的增长率(运行时和空间)。到目前为止,我只能找到队列pop()s 的平均案例运行时间,但没有任何东西可以全面探索这两种数据结构并比较它们的运行时间/空间行为。

具体来说,我希望比较 push()pop() 的队列和堆栈,实现为 数组和链表(因此 2 个操作 x 2 个结构 x 2 个实现,或8 个值)。

此外,我很欣赏这两种情况的最佳、平均和最坏情况值,以及与它们消耗的空间量相关的任何值。

我能找到的最接近的东西是这个“所有 cs 备忘单之母”pdf,它显然是高级算法和离散函数的硕士或博士级备忘单。

我只是在寻找一种方法来确定我应该在何时何地对堆栈和队列使用基于数组的实现与基于列表的实现。

【问题讨论】:

  • 您是否编写并分析了竞争实现?
  • 不,我喜欢保留它DRY

标签: arrays data-structures stack queue linked-list


【解决方案1】:

有多种不同的方法可以使用链表和数组来实现队列和堆栈,我不确定您要寻找哪些。不过,在分析这些结构之前,让我们回顾一下上述数据结构的一些重要的运行时注意事项。

在只有一个头指针的单链表中,添加一个值的成本是 O(1) - 我们只需创建新元素,将其指针连接到列表的旧头,然后更新头指针。删除第一个元素的成本也是 O(1),这是通过更新头指针指向当前头之后的元素来完成的,然后为旧头释放内存(如果执行显式内存管理)。但是,由于动态分配的代价,这些 O(1) 项中的常数因子可能很高。由于在每个元素中存储了一个额外的指针,链表的内存开销通常为 O(n) 总额外内存。

在动态数组中,我们可以在 O(1) 时间内访问任何元素。我们还可以在amortized O(1) 中附加一个元素,这意味着 n 次插入的总时间为 O(n),尽管任何插入的实际时间界限可能要差得多。通常,动态数组的实现方式是,通过附加到预分配的空间,使大多数插入花费 O(1),但通过将数组容量加倍并复制元素,使少量插入在 Θ(n) 时间内运行。有一些技术可以通过分配额外的空间和懒惰地复制元素来尝试减少这种情况(例如,参见this data structure)。通常,动态数组的内存使用情况非常好 - 例如,当数组完全填满时,只有 O(1) 额外开销 - 尽管在数组大小翻倍之后可能有 O(n) 未使用在数组中分配的元素。由于分配不频繁且访问速度很快,因此动态数组通常比链表快。

现在,让我们考虑如何使用链表或动态数组来实现堆栈和队列。有很多方法可以做到这一点,所以我假设您正在使用以下实现:

让我们依次考虑。

由单链表支持的堆栈。 因为单链表支持 O(1) 时间前置和先删除,所以推送或弹出到支持链表的成本stack 也是 O(1) 最坏情况。但是,添加的每个新元素都需要新的分配,与其他操作相比,分配可能会很昂贵。

由动态数组支持的堆栈。 可以通过将新元素附加到动态数组来实现推入堆栈,这需要平均 O(1) 时间和最坏情况 O(n)时间。从堆栈中弹出可以通过仅删除最后一个元素来实现,该元素在最坏情况下运行 O(1)(如果您想尝试回收未使用的空间,则为摊销 O(1))。换句话说,最常见的实现有最佳情况 O(1) 推送和弹出,最坏情况 O(n) 推送和 O(1) 弹出,以及摊销 O(1) 推送和 O(1) 弹出。 /p>

由单链表支持的队列。 加入链表可以通过附加到单链表的后面来实现,这需要最坏情况的时间 O(1)。可以通过删除第一个元素来实现出队,这也需要最坏情况的时间 O(1)。这也需要为每个队列分配新的内存,这可能会很慢。

由不断增长的循环缓冲区支持的队列。 通过在循环缓冲区的下一个空闲位置插入一些内容,将其加入循环缓冲区。这通过在必要时增长数组,然后插入新元素来工作。对动态数组使用类似的分析,这需要最佳情况时间 O(1)、最坏情况时间 O(n) 和摊销时间 O(1)。通过移除循环缓冲区的第一个元素来从缓冲区中出队,这在最坏的情况下需要 O(1) 时间。

总而言之,所有结构都支持在 O(n) 时间内推送和弹出 n 个元素。链表版本具有更好的最坏情况行为,但由于执行的分配次数可能会有更差的整体运行时间。数组版本在最坏的情况下速度较慢,但​​如果每次操作的时间不太重要,则具有更好的整体性能。

这些不是您实施列表的唯一方法。您可以有一个unrolled linked list,其中每个链表单元格包含多个值。这略微增加了查找的引用位置并减少了使用的分配数量。其他选项(例如,使用以索引为键的平衡树)代表一组不同的权衡。

希望这会有所帮助!

【讨论】:

  • VList链接无效。
  • @sbhatla 感谢您的提醒。我已经更改了链接。
  • 什么是块列表?你指的是这样的事情吗? en.wikipedia.org/wiki/Unrolled_linked_list
  • @Alex 是的!我以不同的名称了解到它...抱歉造成混淆!
  • 使用循环缓冲区的有趣想法。关于 LinkedList 实现,您可以将节点池化,以免在队列“预热”后浪费时间重新分配和分配内存。这是游戏开发中的常见模式,每秒实例化如此多的游戏对象。只需在池中移动节点并在需要时重新分配它的值。
【解决方案2】:

对不起,如果我误解了您的问题,但如果我没有,那么我相信这就是您正在寻找的答案。

使用向量,您只能在容器末尾有效地添加/删除元素。 使用双端队列,您可以有效地在容器的开头/结尾添加/删除元素。 使用列表,您可以有效地在容器中的任何位置插入/删除元素。

vectors/deque 允许随机访问迭代器。 列表只允许顺序访问。

您需要如何使用和存储数据是您如何确定最合适的数据。

编辑:

这还有很多,我的回答非常笼统。如果我知道你的问题是关于什么的,我可以更深入地讨论。

【讨论】:

  • 感谢您的输入 fhaddad78 - “向量”是什么意思?
猜你喜欢
  • 1970-01-01
  • 2016-09-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-20
  • 2023-03-11
  • 2015-10-07
  • 1970-01-01
相关资源
最近更新 更多