我遇到了与堆栈中的push和pop相关的运行时问题。
在这里,我使用数组实现了一个堆栈。
当我将一个新元素插入一个完整的堆栈时,我想避免堆栈中的溢出,所以当堆栈已满时,我会执行以下操作(伪代码):
(我认为堆栈是一个数组)
生成一个大小为原始数组的两倍的新数组。
以相同的顺序将原始堆栈中的所有元素复制到新数组中。
现在,我知道对于大小为n的堆栈的单次推送操作,操作在最坏的情况下在O(n)中执行。
我想证明在最坏的情况下n推送到空堆栈的运行时也是O(n)。
另外,如何更新此算法,在最坏的情况下,对于每次推送,操作将在常量运行时执行?
答案 0 :(得分:3)
如果不比常数时间替代方案更好,摊销的常数时间在实践中通常同样好。
- 生成一个大小为原始数组的两倍的新数组。
- 以相同的顺序将原始堆栈中的所有元素复制到新数组中。
醇>
对于堆栈实现来说,这实际上是一个非常体面和可敬的解决方案,因为它具有良好的引用局部性,并且重新分配和复制的成本被分摊到几乎可以忽略不计的程度。 Java中的ArrayList
或C ++中的std::vector
等“可扩展数组”的大多数通用解决方案都依赖于这种类型的解决方案,尽管它们的大小可能不会完全加倍(许多std::vector
实现增加了它们大小接近1.5而不是2.0)。
这比听起来要好得多的原因之一是因为我们的硬件在顺序复制位和字节方面非常快。毕竟,我们经常依赖数百万像素在我们的日常软件中每秒多次出现。这是从一个图像到另一个图像(或帧缓冲区)的复制操作。如果数据是连续的并且只是顺序处理,我们的硬件可以非常快速地完成。
另外,如何更新此算法,以便每次推送操作 在最坏的情况下会在常量运行时执行吗?
我已经提出了C ++中的堆栈解决方案,它比std::vector
更快,可以推送和弹出一亿个元素并满足您的要求,但仅限于以LIFO模式推送和弹出。我们谈论的是向量为0.22秒而不是我的堆栈的0.19秒。这依赖于只分配这样的块:
...当然,每个块通常有超过5个元素的数据! (我只是不想绘制史诗图)。每个块都存储一系列连续数据,但当它填满时,它会链接到下一个块。这些块是链接的(仅存储前一个链接),但每个块可能存储具有64字节对齐的512字节数据。这允许持续时间推送和弹出,而无需重新分配/复制。当一个块填满时,它只是将一个新块链接到前一个块并开始填充它。当你弹出时,你只是弹出直到块变空,然后一旦它变空,你遍历它的前一个链接到它之前的前一个块然后开始弹出(你也可以在这一点上释放现在空的块)
这是数据结构的基本伪C ++示例:
template <class T>
struct UnrolledNode
{
// Points to the previous block. We use this to get
// back to a former block when this one becomes empty.
UnrolledNode* prev;
// Stores the number of elements in the block. If
// this becomes full with, say, 256 elements, we
// allocate a new block and link it to this one.
// If this reaches zero, we deallocate this block
// and begin popping from the previous block.
size_t num;
// Array of the elements. This has a fixed capacity,
// say 256 elements, though determined at runtime
// based on sizeof(T). The structure is a VLS to
// allow node and data to be allocated together.
T data[];
};
template <class T>
struct UnrolledStack
{
// Stores the tail end of the list (the last
// block we're going to be using to push to and
// pop from).
UnrolledNode<T>* tail;
};
那就是说,我实际上推荐你的解决方案来代替性能,因为我的简单重新分配和复制解决方案几乎没有性能优势,而你的遍历因为它可以直接遍历数组而略有优势。顺序时尚(如果需要,还可以直接随机访问)。出于性能原因,我实际上没有实现我的。我实现它是为了防止指针在你向容器推送时失效(实际上是C中的内存分配器),并且,尽管实现了真正的恒定时间回退和回弹,它仍然几乎没有任何更快而不是涉及重新分配和记忆复制的摊还的固定时间解决方案。