这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解这个题目了, 所以就将整个过程写下来了。
这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
https://space.bilibili.com/8888480?spm_id_from=333.999.0.0
顺便提供一下测试链接:
- 用栈实现队列:https://leetcode.cn/problems/implement-queue-using-stacks/
- 用队列实现栈:https://leetcode.cn/problems/implement-stack-using-queues/
1. 用栈实现队列
1.1 逻辑结构
可以用两个“栈”实现一个队列, 一个栈是 in栈
, 另一个是 out栈
, 用一段数据放入 in栈
, 然后将 in栈
中的数据放入 out栈
因为“栈”的特点是:后进先出, 所以经过两次后进先出就成了先进先出了.
下面的图片可以很好解释, 将 a, b, c, d
放入 in栈
, 再将 in栈
中的数据进行弹出放到 out栈
, 最后将 out栈
中的数据弹出, (所有实现都是利用 栈
的后进先出的方式), 这样最后的数据也是:“先进先出”了.
1.2 代码实现
所有的实现都在下面的代码中, 而且每一条代码写上了注释, 应该可以理解.
以下的所有方法, 若是有理解不了的地方, 可以自己画一下逻辑结构, 可以尝试放入一组数据, 简单画一下就能理解.
public class ConvertQueueAndStack {
class MyQueue {
public Stack<Integer> in; // 使用系统自己的“栈”结构
public Stack<Integer> out;
public MyQueue() {
in = new Stack<Integer>();
out = new Stack<Integer>();
}
// 看下面的代码的时候, 要结合“队列”的特点来看.
// 倒数据
// 从in栈,把数据倒入out栈
// 1) out空了,才能倒数据
// 2) 如果倒数据,in必须倒完
private void inToOut() {
if (out.empty()) { // 保证“out栈”为“空”, 才能将“in栈”中的数据倒进去.
while (!in.empty()) { // 先判断“in栈”中是不是“空”, 只要不是“空”就全部倒进“out”栈
out.push(in.pop());
// 这行代码的意思是:将“in栈”中的数据弹出来一个, 然后再由“out栈”将弹出来的数据接收.
}
}
}
public void push(int x) {
in.push(x); // 先往“in栈”中放进去一个“数据”,
inToOut(); // 然后立刻将“in栈”中的数据放入“out栈”. 要是能放进去就放, 不能放就还存在“in栈”中.
} // 因为我们拿出“数据”都是从“out栈”中拿出.
public int pop() {
inToOut(); // 相当于先检查一下“out栈”中有没有数据, 要是没有数据根本就不会弹出数据.
return out.pop(); // 然后弹出“out栈”中最上方的“数据”.
}
// 结合“队列”的特点, 队列的顶部(头部)是“l”, 那这样的话, 不就是“out”栈中顶部的数据吗.
public int peek() { // 和上面一样, 先检查一下“out栈”中有没有数据, 要是没有数据根本就返回不了.
inToOut();
return out.peek(); // 然后返回“out栈”中的“栈顶元素”.
}
public boolean empty() {
return in.isEmpty() && out.isEmpty();// 判断这个“队列”中为不为空, 只有“in栈和out栈”同时不为“空”才行.
} // 因为只要为任何一个为“空”就相当于整个“队列”为“空”.
}
1.3 时间复杂度分析
结论:均摊之后的时间复杂度是:O(1)
.
分析:因为一个“数据”, 最开始是放入 in栈
, 然后从 in栈
出, 放入 out栈
, 然后从 out栈
出来, 让用户使用, 无论怎么样, 进去多少个数字, 平均每一个数字最多执行 4
次, 因为不可能从 out栈
回到 in栈
, 哪怕是倒入 1000000000
个数字, 那每一个数字均摊下来的时间复杂度是:O(1)
.
2. 用队列实现栈
2.1 逻辑结构
注意:这里的队列就是货真价实的队列, 没有什么别的特殊的性质, 就是一个最基本的队列, 一定是符合 先进先出
的特点的.
- 将“数据”放入队列中, 只能从尾部放入, 也只能从头部出.
- 先放入一个“数据
a
”, 此时不用管任何事, 队列中只有a
- 然后放入下一个数据
b
(继续放入数据), 放入之后, 此时队列元素是:a, b
, 将a
从头部放出, 然后从尾部放入, 此时队列元素是:b, a
. - 之后以此类推, 先记录一下放入“数据”之前, 队列中原来有几个元素, 然后将数据放入, 将原来所有的元素按顺序从队列的“头部出, 尾部进”, 这样就实现了用“队列实现栈”, 并且此时的“栈”的“栈底”是“队列的尾部”.
过程如下图所示, 自己可以随便用组数据测试一下就能理解了.
2.2 代码实例
所有的实现都在下面的代码中, 而且每一条代码写上了注释, 应该可以理解.
以下的所有方法, 若是有理解不了的地方, 可以自己画一下逻辑结构, 可以尝试放入一组数据, 简单画一下就能理解.
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<Integer>();
}
// O(n)
public void push(int x) {
int n = queue.size(); // 先确定放入数据之前, 队列中有几个数字.
queue.offer(x); // 然后将“数据”进行放入
for (int i = 0; i < n; i++) {
queue.offer(queue.poll()); // 最后将“放入数据”之前的所有的数据都从头部弹出, 从尾部放入.
} // 这样就能实现一个“栈”结构了.
}
public int pop() { // 因为想要弹出, 肯定需要放入数据, 所以原来已经执行过“push”操作了,
return queue.poll(); // 所以这里直接进行弹出就可以, 肯定是以“栈”的方式进行弹出的.
}
public int top() {
return queue.peek(); // 也是直接返回头部就可以. 还是和原来一样, 肯定执行了“push”
} // 所以直接返回“队列”的头部就行了,
public boolean empty() {
return queue.isEmpty(); // 直接判断是不是“空”, 这样不用说.
}
}
2.3 时间复杂度分析
结论:时间复杂度是:O(n)
.
分析:这个方法中, 每次进来一个数字, 都是要计算一下“队列”中原来的数字有多少, 所以这样就表示我放进来的数字越多, 那我需要执行的次数就越多, 比如我放入一个数字之前, “队列”中原来的数字有 10
个, 那我就要将这 10 个数字拿出来再放进去, 若是有 100
个, 我就要将这 100 个数字拿出来再放进去, 所以这个的时间复杂度是 O(n)
.
也可以这样理解:我放进来的数字越多, 那就说明我需要移动的次数越多 (比如拿 第一个放进来元素
) 为例, 若是一直放进来, 那原来放进来的元素就一直会进行移动, 这样移动次数是不确定的. 所以均摊下来, 只要我放进去 1
个数字, 那就将所有的数字执行 1
次, 每一个“数据”的时间复杂度是 O(n)
.
或者直接算一遍:放入 1
个“数据”前面需要执行 0
次, 放入 2
个数据, 前面需要执行 1 * 2
次, 放入 3
个“数据”前面需要执行 2 * 2
次, 所以以此类推:放入 n
个数字, 需要执行 (n - 1) * 2
次, 所以最后的时间复杂度是 O(2n - 1) -> O(n)
. (* 2
的是因为要从头弹出, 再从尾进去).