Article / 文章

LeetCode第232题:用栈实现队列

LeetCode第232题:用栈实现队列

问题描述

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

实现 MyQueue 类:

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

说明:

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

难度:简单

示例

输入:
["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
  • 最多调用 100 次 pushpoppeekempty
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

进阶

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

解题思路

栈是一种后进先出(LIFO)的数据结构,而队列是一种先进先出(FIFO)的数据结构。要用栈来实现队列的行为,我们需要使用两个栈来反转元素的顺序。

我们可以把这两个栈命名为:

  1. 输入栈(inStack):用于处理入队(push)操作。
  2. 输出栈(outStack):用于处理出队(pop)和查看队首元素(peek)操作。

基本思路:

  1. 对于 push 操作,我们直接把元素压入输入栈。
  2. 对于 poppeek 操作,我们需要确保输出栈不为空:
    • 如果输出栈为空,我们将输入栈中的所有元素依次弹出并压入输出栈,这样原本在输入栈底的元素(最早进入队列的元素)就会在输出栈的顶部。
    • 然后从输出栈顶弹出或查看元素。
  3. 对于 empty 操作,当且仅当两个栈都为空时,队列才为空。

这种方法保证了每个元素只会从输入栈到输出栈移动一次,所以平均时间复杂度是O(1),虽然最坏情况下单次操作的时间复杂度是O(n)。

代码实现

C#实现

public class MyQueue {
    private Stack<int> inStack;
    private Stack<int> outStack;

    public MyQueue() {
        inStack = new Stack<int>();
        outStack = new Stack<int>();
    }
    
    public void Push(int x) {
        inStack.Push(x);
    }
    
    public int Pop() {
        // 确保outStack不为空
        if (outStack.Count == 0) {
            // 将inStack中所有元素移到outStack
            while (inStack.Count > 0) {
                outStack.Push(inStack.Pop());
            }
        }
        return outStack.Pop();
    }
    
    public int Peek() {
        // 确保outStack不为空
        if (outStack.Count == 0) {
            // 将inStack中所有元素移到outStack
            while (inStack.Count > 0) {
                outStack.Push(inStack.Pop());
            }
        }
        return outStack.Peek();
    }
    
    public bool Empty() {
        return inStack.Count == 0 && outStack.Count == 0;
    }
}

Python实现

class MyQueue:
    def __init__(self):
        self.in_stack = []
        self.out_stack = []

    def push(self, x: int) -> None:
        self.in_stack.append(x)

    def pop(self) -> int:
        # 确保out_stack不为空
        if not self.out_stack:
            # 将in_stack中所有元素移到out_stack
            while self.in_stack:
                self.out_stack.append(self.in_stack.pop())
        return self.out_stack.pop()

    def peek(self) -> int:
        # 确保out_stack不为空
        if not self.out_stack:
            # 将in_stack中所有元素移到out_stack
            while self.in_stack:
                self.out_stack.append(self.in_stack.pop())
        return self.out_stack[-1]

    def empty(self) -> bool:
        return not self.in_stack and not self.out_stack

C++实现

class MyQueue {
private:
    stack<int> inStack;
    stack<int> outStack;

public:
    MyQueue() {
        
    }
    
    void push(int x) {
        inStack.push(x);
    }
    
    int pop() {
        // 确保outStack不为空
        if (outStack.empty()) {
            // 将inStack中所有元素移到outStack
            while (!inStack.empty()) {
                outStack.push(inStack.top());
                inStack.pop();
            }
        }
        int result = outStack.top();
        outStack.pop();
        return result;
    }
    
    int peek() {
        // 确保outStack不为空
        if (outStack.empty()) {
            // 将inStack中所有元素移到outStack
            while (!inStack.empty()) {
                outStack.push(inStack.top());
                inStack.pop();
            }
        }
        return outStack.top();
    }
    
    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};

性能分析

时间复杂度

  • push(): O(1),直接将元素添加到输入栈的顶部。
  • pop(): 均摊O(1)。最坏情况下需要将所有元素从输入栈移到输出栈,这是O(n)的操作,但每个元素只会被移动一次,所以n个元素的n次操作均摊下来是O(1)。
  • peek(): 与pop()相同,均摊O(1)。
  • empty(): O(1),只需检查两个栈是否都为空。

空间复杂度

  • O(n),其中n是队列中的元素数量。所有元素都存储在两个栈中。

不同语言实现的性能比较

语言 执行时间 内存消耗
C++ 较快 较少
C# 中等 中等
Python 较慢 较多

C++实现通常最高效,因为它的栈操作是原生支持的,且内存管理更为直接。C#的性能次之,Python由于其动态类型和解释执行的特性,性能通常较差,但代码更为简洁易读。

代码特点

  1. 方法简单直观,易于理解和实现
  2. 使用了两个栈来反转元素顺序,从而实现先进先出的队列特性
  3. pop()和peek()方法中有相似的逻辑,可以考虑将共同部分抽取为辅助方法
  4. 时间复杂度达到了进阶要求的均摊O(1)

优化方向

如果需要进一步优化代码,可以考虑以下方向:

  1. 对于C#和C++,可以将pop()peek()方法中共同的代码提取为一个私有辅助方法,减少代码重复
  2. 如果队列操作频繁,可以考虑使用更高效的数据结构如循环缓冲区,但这超出了本题的范围
  3. 在某些特定场景下,可以根据操作模式预先调整栈的容量,减少内存重分配

常见错误

  1. 忘记检查输出栈是否为空,直接从输入栈弹出元素
  2. 在pop()操作后忘记返回弹出的元素
  3. 没有正确实现empty()方法,只检查了一个栈是否为空
  4. 在移动元素时搞混了栈的顺序,导致队列顺序错误

相关题目

  • LeetCode 225: 用队列实现栈
  • LeetCode 155: 最小栈
  • LeetCode 622: 设计循环队列
  • LeetCode 641: 设计循环双端队列