题目介绍
力扣i225题:https://leetcode-cn.com/problems/implement-stack-using-queues/
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通队列的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
- void push(int x) 将元素 x 压入栈顶。
- int pop() 移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
- 你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty这些操作。
- 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
进阶:你能否实现每种操作的均摊时间复杂度为 O(1) 的栈?换句话说,执行 n 个操作的总时间复杂度 O(n) ,尽管其中某个操作可能需要比其他操作更长的时间。你可以使用两个以上的队列。
分析
这道题目涉及到栈和队列两种数据结构。它们的共同特点是,数据元素以线性序列的方式存储;区别在于,元素进出的方式不同。
队列本身对数据元素的保存,是完全符合数据到来次序的,同时也保持这个顺序依次出队。而弹栈操作的实现,是要删除最后进入的数据,相当于反序弹出。
实现的基本思路是,我们可以用一个队列保存当前所有的数据,以它作为栈的物理基础;而为了保证后进先出,我们在数据入队之后,就把它直接移动到队首。
方法一:两个队列实现
可以增加一个队列来做辅助。我们记原始负责存储数据的队列为queue1,新增的辅助队列为queue2。
-
当一个数据x压栈时,我们不是直接让它进入queue1,而是先在queue2做一个缓存。默认queue2中本没有数据,所以当前元素一定在队首。
queue1:a b
queue2:x -
接下来,就让queue1执行出队操作,把之前的数据依次输出,同时全部添加到queue2中来。这样,queue2就实现了把新元素添加到队首的目的。
queue1:
queue2:x a b -
最后,我们将queue2的内容复制给queue1做存储,然后清空queue2。在代码上,这个实现非常简单,只要交换queue1和queue2指向的内容即可。
queue1:x a b
queue2:
而对于弹栈操作,只要直接让queue1执行出队操作,删除队首元素就可以了
代码演示如下:
// 使用两个队列实现自定义栈
public class MyStack {
// 定义两个队列
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
// 入栈方法
public void push(int x){
// 1. 把x保存到queue2中
queue2.offer(x);
// 2. 将queue1中所有元素依次出队,然后放入queue2
while (!queue1.isEmpty()){
queue2.offer( queue1.poll() );
}
// 3. 交换两个队列
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
// 出栈操作
public int pop(){
// queue1出队就是出栈
return queue1.poll();
}
// 获取栈顶元素
public int top(){
return queue1.peek();
}
// 判断为空
public boolean empty(){
return queue1.isEmpty();
}
}
复杂度分析
- 时间复杂度:入栈操作 O(n),其余操作都是 O(1)。push:入栈操作,需要将queue1中的 n 个元素出队,并入队 n+1个元素到 queue2,总计 2n+1 次操作。每次出队和入队操作的时间复杂度都是 O(1),因此入栈操作的时间复杂度是O(n)。pop:出栈操作,只是将queue1的队首元素出队,时间复杂度是O(1)。top:获得栈顶元素,对应获得queue1的队首元素,时间复杂度是O(1)。isEmpty:判断栈是否为空,只需要判断queue1是否为空,时间复杂度是 O(1)。
- 空间复杂度:O(n),其中 n 是栈内的元素。需要使用两个队列存储栈内的元素。
方法二:一个队列实现
当一个新的元素x压栈时,其实我们可以不借助辅助队列,而是让它直接入队queue1,它会添加在队尾。然后接下来,只要将之前的所有数据依次出队再重新入队添加进queue1,就自然让x移动到队首。
代码演示如下:
// 用一个队列实现自定义栈
public class MyStack2 {
// 定义一个队列
Queue<Integer> queue;
public MyStack2() {
queue = new LinkedList<>();
}
public void push(int x){
// 1. 首先记录当前队列长度
int l = queue.size();
// 2. 把x入队
queue.offer(x);
// 3. 把queue中原先的所有元素依次出队,然后再入队
for (int i = 0; i < l; i++)
queue.offer( queue.poll() );
}
public int pop(){
return queue.poll();
}
public int top(){
return queue.peek();
}
public boolean empty(){
return queue.isEmpty();
}
}
复杂度分析
- 时间复杂度:入栈操作 O(n),其余操作都是 O(1)。 push:入栈操作,需要将queue1中的 n 个元素出队,并入队 n+1个元素到 queue2,总计 2n+1 次操作。每次出队和入队操作的时间复杂度都是 O(1),因此入栈操作的时间复杂度是 O(n)。pop:出栈操作。只是将queue1的队首元素出队,时间复杂度是 O(1)。top:获得栈顶元素,对应获得queue1的队首元素,时间复杂度是 O(1)。isEmpty:判断栈是否为空,只需要判断queue1是否为空,时间复杂度是 O(1)。
- 空间复杂度:O(n),其中 n 是栈内的元素。需要使用两个队列存储栈内的元素。