Article / 文章
LeetCode第232题:用栈实现队列
LeetCode第232题:用栈实现队列
问题描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 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 次
push、pop、peek和empty - 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop或者peek操作)
进阶
- 你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。
解题思路
栈是一种后进先出(LIFO)的数据结构,而队列是一种先进先出(FIFO)的数据结构。要用栈来实现队列的行为,我们需要使用两个栈来反转元素的顺序。
我们可以把这两个栈命名为:
- 输入栈(inStack):用于处理入队(push)操作。
- 输出栈(outStack):用于处理出队(pop)和查看队首元素(peek)操作。
基本思路:
- 对于 push 操作,我们直接把元素压入输入栈。
- 对于 pop 和 peek 操作,我们需要确保输出栈不为空:
- 如果输出栈为空,我们将输入栈中的所有元素依次弹出并压入输出栈,这样原本在输入栈底的元素(最早进入队列的元素)就会在输出栈的顶部。
- 然后从输出栈顶弹出或查看元素。
- 对于 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由于其动态类型和解释执行的特性,性能通常较差,但代码更为简洁易读。
代码特点
- 方法简单直观,易于理解和实现
- 使用了两个栈来反转元素顺序,从而实现先进先出的队列特性
- pop()和peek()方法中有相似的逻辑,可以考虑将共同部分抽取为辅助方法
- 时间复杂度达到了进阶要求的均摊O(1)
优化方向
如果需要进一步优化代码,可以考虑以下方向:
- 对于C#和C++,可以将
pop()和peek()方法中共同的代码提取为一个私有辅助方法,减少代码重复 - 如果队列操作频繁,可以考虑使用更高效的数据结构如循环缓冲区,但这超出了本题的范围
- 在某些特定场景下,可以根据操作模式预先调整栈的容量,减少内存重分配
常见错误
- 忘记检查输出栈是否为空,直接从输入栈弹出元素
- 在pop()操作后忘记返回弹出的元素
- 没有正确实现empty()方法,只检查了一个栈是否为空
- 在移动元素时搞混了栈的顺序,导致队列顺序错误
相关题目
- LeetCode 225: 用队列实现栈
- LeetCode 155: 最小栈
- LeetCode 622: 设计循环队列
- LeetCode 641: 设计循环双端队列