实现一个支持O(1)时间获取最小值的队列
字数 1823 2025-11-07 12:34:04

实现一个支持O(1)时间获取最小值的队列

题目描述
设计一个队列,该队列除了支持标准的入队(enqueue)和出队(dequeue)操作外,还需要支持在常数时间O(1)内获取队列中的最小值(getMin)。假设队列中存储的元素是可比较的(例如整数)。

解题思路分析
这个问题的难点在于,队列是先进先出(FIFO)的数据结构。当一个元素入队后,它会在队列中停留一段时间,直到所有在它之前入队的元素都出队后,它才会出队。如果我们简单地维护一个变量来记录当前最小值,那么当这个最小值元素被出队后,我们就需要知道新的最小值是谁,而这个新的最小值必须是队列中剩余元素里的最小值。由于元素是动态进出队列的,我们无法通过一次遍历(那将是O(n)时间复杂度)来找到新的最小值。

核心思路是使用两个栈来模拟一个队列,并利用“支持O(1)时间获取最小值的栈”的现有解法。

  1. 基础组件:最小栈
    我们先回顾一个已解决的问题:实现一个支持O(1)时间获取最小值的栈。我们通常使用一个辅助栈(min_stack)来解决问题。主栈进行push和pop操作,辅助栈的栈顶始终保存着当前主栈中所有元素的最小值。

    • push(x): 将x压入主栈。同时,如果辅助栈为空,或者x小于等于辅助栈栈顶,则将x也压入辅助栈。
    • pop(): 弹出主栈栈顶元素。如果弹出的元素等于辅助栈栈顶,则也弹出辅助栈栈顶。
    • getMin(): 直接返回辅助栈的栈顶元素。
  2. 用栈实现队列
    我们知道,用两个栈可以模拟一个队列。设这两个栈为stack_instack_out

    • enqueue(x): 将所有入队元素都压入stack_in
    • dequeue(): 如果stack_out为空,则将stack_in中的所有元素依次弹出并压入stack_out。然后从stack_out中弹出一个元素。如果stack_out不为空,则直接从stack_out弹出。
  3. 组合方案:最小队列
    现在,我们将这两个思路结合起来。我们不再使用两个普通的栈,而是使用两个“最小栈”来分别作为stack_instack_out。每个最小栈自身都维护着其栈内元素的最小值。

    • MinQueue的结构:

      • stack_in: 一个最小栈,专门负责处理入队操作。
      • stack_out: 另一个最小栈,专门负责处理出队操作。
    • 关键点:队列的全局最小值,要么在stack_in中,要么在stack_out中。具体来说,它是stack_in的最小值和stack_out的最小值中更小的那个。因为队列中的所有元素,要么在stack_in里,要么在stack_out里。

循序渐进讲解

步骤一:定义“最小栈”类
首先,我们需要实现之前学过的最小栈,作为我们构建最小队列的基础模块。

class MinStack:
    def __init__(self):
        self.data_stack = []   # 主数据栈
        self.min_stack = []     # 辅助最小栈,栈顶保存当前最小值

    def push(self, x: int) -> None:
        # 元素压入主栈
        self.data_stack.append(x)
        # 如果辅助栈为空,或者新元素小于等于当前辅助栈栈顶(即当前最小值),则也压入辅助栈
        if not self.min_stack or x <= self.min_stack[-1]:
            self.min_stack.append(x)

    def pop(self) -> None:
        # 如果主栈为空,则无法弹出
        if not self.data_stack:
            return
        # 弹出主栈栈顶元素
        popped_value = self.data_stack.pop()
        # 如果弹出的这个值正好等于当前辅助栈栈顶(即当前最小值),则辅助栈也需要弹出
        if popped_value == self.min_stack[-1]:
            self.min_stack.pop()

    def top(self) -> int:
        # 获取主栈栈顶元素
        if not self.data_stack:
            return -1 # 或者抛出异常
        return self.data_stack[-1]

    def get_min(self) -> int:
        # 获取当前栈内最小值,即辅助栈栈顶
        if not self.min_stack:
            return -1 # 或者抛出异常
        return self.min_stack[-1]

步骤二:利用两个“最小栈”构建“最小队列”
现在,我们使用两个MinStack对象来构建我们的MinQueue

class MinQueue:
    def __init__(self):
        self.stack_in = MinStack()  # 用于入队的栈
        self.stack_out = MinStack() # 用于出队的栈

    def enqueue(self, x: int) -> None:
        """入队操作"""
        # 所有新元素都压入 stack_in
        self.stack_in.push(x)

    def dequeue(self) -> None:
        """出队操作"""
        # 如果 stack_out 为空,需要将 stack_in 中的所有元素“倒入” stack_out
        if self.stack_out.get_min() == -1: # 或者用 len(self.stack_out.data_stack) == 0 判断是否为空
            while self.stack_in.get_min() != -1: # 当 stack_in 不为空时
                # 从 stack_in 弹出一个元素
                val = self.stack_in.top()
                self.stack_in.pop()
                # 将该元素压入 stack_out
                self.stack_out.push(val)
        # 从 stack_out 中弹出栈顶元素,即完成了出队
        self.stack_out.pop()

    def get_min(self) -> int:
        """获取队列中的最小值"""
        min_in = self.stack_in.get_min() # stack_in 中的最小值,如果栈为空,返回-1
        min_out = self.stack_out.get_min() # stack_out 中的最小值,如果栈为空,返回-1

        # 情况1: 两个栈都为空,队列为空
        if min_in == -1 and min_out == -1:
            return -1
        # 情况2: 只有 stack_in 有元素
        elif min_out == -1:
            return min_in
        # 情况3: 只有 stack_out 有元素
        elif min_in == -1:
            return min_out
        # 情况4: 两个栈都有元素,返回两者中更小的那个
        else:
            return min(min_in, min_out)

    # 通常队列还需要一个 front() 方法,获取队首元素(即下一个要出队的元素)
    def front(self) -> int:
        """获取队首元素但不出队"""
        # 原理同 dequeue,如果 stack_out 为空,需要倒入
        if self.stack_out.get_min() == -1:
            while self.stack_in.get_min() != -1:
                val = self.stack_in.top()
                self.stack_in.pop()
                self.stack_out.push(val)
        # 现在队首元素就是 stack_out 的栈顶
        return self.stack_out.top()

步骤三:复杂度分析

  • 时间复杂度:

    • enqueue(x): O(1)。直接调用stack_in.push(x)
    • dequeue(): 摊还时间复杂度为O(1)。虽然最坏情况下,当stack_out为空时,需要将stack_in中的所有元素移动到stack_out,这是一个O(n)的操作。但是,每个元素只会从stack_in被压入stack_out一次,也只会从stack_out弹出一次。因此,将一系列操作的总成本平均到每个操作上,dequeue的摊还成本是常数时间。
    • get_min(): O(1)。只是比较两个栈的最小值,是常数时间操作。
    • front(): 摊还时间复杂度为O(1),原因同dequeue
  • 空间复杂度: O(n)。需要两个栈来存储所有元素,每个栈(包括其辅助栈)的空间消耗与元素数量n成线性关系。

总结
实现一个支持O(1)时间获取最小值的队列,其核心是“用栈实现队列”和“最小栈”两种思想的结合。通过维护两个最小栈(stack_instack_out),并保证队列的全局最小值是这两个栈各自最小值中的较小者,我们成功地满足了所有操作在常数或摊还常数时间内完成的要求。这是一个非常经典且巧妙的面试题,深刻考察了对栈和队列这两种基础数据结构特性的理解与灵活运用。

实现一个支持O(1)时间获取最小值的队列 题目描述 设计一个队列,该队列除了支持标准的入队(enqueue)和出队(dequeue)操作外,还需要支持在常数时间O(1)内获取队列中的最小值(getMin)。假设队列中存储的元素是可比较的(例如整数)。 解题思路分析 这个问题的难点在于,队列是先进先出(FIFO)的数据结构。当一个元素入队后,它会在队列中停留一段时间,直到所有在它之前入队的元素都出队后,它才会出队。如果我们简单地维护一个变量来记录当前最小值,那么当这个最小值元素被出队后,我们就需要知道新的最小值是谁,而这个新的最小值必须是队列中剩余元素里的最小值。由于元素是动态进出队列的,我们无法通过一次遍历(那将是O(n)时间复杂度)来找到新的最小值。 核心思路是使用两个栈来模拟一个队列,并利用“支持O(1)时间获取最小值的栈”的现有解法。 基础组件:最小栈 我们先回顾一个已解决的问题:实现一个支持O(1)时间获取最小值的栈。我们通常使用一个辅助栈(min_ stack)来解决问题。主栈进行push和pop操作,辅助栈的栈顶始终保存着当前主栈中所有元素的最小值。 push(x) : 将x压入主栈。同时,如果辅助栈为空,或者x小于等于辅助栈栈顶,则将x也压入辅助栈。 pop() : 弹出主栈栈顶元素。如果弹出的元素等于辅助栈栈顶,则也弹出辅助栈栈顶。 getMin() : 直接返回辅助栈的栈顶元素。 用栈实现队列 我们知道,用两个栈可以模拟一个队列。设这两个栈为 stack_in 和 stack_out 。 enqueue(x) : 将所有入队元素都压入 stack_in 。 dequeue() : 如果 stack_out 为空,则将 stack_in 中的所有元素依次弹出并压入 stack_out 。然后从 stack_out 中弹出一个元素。如果 stack_out 不为空,则直接从 stack_out 弹出。 组合方案:最小队列 现在,我们将这两个思路结合起来。我们不再使用两个普通的栈,而是使用两个“最小栈”来分别作为 stack_in 和 stack_out 。每个最小栈自身都维护着其栈内元素的最小值。 MinQueue 的结构: stack_in : 一个最小栈,专门负责处理入队操作。 stack_out : 另一个最小栈,专门负责处理出队操作。 关键点:队列的全局最小值,要么在 stack_in 中,要么在 stack_out 中。具体来说,它是 stack_in 的最小值和 stack_out 的最小值中更小的那个。因为队列中的所有元素,要么在 stack_in 里,要么在 stack_out 里。 循序渐进讲解 步骤一:定义“最小栈”类 首先,我们需要实现之前学过的最小栈,作为我们构建最小队列的基础模块。 步骤二:利用两个“最小栈”构建“最小队列” 现在,我们使用两个 MinStack 对象来构建我们的 MinQueue 。 步骤三:复杂度分析 时间复杂度 : enqueue(x) : O(1)。直接调用 stack_in.push(x) 。 dequeue() : 摊还时间复杂度为O(1)。虽然最坏情况下,当 stack_out 为空时,需要将 stack_in 中的所有元素移动到 stack_out ,这是一个O(n)的操作。但是,每个元素只会从 stack_in 被压入 stack_out 一次,也只会从 stack_out 弹出一次。因此,将一系列操作的总成本平均到每个操作上, dequeue 的摊还成本是常数时间。 get_min() : O(1)。只是比较两个栈的最小值,是常数时间操作。 front() : 摊还时间复杂度为O(1),原因同 dequeue 。 空间复杂度 : O(n)。需要两个栈来存储所有元素,每个栈(包括其辅助栈)的空间消耗与元素数量n成线性关系。 总结 实现一个支持O(1)时间获取最小值的队列,其核心是“用栈实现队列”和“最小栈”两种思想的结合。通过维护两个最小栈( stack_in 和 stack_out ),并保证队列的全局最小值是这两个栈各自最小值中的较小者,我们成功地满足了所有操作在常数或摊还常数时间内完成的要求。这是一个非常经典且巧妙的面试题,深刻考察了对栈和队列这两种基础数据结构特性的理解与灵活运用。