栈模拟队列是一个经典问题,因此将其记录下来。
与队列不同的是,栈是一个先进后出的数据结构,而队列是一个先进先出的数据结构,很容易想到想用栈模拟队列需要多个栈。
方法一 (入队O(n), 出队O(1))
该方法是在每个元素入队时对栈内的元素进行重排列,使得第一个进的元素位于栈顶。第二个栈的功能就是充当在重排列时的临时存储空间,具体过程如下图所示:
原本模拟的队列中已经插入了1、2两个元素,元素已经在栈中排列好,接着将插入3这个元素,首先将第一个栈的元素全部放入第二个栈中,如图所示:
此时将元素3放入第一个栈中:
最后将第二个栈中的元素重新放入第一个栈中,结果如图:
其排序原理是对于一组元素,入栈、出栈两次并不影响这组元素的顺序。由于栈是先进后出,也就是栈底的元素最后才能出栈。因此每一个元素在入栈时都是该元素及之前所有入栈的元素的最后一个,因此在第一个栈为空时放入栈。利用归纳法可以轻松证明该方法是正确的。
对于pop、top或者peek的访问操作,只需要对第一个栈进行操作即可,也就是第一个栈的栈顶元素。对整个队列进行非空判断也只需要对第一个栈进行判断即可。
这里需要讨论的是入栈时的时间复杂度,因为出栈的时间复杂度明显为O(1)。入栈时除了新入栈的元素,其余元素均入栈两次,出栈两次。因此入栈的时间复杂度为O(n)。
方法二 (入队O(1), 出队O(1))
该方法与第一种不同,主要思路是在入栈时将元素放入第一个栈中,而在出栈时弹出第二个栈的元素。如果第二个栈为空,就将第一个栈的元素全部放入第二个栈中。
这是因为将栈内的一组元素出栈后放入另一个栈中,元素顺序将颠倒,先插入的元素这时将位于第二个栈的栈顶,这也是较为常用的方法。
因此入栈的时间复杂度为O(1),而出栈的时间复杂度使用均摊复杂度衡量,摊还分析给出了所有操作的平均性能
。摊还分析的核心在于,最坏情况下的操作一旦发生了一次,那么在未来很长一段时间都不会再次发生,这样就会均摊每次操作的代价。
在本例中,每 n 次新元素入队产生这么一次代价为 n 的出队操作。因此总时间复杂度为:n(所有的入队操作产生) + 2 * n(第一次出队操作产生) + n - 1(剩下的出队操作产生),所以实际时间复杂度为 O(2*n)。每次操作的平均时间复杂度为 O(2n/2n)=O(1)。
我们也可以理解为每个元素一共只会有两次入栈操作、两次出栈操作,因此时间复杂度均为O(1)。
具体代码如下:
class MyQueue {
public:
stack<int> a, b;
/** Initialize your data structure here. */
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
a.push(x);
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
if(b.empty()){
while(!a.empty()){
b.push(a.top());
a.pop();
}
}
int sum = b.top();
b.pop();
return sum;
}
/** Get the front element. */
int peek() {
if(b.empty()){
while(!a.empty()){
b.push(a.top());
a.pop();
}
}
return b.top();
}
/** Returns whether the queue is empty. */
bool empty() {
if(a.empty() && b.empty())
return true;
else
return false;
}
};