有多种不同的方法可以使用链表和数组来实现队列和堆栈,我不确定您要寻找哪些。不过,在分析这些结构之前,让我们回顾一下上述数据结构的一些重要的运行时注意事项。
在只有一个头指针的单链表中,添加一个值的成本是 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,其中每个链表单元格包含多个值。这略微增加了查找的引用位置并减少了使用的分配数量。其他选项(例如,使用以索引为键的平衡树)代表一组不同的权衡。
希望这会有所帮助!