题解 | #用两个栈实现队列#我用均摊复杂度 已经击败100%的对手啦!
思路如下
无论是「用栈实现队列」还是说「用队列实现栈」,其实思路都是类似的。
都可以通过用两个 栈或者队列 来解决。
现在我们创建两个栈,分别是 out
和 in
,用作处理「输出」和「输入」两个操作。
其实就是两个栈来回的「倒腾」
而对于「何时倒腾」决定了是 O(n) 解法 还是 均摊 O(1) 解法。
1 先来说 O(n) 解法
我们创建两个栈,分别是 out 和 in:
in
用作处理输入操作 push(),使用 in 时需确保 out 为空out
用作处理输出操作 pop() 和 peek(),使用 out 时需确保 in 为空
class MyQueue {
Deque<Integer> out, in;
public MyQueue() {
in = new ArrayDeque<>();
out = new ArrayDeque<>();
}
public void push(int x) {
while (!out.isEmpty()) in.addLast(out.pollLast());
in.addLast(x);
}
public int pop() {
while (!in.isEmpty()) out.addLast(in.pollLast());
return out.pollLast();
}
public int peek() {
while (!in.isEmpty()) out.addLast(in.pollLast());
return out.peekLast();
}
public boolean empty() {
return out.isEmpty() && in.isEmpty();
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2 再来说均摊 O(1) 解法
事实上,我们不需要在每次的「入栈」和「出栈」操作中都进行「倒腾」。
我们只需要保证,输入的元素总是跟在前面的输入元素的后面就行啦,而输出元素总是最早输入的那个元素即可。
我们可以通过调整「倒腾」的时机来确保满足上述要求,但又不需要发生在每一次操作中:
- 即只有在「输出栈」为空的时候,才发生一次性的「倒腾」
//java的版本
class MyQueue {
Deque<Integer> out, in;
public MyQueue() {
in = new ArrayDeque<>();
out = new ArrayDeque<>();
}
public void push(int x) {
in.addLast(x);
}
public int pop() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.addLast(in.pollLast());
}
return out.pollLast();
}
public int peek() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.addLast(in.pollLast());
}
return out.peekLast();
}
public boolean empty() {
return out.isEmpty() && in.isEmpty();
}
}
- 时间复杂度:pop() 和 peek()两个操作都是均摊,所以是
O(1)
- 空间复杂度:O(n)
你可能没听过均摊复杂度
3 我说明下
我们可以用另外一个例子来理解「均摊复杂度」,你就明白了,大家都知道「哈希表」的底层是通过数组来实现的。
正常情况下,我们要算元素在哈希桶的位置,然后放入哈希桶里面,复杂度为 O(1),我们可以通过简单的“拉链法”搭配「头插法」方式来解决哈希冲突的问题。
但是当某次元素插入后,「哈希表」能达到扩容的阈值,那么需要对底层所使用的数组进行扩容,这个复杂度是 O(n)
很显然「扩容」操作不会发生在每一次的元素插入中,扩容的 O(n) 都会伴随着 n 次的 O(1),换句话说 O(n) 的复杂度会被均摊到每一次插入当中去,所以哈希表插入仍然还是 O(1)
的。
同样的道理,我们的「倒腾」也不是发生在每一次的「输出操作」中,而是集中发生在一次「输出栈为空」的这个时候,所以 pop
和 peek
都是均摊复杂度为 O(1) 的操作。
由于本题的调用次数它只有 100 次,所以铁定是一个人均 100% 的算法(也就是花费 0 ms)🐶 🐶
但是我们需要对操作进行复杂度分析进行判断,而不是看时间来判断自己是不是均摊 O(1) 哈 ~