为啥 Stack<T> 和 Queue<T> 用数组实现?

Posted

技术标签:

【中文标题】为啥 Stack<T> 和 Queue<T> 用数组实现?【英文标题】:Why are Stack<T> and Queue<T> implemented with an array?为什么 Stack<T> 和 Queue<T> 用数组实现? 【发布时间】:2014-03-24 00:40:02 【问题描述】:

我正在阅读 Albahari 兄弟的 C# 4.0 in a Nutshell,我发现了这个:

堆栈是在内部使用 根据需要调整大小的数组实现的,与队列和列表一样。 (第 288 页,第 4 段)

我不禁想知道为什么。 LinkedList 提供 O(1) 头尾插入和删除(这应该适用于堆栈或队列)。可调整大小的数组具有 O(1) 分期插入(如果我没记错的话),但 O(n) 最坏情况(我不确定删除)。而且它可能比链表使用更多的空间(对于大型堆栈/队列)。

还有比这更多的吗?双向链表实现的缺点是什么?

【问题讨论】:

还有一点是底层数组是循环使用的,所以数组元素在头部和尾部移动时被回收(如果没有超出边界)。 三个字:内存管理开销。 @SebastianNegraszus 谢谢。你是怎么找到的?我搜索了很多,没有找到任何东西。 @KooKoo 是本页“相关”下的热门链接之一。我不能说我是否会通过搜索找到它。 【参考方案1】:

但 O(n) 最坏情况

摊销的最坏情况仍然是 O(1)。插入时间的长短均化——这就是摊销分析的重点(删除也是如此)。

数组也比链表使用更少空间(毕竟链表必须为每个元素存储一个额外的指针)。

此外,开销比使用链表要少得多。总而言之,对于几乎所有用例来说,基于数组的实现(非常)效率更高,即使偶尔访问会花费更长的时间(事实上,队列可以通过以下方式更有效地实现页面本身在链接列表中管理的优势 - 请参阅 C++'std::deque 实现)。

【讨论】:

@Femaref:不——它真的叫deque,而不是dequeue 另外,使用数组会给你带来局部性的好处。 哦,对不起。拼写“queue”是一个很常见的错误,所以我以为你已经上当了,无意冒犯。【参考方案2】:

这里粗略估计了 100 个System.Int32s 堆栈所使用的内存资源:

数组实现需要以下内容:

type designator                          4 bytes
object lock                              4
pointer to the array                     4 (or 8)
array type designator                    4
array lock                               4
int array                              400
stack head index                         4
                                       ---
Total                                  424 bytes  (in 2 managed heap objects)

链表实现需要以下内容:

type designator                          4 bytes
object lock                              4
pointer to the last node                 4 (or 8)
node type designator         4 * 100 = 400
node lock                    4 * 100 = 400
int value                    4 * 100 = 400
pointer to next node  4 (or 8) * 100 = 400 (or 800)
                                     -----
Total                                1,612 bytes  (in 101 managed heap objects)

数组实现的主要缺点是在需要扩展数组时复制数组。忽略所有其他因素,这将是一个 O(n) 操作,其中 n 是堆栈中的项目数。这似乎是一件非常糟糕的事情,除了两个因素:它几乎从未发生过,因为每次递增时扩展都会加倍,并且数组复制操作经过高度优化并且速度惊人。因此,在实践中,扩展很容易被其他堆栈操作淹没。

队列也是如此。

【讨论】:

您的假设仅对值类型是正确的。如果 T 是引用类型,则两种实现都需要几乎相同的资源。因为数组条目将是每个项目对堆元素的引用。 @MichaelBarabash - 这取决于您所说的“几乎相同”是什么意思。如果您将我给出的示例从 Int32 转换为引用类型,那么一切都是一样的,只是您将引用类型值的存储添加到两者中,这将是完全相同的。如果您使用的是 64 位,那么您还将存储值的大小加倍以适应更大的引用,但无论哪种方式,两种方法的总大小都会增加完全相同的数量。因此,链表使用的附加存储仍然是附加的。 (续...) 但是,您正确的意思是链表开销占总数的比例较小。 嗨,杰弗里,完全同意。【参考方案3】:

这是因为 .NET 是为在现代处理器上运行而设计的。这比内存总线快得多。处理器以大约 2 GHz 的速度运行。您机器中的 RAM 的时钟频率通常为几百兆赫。从 RAM 中读取一个字节需要超过一百个时钟周期。

这使得 CPU 缓存在现代处理器上非常重要,大量的芯片空间被烧毁以使缓存尽可能大。今天的典型情况是 64 KB 用于 L1 高速缓存,这是最快的内存,并且物理位置非常靠近处理器内核,L2 高速缓存为 256 KB,速度较慢且离内核较远,L3 高速缓存约为 8 MB,速度较慢且最远离开,由芯片上的所有内核共享。

为了使缓存有效,顺序访问内存非常重要。如果需要 L3 或 RAM 存储器访问,读取第一个字节可能非常昂贵,接下来的 63 个字节非常便宜。 “缓存线”的大小,内存总线的数据传输单位。

这使得 array 成为迄今为止最有效的数据结构,它的元素按顺序存储在内存中。链表是迄今为止最糟糕的数据结构,它的元素自然地分散在内存中,可能会导致每个元素的高速缓存未命中。

因此,所有 .NET 集合,除了 LinkedList 都在内部实现为数组。请注意, Stack 已经自然地实现为数组,因为您只能从数组末尾推送和弹出元素。 O(1) 操作。调整数组大小的摊销成本为 O(logN)。

【讨论】:

以上是关于为啥 Stack<T> 和 Queue<T> 用数组实现?的主要内容,如果未能解决你的问题,请参考以下文章

queue 的使用

stack栈和Queue队列

stack queue priority_queue

C++——stack和queue的简介和使用

C++——stack和queue的简介和使用

简单计算器-栈stack和队列queue