请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(
push
、pop
、peek
、empty
)
- 直接上最优解
两个栈分别充当不同的角色:
- 一个只负责存(push)
- 一个只负责取(pop)
假设负责存和取的栈分别为s1
和s2
:
- 当存的时候只需要直接
push
到s1
即可,不需要考虑其他的情况 - 当取的时候,肯定直接从
s2
中取,如果s2
不为空有则直接s2.pop()
,如果s2
为空呢,就只能将s1
中的所有元素添加到s2
中,然后再执行s2.pop()
peek()
的实现逻辑与pop()
相同,只是peek()
只是拿到栈顶元素而不取出而已。
如下图阐述了整个过程:
Java实现如下:
class MyQueue {
private Stack<Integer> s1;
private Stack<Integer> s2;
/** Initialize your data structure here. */
public MyQueue() {
s1 = new Stack<>();
s2 = new Stack<>();
}
/** Push element x to the back of queue. */
public int push(int x) {
return s1.push(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
if(s2.isEmpty()){
while(!s1.isEmpty()){
s2.push(s1.pop());
}
}
return s2.pop();
}
/** Get the front element. */
public int peek() {
if(s2.isEmpty()){
while(!s1.isEmpty()){
s2.push(s1.pop());
}
}
return s2.peek();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return s1.isEmpty() && s2.isEmpty();
}
}
时间复杂度分析
- push()
显而易见,push
操作的时间复杂度为O(1)
- pop()
pop()
的时间复杂度为 O(1)
,有人可能会问,不是最坏情况下需要考虑将s1
中所有的元素取出然后push到s2
中吗?下面就来分析一下,pop()的时间复杂度为什么是 O(1)
而不是O(n)
。
pop()
操作的时间复杂度比较有意思,某一次pop()
操作的时间复杂度取决于s2
是否为空:
- 如果
s2
为不为空,则pop()
操作的时间复杂度为O(1)
- 如果
s2
为为空,需要将s1
中所有的元素取出然后push到s2
中,然后再执行s2.pop()
,则pop()
操作的时间复杂度取决于s1
中元素的个数。可以发现,只要执行一次「将s1
中所有的元素取出然后push到s2
中」的操作,那么,之后s2.pop()
操作的时间复杂度都为O(1)
,直到s2
再次为空。
摊还分析
摊还分析给出了所有操作的平均性能。摊还分析的核心在于,最坏情况下的操作一旦发生了一次,那么在未来很长一段时间都不会再次发生,这样就会均摊每次操作的代价。
单次s2.pop()
操作最坏情况下的时间复杂度为 O(n)。考虑到要做 n 次出队操作,如果用最坏情况下的时间复杂度来计算的话,那么所有操作的时间复杂度为 O(n^2)。然而,在一系列的操作中,最坏情况不可能每次都发生,在一系列的的s2.pop()
操作中,某一次操作为 O(n)的时间内复杂度摊还给后续的时间复杂度为O(1)的操作。
某次的出队操作最多可以执行的次数跟它之前执行过入队
操作的次数有关。虽然一次出队操作代价可能很大,但是每 n 次入队才能产生这么一次代价为 n 的 出队 操作。因此所有操作的总时间复杂度为:n(所有的入队操作产生) + 2 * n(第一次出队操作产生) + n - 1(剩下的出队操作产生), 所以实际时间复杂度为 O(2*n)。于是我们可以得到每次操作的平均时间复杂度为 O(2n/2n)=O(1)
。
- peek()
同理,peek()
的时间复杂度也为O(1)
- empty()
empty()
的时间复杂度为O(1)