Skip to content

232.用栈实现队列

难度:容易

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 只能 使用标准的栈操作 —— 也就是只有 push to toppeek/pop from topsizeis empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

提示:

  • 1 <= x <= 9
  • 最多调用 100pushpoppeekempty
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

进阶:

  • 你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。

解题思路

这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。

队列的特性是 FIFO(先入先出),而栈的特性是 FILO(先入后出)。

知道两者特性之后,我们需要用两个栈来模拟队列的特性,一个栈为 输入栈,一个栈为 输出栈

我们只需要保证,输入的元素总是跟在前面的输入元素的后面,而输出元素总是最早输入的那个元素即可。

  1. push 数据的时候,只要将数据放进输入栈就好

  2. pop 数据的时候,操作就复杂一些:

    1. 如果 输出栈 为空,就把 输入栈 的数据全部导入进来(注意是全部导入),再从 输出栈 栈顶弹出数据
    2. 如果 输出栈 不为空,则直接从 输出栈 栈顶弹出数据,因为此栈顶元素是最先进入 push 进队列的
  3. peek 和 pop 函数功能类似:

    1. 如果 输出栈 为空,就把 输入栈 的数据全部导入进来(注意是全部导入),再返回 输出栈 栈顶的数据
    2. 如果 输出栈 不为空,则直接返回 输出栈 栈顶的数据
  4. 最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空。

其实就是两个栈来回 倒腾,但只有在 输出栈为空 的时候,才发生 倒腾

注意:根据栈的的特性,我们仅能使用 push 和 pop 操作。

Deque 的常用方法

作用方法
栈顶添加void push(E e)boolean offerFirst(E e)
栈底添加boolean add(E e)boolean offer(E e)boolean offerLast(E e)
栈顶删除E remove()E pop()E poll()pollFirst()
栈底删除E pollLast()
栈顶查看E peek()E peekFirst()
栈底查看E peekLast()
栈的大小int size()
栈是否为空boolean isEmpty()

代码展示

java
class MyQueue {
    Deque<Integer> inStack;
    Deque<Integer> outStack;

    public MyQueue() {
        inStack = new LinkedList<Integer>();
        outStack = new LinkedList<Integer>();
    }

    public void push(int x) {
        inStack.push(x);
    }

    public int pop() {
        if (outStack.isEmpty()) {
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
        }
        return outStack.pop();

    }

    public int peek() {
        if (outStack.isEmpty()) {
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
        }
        return outStack.peek();
    }

    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }
}

时间复杂度:pop()peek() 操作都是均摊 O(1)

空间复杂度:O(n)

关于均摊复杂度的说明

我们先用另外一个例子来理解「均摊复杂度」,我们都知道「哈希表」底层是通过数组实现的。

正常情况下,计算元素在哈希桶的位置,然后放入哈希桶,复杂度为 O(1),假定是通过简单的“拉链法”搭配「头插法」方式来解决哈希冲突。

但当某次元素插入后,「哈希表」达到扩容阈值,则需要对底层所使用的数组进行扩容,这个复杂度是 O(n)

显然「扩容」操作不会发生在每一次的元素插入中,因此扩容的 O(n) 都会伴随着 n 次的 O(1),也就是 O(n) 的复杂度会被均摊到每一次插入当中,因此哈希表插入仍然是 O(1) 的。

我们的 倒腾 不是发生在每一次 pop()push() 中,而是集中发生在一次 输出栈为空 的时候,因此 pop()peek() 都是均摊复杂度为 O(1) 的操作。

总结

在工业级别代码开发中,最忌讳的就是 实现一个类似的函数,直接把代码粘过来改一改就完事了。

这样的项目代码会越来越乱,一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)

工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方便了同事们。

同事们就会逐渐认可你的工作态度和工作能力,自己的口碑都是这么一点一点积累起来的!在同事圈里口碑起来了之后,你就发现自己走上了一个正循环,以后的升职加薪才少不了你!