代码随想录算法训练营第八天 | 232.用栈实现队列、225.用队列实现栈

代码随想录算法训练营第八天 | 232.用栈实现队列、225.用队列实现栈

文章目录

  • 代码随想录算法训练营第八天 | 232.用栈实现队列、225.用队列实现栈
    • 1 栈的理论基础
      • 1.1 栈的基本概念
      • 1.2 栈的存储结构
      • 1.3 在Python中的栈实现
      • 1.4 栈的应用
    • 2 队列的理论基础
      • 2.1 队列的基本概念
      • 2.2 队列的存储结构
      • 2.3 在Python中的队列实现
      • 2.4 队列的应用
    • 3 LeetCode 232.用栈实现队列
    • 4 LeetCode 225.用队列实现栈
      • 4.1 使用两个队列实现栈
      • 4.2 使用一个队列实现栈

1 栈的理论基础

1.1 栈的基本概念

(1)定义

栈(Stack)是一种重要的数据结构,具有先进后出(First In Last Out, FILO)的特点,在栈结构中,最后加入的元素将会首先被移除,这种结构类似于现实生活中的一摞盘子,最后放上去的盘子会首先被取走。

代码随想录算法训练营第八天 | 232.用栈实现队列、225.用队列实现栈_第1张图片

(2)基本操作

  • 入栈(Push):将一个元素放入栈顶。
  • 出栈(Pop):移除并返回栈顶元素。
  • 查看栈顶元素(Peek):返回栈顶元素而不移除它。
  • 判断栈空:检查栈是否为空。

(3)栈的数学性质

n个不同元素进栈,出栈元素不同排列的个数为$\C_n = \frac{1}{n+1}\binom{2n}{n} = \frac{(2n)!}{(n+1)!n!} $上述公式被称为卡特兰数(Catalan number),这个知识点408也曾经考察过。

1.2 栈的存储结构

(1)顺序存储结构

这种栈使用数组来存储元素,它有一个指针指向栈顶,每次入栈或出栈操作都会更新这个指针,顺序栈的优点是简单易于实现,缺点是大小固定,可能会有空间浪费或空间不足的情况。

(2)链式存储结构

这种栈通过链表来实现,每个节点包含数据和指向下一个节点的指针,栈顶元素是链表的头节点,链式栈的优点是可以动态地扩展大小,但其操作相对于数组要复杂一些。

更多的介绍可以参考王道数据结构的第三章内容。

1.3 在Python中的栈实现

由于Python中没有专门的栈数据结构,但可以使用列表(List)来模拟栈,列表自带的方法可以很方便地实现栈的基本操作:

  • 入栈:使用append()方法。
  • 出栈:使用pop()方法,它默认移除并返回最后一个元素,即栈顶元素。
  • 查看栈顶元素:通过索引访问列表的最后一个元素,例如list[-1]
  • 判断栈空:检查列表长度,使用len(list),或直接判断list是否为空。
class Stack:
    def __init__(self):
        self.items = []  # 使用列表存储栈的元素

    def is_empty(self):
        """检查栈是否为空"""
        return len(self.items) == 0

    def push(self, item):
        """入栈操作"""
        self.items.append(item)

    def pop(self):
        """出栈操作,返回并移除栈顶元素"""
        if not self.is_empty():
            return self.items.pop()
        return None  # 如果栈为空,则返回None

    def peek(self):
        """查看栈顶元素"""
        if not self.is_empty():
            return self.items[-1]
        return None

    def size(self):
        """返回栈的大小"""
        return len(self.items)
    
stack = Stack()
stack.push('apple')
stack.push('banana')
print(stack.peek())  # 输出 'banana'
print(stack.pop())   # 输出 'banana'
print(stack.size())  # 输出 1

1.4 栈的应用

栈在计算机科学中应用广泛,例如:

  • 函数调用栈:在程序中调用函数时使用栈来管理。
  • 表达式求值:如编译器中的算术和逻辑表达式求值。
  • 撤销机制:如文本编辑器中的撤销操作。

2 队列的理论基础

2.1 队列的基本概念

(1)定义

队列(Queue)也是计算机科学中的一种基本数据结构,它也遵循先进先出(First In First Out, FIFO)的原则。在队列中,元素被添加到一端(通常称为"尾部")并从另一端(“头部”)移除。这类似于现实生活中人们排队等候的情形,先来的人将先得到服务。

代码随想录算法训练营第八天 | 232.用栈实现队列、225.用队列实现栈_第2张图片

(2)基本操作

  • 入队(Enqueue):将一个元素添加到队列尾部。
  • 出队(Dequeue):移除并返回队列头部的元素。
  • 查看队首元素(Front/Peek):返回队列头部的元素,但不移除它。
  • 检查队列是否为空(IsEmpty):判断队列是否没有元素。
  • 获取队列大小(Size):返回队列中元素的数量。

2.2 队列的存储结构

(1)顺序存储结构

使用数组实现,有一个指针指向队头,另一个指针指向队尾,当进行入队或出队操作时,相关指针会相应地移动,这种实现方式可能涉及到元素的搬移以避免空间的浪费(即队列的"环形"实现)。

(2)链式存储结构

使用链表实现,链表的头部作为队头,尾部作为队尾,入队操作在链表尾部进行,出队操作在头部进行,链式队列不需要搬移元素,因此更适合实现动态队列。

更多的介绍可以参考王道数据结构的第三章内容。

2.3 在Python中的队列实现

在Python中,可以使用列表(List)来模拟队列,但由于列表的头部操作(如插入和删除)是时间消耗较大的,因此在需要高效队列操作时,建议使用collections.deque,它是一个双端队列,提供了在两端快速添加和删除元素的能力。

from collections import deque

class Queue:
    def __init__(self):
        self.items = deque()  # 使用deque来存储队列元素

    def is_empty(self):
        """检查队列是否为空"""
        return len(self.items) == 0

    def enqueue(self, item):
        """入队操作"""
        self.items.append(item)

    def dequeue(self):
        """出队操作,返回并移除队首元素"""
        if not self.is_empty():
            return self.items.popleft()
        return None

    def front(self):
        """查看队首元素"""
        if not self.is_empty():
            return self.items[0]
        return None

    def size(self):
        """返回队列的大小"""
        return len(self.items)

queue = Queue()
queue.enqueue('apple')
queue.enqueue('banana')
print(queue.front())  # 输出 'apple'
print(queue.dequeue()) # 输出 'apple'
print(queue.size())    # 输出 1

2.4 队列的应用

队列在计算机科学中的应用同样非常广泛,包括但不限于:

  • 数据缓冲(Buffering):如打印机任务队列。
  • 任务调度:操作系统中的任务调度。
  • 广度优先搜索(BFS)算法:在图和树的遍历中使用。

3 LeetCode 232.用栈实现队列

题目链接:https://leetcode.cn/problems/implement-queue-using-stacks/description/

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(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(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 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) ,即使其中一个操作可能花费较长时间。

这道题目是一道模拟题,也就是考察我们对栈和队列的理解,如何使用栈来模拟队列?很容易就能想到,如果我们只用一个栈是肯定不够的,所以我们需要使用两个栈来实现,因为队列是一端输入,一端输出(这里我们默认不是双端队列),因此我们就需要一个输入栈和一个输出栈,输入栈接收输入的元素,此时出栈顺序与出队顺序相反,然后我们再把出栈的元素依次再次压入输出栈中(注意:是全部元素都要压栈,如果不是则会打乱输出顺序,可以举例模拟一下就清楚了),然后再出栈,这样输出的顺序就和队列的输出顺序一致了。

(1)Python版本代码

class MyQueue:

    def __init__(self):
        self.stack1 = []        # 输入栈
        self.stack2 = []        # 输出栈
        self.front = None       # 队首元素


    def push(self, x: int) -> None:
        if not self.stack1:    # 若输入栈为空,则x为队首元素
            self.front = x
        self.stack1.append(x)           


    def pop(self) -> int:
        if not self.stack2:                     
            while self.stack1:        # 若输出栈为空,则将输入栈元素全部转移到输出栈
                self.stack2.append(self.stack1.pop())
        return self.stack2.pop()


    def peek(self) -> int:  
        if not self.stack2:
            return self.front
        return self.stack2[-1]       # 输出栈的栈顶元素即为队首元素


    def empty(self) -> bool:
        return not (self.stack1 or self.stack2)     # 若输入栈和输出栈都为空,则队列为空



# Your MyQueue object will be instantiated and called as such:
# obj = MyQueue()
# obj.push(x)
# param_2 = obj.pop()
# param_3 = obj.peek()
# param_4 = obj.empty()

(2)C++版本代码

class MyQueue {
public:
    stack stIn;
    stack stOut;
    MyQueue() {

    }
    void push(int x) {
        stIn.push(x);
    }

    int pop() {
        // 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据)
        if (stOut.empty()) {
            // 从stIn导入数据直到stIn为空
            while(!stIn.empty()) {
                stOut.push(stIn.top());
                stIn.pop();
            }
        }
        int result = stOut.top();
        stOut.pop();
        return result;
    }

    int peek() {
        int res = this->pop(); // 直接使用已有的pop函数
        stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去
        return res;
    }

    bool empty() {
        return stIn.empty() && stOut.empty();
    }
};
  • 时间复杂度: push和empty为O(1), pop和peek为O(n)
  • 空间复杂度: O(n)

4 LeetCode 225.用队列实现栈

题目链接:https://leetcode.cn/problems/implement-stack-using-queues/description/

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

注意:

  • 你只能使用队列的基本操作 —— 也就是 push to backpeek/pop from frontsizeis empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

提示:

  • 1 <= x <= 9
  • 最多调用100pushpoptopempty
  • 每次调用 poptop 都保证栈不为空

**进阶:**你能否仅用一个队列来实现栈。

如果沿用上一题的思路,我们仍然也可以使用两个队列来模拟栈的操作。

4.1 使用两个队列实现栈

使用两个队列实现栈的方法涉及将新元素始终入队到主队列(queue1),在执行出栈操作时,除了最后一个元素,将主队列中的所有元素转移到辅助队列(queue2)中,然后出队并返回主队列中的最后一个元素,即栈顶元素,之后将主队列和辅助队列的角色互换,使得辅助队列成为新的主队列,而原主队列则变为空。在查看栈顶元素时,执行与出栈相同的操作,但在返回栈顶元素之前,将其重新放回队列中。栈是否为空的判断则通过检查主队列是否为空来实现。

(1)Python版本代码

from collections import deque

class MyStack:
    def __init__(self):
        self.queue1 = deque()  # 主队列,用于模拟栈
        self.queue2 = deque()  # 辅助队列,用于在出栈操作中临时存储元素

    def push(self, x: int):
        self.queue1.append(x)  # 入栈操作,直接在队列尾部添加元素

    def pop(self):
        # 出栈操作
        while len(self.queue1) > 1:
            self.queue2.append(self.queue1.popleft())  # 将queue1的元素(除了最后一个)移到queue2
        res = self.queue1.popleft()  # 获取队列中的最后一个元素,即栈顶元素
        self.queue1, self.queue2 = self.queue2, self.queue1  # 交换两个队列的角色
        return res  # 返回栈顶元素

    def top(self):
        # 获取栈顶元素
        res = self.pop()  # 先用pop方法取出栈顶元素
        self.queue1.append(res)  # 然后将其重新入队到queue1
        return res  # 返回栈顶元素

    def empty(self):
        # 检查栈是否为空
        return not self.queue1  # 如果queue1为空,则栈为空

(2)C++版本代码

#include 
using namespace std;

class MyStack {
public:
    MyStack() {
        // 构造函数,初始化时不需要做特别操作
    }

    void push(int x) {
        queue1.push(x);  // 入栈操作,直接在队列尾部添加元素
    }

    int pop() {
        // 出栈操作
        while (queue1.size() > 1) {
            queue2.push(queue1.front());  // 将queue1的元素(除了最后一个)移到queue2
            queue1.pop();
        }
        int res = queue1.front();  // 获取队列中的最后一个元素,即栈顶元素
        queue1.pop();  // 移除该元素
        swap(queue1, queue2);  // 交换两个队列的角色
        return res;  // 返回栈顶元素
    }

    int top() {
        // 获取栈顶元素
        int res = this->pop();  // 先用pop方法取出栈顶元素
        queue1.push(res);  // 然后将其重新入队到queue1
        return res;  // 返回栈顶元素
    }

    bool empty() {
        // 检查栈是否为空
        return queue1.empty();  // 如果queue1为空,则栈为空
    }

private:
    queue queue1;  // 主队列,用于模拟栈
    queue queue2;  // 辅助队列,用于在出栈操作中临时存储元素
};

4.2 使用一个队列实现栈

在使用单个队列实现栈的方法中,首先创建一个队列 queue 来存储栈元素,当一个新元素需要入栈时,它首先被添加到队列的末尾。接着队列中之前的所有元素(即在新元素之前入队的元素)被依次出队并立即重新入队到队列的末尾,这样新入队的元素就被移动到了队列的前面,这个过程模拟了栈的后进先出(LIFO)行为,出栈操作简单地出队队列的第一个元素,即为最后入栈的元素。查看栈顶元素时,只需返回队列的头部元素。判断栈是否为空的操作则通过检查队列是否为空来完成。

(1)Python版本代码

from collections import deque

class MyStack:
    def __init__(self):
        self.queue = deque()  # 使用deque创建一个队列,用来存储栈内的元素

    def push(self, x: int):
        n = len(self.queue)  # 记录当前队列中元素的数量
        self.queue.append(x)  # 将新元素入队到队列的末尾
        for _ in range(n):
            self.queue.append(self.queue.popleft())  # 将队列前面的元素依次出队并重新入队到末尾,以确保最新入队的元素位于队列的前面

    def pop(self):
        return self.queue.popleft()  # 出栈操作,直接出队队列的第一个元素,即最后入栈的元素

    def top(self):
        return self.queue[0]  # 返回队列的第一个元素,即栈顶元素

    def empty(self):
        return not self.queue  # 检查队列是否为空,即栈是否为空

(2)C++版本代码

#include 
using namespace std;

class MyStack {
public:
    MyStack() {
        // 构造函数,初始化时不需要特别操作
    }

    void push(int x) {
        int n = queue.size();  // 获取当前队列中的元素数量
        queue.push(x);  // 将新元素入队到队列的末尾
        for (int i = 0; i < n; ++i) {
            queue.push(queue.front());  // 将队列前面的元素依次出队并重新入队到末尾
            queue.pop();  // 完成元素的重新排列
        }
    }

    int pop() {
        int topElement = queue.front();  // 获取队列的第一个元素,即栈顶元素
        queue.pop();  // 出队操作
        return topElement;  // 返回栈顶元素
    }

    int top() {
        return queue.front();  // 返回队列的第一个元素,即栈顶元素
    }

    bool empty() {
        return queue.empty();  // 检查队列是否为空
    }

private:
    queue queue;  // 使用单个队列来存储栈内的元素
};

你可能感兴趣的:(代码随想录算法训练营,算法,python,数据结构)