一、栈与队列理论基础
1、栈
栈是一种后进先出(LIFO)的数据结构。最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素的存放位置叫作栈顶(top)。
栈的主要操作有:
- 入栈(push):在栈顶添加一个元素。
- 出栈(pop):删除并返回栈顶的元素。如果栈为空,则此操作无效。
- 查看栈顶(peek):返回栈顶的元素但不删除。如果栈为空,则此操作无效。
- 检查栈是否为空(is_empty):返回一个布尔值,表示栈是否为空。
2、队列(Queue)
队列是一种先进先出(FIFO)的数据结构。第一个添加的元素总是第一个被删除的。就像排队一样,最早排队的人将是第一个被服务的。队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)。
队列的主要操作有:
- 入队(enqueue):在队列的尾部添加一个元素。
- 出队(dequeue):删除并返回队列的头部元素。如果队列为空,则此操作无效。
- 查看队列头部(peek):返回队列的头部元素但不删除。如果队列为空,则此操作无效。
- 检查队列是否为空(is_empty):返回一个布尔值,表示队列是否为空。
二、 LeetCode 232.用栈实现队列
1、双栈
思路
将一个栈当作输入栈,用于压入 push 传入的数据;另一个栈当作输出栈,用于 pop 和 peek 操作。
每次 pop 或 peek 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。
代码
class MyQueue {
private:
stack<int> inStack;
stack<int> outStack;
public:
MyQueue() {
}
// 向队列中添加一个元素,将其压入 inStack
void push(int x) {
inStack.push(x);
}
// 从队列中移除并返回一个元素
int pop() {
// 1.如果 outStack 为空,就将 inStack 中的所有元素弹出并压入 outStack,然后返回 outStack 的栈顶元素并弹出
// 2.如果 outStack 不为空,直接返回 outStack 的栈顶元素并弹出
if (outStack.empty()) {
while (!inStack.empty()) {
outStack.push(inStack.top());
inStack.pop();
}
}
int result = outStack.top();
outStack.pop();
return result;
}
// 返回队列的第一个元素
// 1.如果 outStack 为空,就调用 pop() 方法将inStack里元素压入 outStack。然后返回outStack 的栈顶元素并弹出。再将弹出的元素添加回去
// 2.如果 outStack 不为空,就调用 pop() 方法返回outStack 的栈顶元素并弹出。再将弹出的元素添加回去
int peek() {
int result = this->pop();
outStack.push(result);
return result;
}
// 当 inStack 和 outStack 都为空时返回 true
bool empty() {
return inStack.empty() && outStack.empty();
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
复杂度分析
时间复杂度: push和empty为 O(1), pop和peek为 O(n)。
空间复杂度: O(n)。
三、 LeetCode 225. 用队列实现栈
1、两个队列
代码
class MyStack {
public:
queue<int> que1;
queue<int> que2;
MyStack() {
}
// push 方法,将一个整数 x 添加到队列 que1 的尾部
void push(int x) {
que1.push(x);
}
// pop 方法,从队列 que1 的头部删除一个元素并返回它。然后将这个元素添加到队列 que2 的尾部,
// 再将 que1 的所有元素依次从头部取出并添加到 que2 的尾部,这样就可以保证 que2 的头部元素是最后一个进入 que1 的元素。
// 最后返回 que1 的头部元素。这个过程相当于实现了一个后进先出(LIFO)的栈。
int pop() {
int size = que1.size();
--size; // 由于堆栈是后进先出(LIFO),因此需要将队列 que1 的元素逆序,以便于取出第一个进入队列的元素
// 反转队列 que1 中的元素顺序,时间复杂度为 O(n)
while (size--) {
que2.push(que1.front()); // 将队列 que1 的头部元素压入尾部,相当于将整个队列向后移动一位
que1.pop(); // 从队列 que1 中删除头部元素
}
// 此时队列 que2 的头部元素即为最早进入队列 que1 的元素,将其赋给变量 result
int result = que2.front();
que2.pop(); // 从队列 que2 中删除头部元素,即最早进入队列 que1 的元素
que1 = que2; // 将 que2 的内容复制到 que1,这样 que1 就变成了最新的栈,而 que2 则被清空了。
while (!que2.empty()) {
que2.pop(); // 清空队列 que2
}
return result; // 返回最早进入队列 que1 的元素,即最早进入这个栈的元素。
}
// top 方法,返回队列 que1 的尾部元素,即最后一个进入这个栈的元素。此处直接返回其值即可。
int top() {
return que1.back();
}
// empty 方法,检查队列 que1 是否为空。如果为空则返回 true,否则返回 false。这个方法可以用来检查这个栈是否为空。
bool empty() {
return que1.empty();
}
};
复杂度分析
时间复杂度:pop为O(n),其他为O(1)
空间复杂度:O(n)
2、一个队列
代码
class MyStack {
private:
queue<int> queue;
public:
MyStack() {
}
// push 方法将一个 int 类型的数值 x 压入队列 queue 的尾部
void push(int x) {
queue.push(x);
}
int pop() {
int size = queue.size(); // 获取队列的当前大小,保存在变量 size 中
--size; // 由于堆栈是后进先出(LIFO),因此需要将队列的元素逆序,以便于取出第一个进入队列的元素
// 反转队列中的元素顺序,时间复杂度为 O(n)
while (size--) {
queue.push(queue.front()); // 将队列的头部元素压入尾部,相当于将整个队列向后移动一位
queue.pop(); // 从队列中删除头部元素
}
// 此时队列的头部元素即为最早进入队列的元素,将其赋给变量 result
int result = queue.front();
queue.pop(); // 从队列中删除头部元素,即最早进入队列的元素
return result; // 返回最早进入队列的元素
}
int top() {
return queue.back(); // 返回队列的尾部元素,即最后一个进入队列的元素,此处返回其值即可
}
bool empty() {
return queue.empty(); // 检查队列是否为空,如果为空返回 true,否则返回 false
}
};
复杂度分析
时间复杂度:pop为O(n),其他为O(1)
空间复杂度:O(n)