200字
【单调栈】入门技巧
2025-12-10
2025-12-10

单调栈全面解析

一、单调栈的核心概念

1. 什么是单调栈?

  • 定义:栈中元素保持单调性(单调递增或单调递减)

  • 核心思想:利用栈的LIFO特性,维护元素的单调关系

  • 主要用途:解决"下一个更大/更小元素"、"最近更大/更小值"等问题

2. 单调栈的两种类型

# 1. 单调递增栈(栈底到栈顶递增)
#    用途:寻找下一个更小的元素
stack = []  # 栈底小,栈顶大

# 2. 单调递减栈(栈底到栈顶递减)
#    用途:寻找下一个更大的元素
stack = []  # 栈底大,栈顶小

二、单调栈通用模板

模板1:下一个更大元素(单调递减栈)

def next_greater_element(nums):
    """
    返回每个元素的下一个更大元素的索引
    时间复杂度:O(n)
    """
    n = len(nums)
    result = [-1] * n  # 初始化结果为-1
    stack = []  # 单调递减栈
    
    for i in range(n):
        # 当前元素大于栈顶元素时,栈顶元素的下一个更大元素就是当前元素
        while stack and nums[stack[-1]] < nums[i]:
            idx = stack.pop()
            result[idx] = i
        stack.append(i)
    
    return result

# 示例
nums = [2, 1, 2, 4, 3]
# 下一个更大元素的索引: [3, 2, 3, -1, -1]

模板2:下一个更小元素(单调递增栈)

def next_smaller_element(nums):
    """
    返回每个元素的下一个更小元素的索引
    """
    n = len(nums)
    result = [-1] * n
    stack = []  # 单调递增栈
    
    for i in range(n):
        # 当前元素小于栈顶元素时,栈顶元素的下一个更小元素就是当前元素
        while stack and nums[stack[-1]] > nums[i]:
            idx = stack.pop()
            result[idx] = i
        stack.append(i)
    
    return result

# 示例
nums = [2, 1, 2, 4, 3]
# 下一个更小元素的索引: [1, -1, -1, 4, -1]

模板3:前后两个方向查找(最常用)

def left_right_smaller(nums):
    """
    返回每个元素的左右第一个较小值的索引
    这是原代码中使用的模式
    """
    n = len(nums)
    left = [-1] * n
    right = [-1] * n
    
    # 找左边第一个较小值
    stack = []
    for i in range(n):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        left[i] = stack[-1] if stack else -1
        stack.append(i)
    
    # 找右边第一个较小值
    stack.clear()
    for i in range(n-1, -1, -1):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        right[i] = stack[-1] if stack else -1
        stack.append(i)
    
    return left, right

三、单调栈的使用技巧

技巧1:处理相等元素

# 1. 严格大于/小于(跳过相等)
while stack and nums[stack[-1]] < nums[i]:  # 严格小于
    stack.pop()

# 2. 大于等于/小于等于(包含相等)
while stack and nums[stack[-1]] <= nums[i]:  # 包含等于
    stack.pop()

# 根据题目要求选择:
# - 找"第一个"较小/较大值:使用严格比较
# - 找"最近"的较小/较大值:可能包含等于

技巧2:存储索引 vs 存储值

python

# 推荐存储索引:可以获得更多信息
stack.append(i)  # 存储索引
# 优点:
# 1. 可以通过索引访问原数组的值
# 2. 可以计算距离、跨度等信息
# 3. 便于处理结果数组

技巧3:循环数组的处理

def next_greater_circular(nums):
    """
    循环数组中的下一个更大元素
    """
    n = len(nums)
    result = [-1] * n
    stack = []
    
    # 遍历两遍数组模拟循环
    for i in range(2 * n):
        idx = i % n
        while stack and nums[stack[-1]] < nums[idx]:
            prev_idx = stack.pop()
            result[prev_idx] = nums[idx]
        
        # 只在第一遍遍历时入栈
        if i < n:
            stack.append(idx)
    
    return result

# 示例
nums = [1, 2, 1]
# 输出: [2, -1, 2]

LeetCode单调栈经典题目详解

一、基础入门题(必刷)

1. 739. 每日温度 ⭐⭐

题目:给定每天的温度,返回需要等待多少天才能有更高的温度

def dailyTemperatures(temperatures):
    n = len(temperatures)
    answer = [0] * n
    stack = []  # 单调递减栈(存索引)
    
    for i in range(n):
        while stack and temperatures[stack[-1]] < temperatures[i]:
            prev_idx = stack.pop()
            answer[prev_idx] = i - prev_idx
        stack.append(i)
    
    return answer

# 示例
# 输入: [73,74,75,71,69,72,76,73]
# 输出: [1,1,4,2,1,1,0,0]

关键点

  • 单调递减栈(找更高温度)

  • 结果 = 当前索引 - 栈顶索引

2. 496. 下一个更大元素 I ⭐

题目nums1nums2 的子集,找到 nums1 中每个元素在 nums2 中的下一个更大元素

def nextGreaterElement(nums1, nums2):
    next_greater = {}
    stack = []  # 单调递减栈
    
    # 为nums2所有元素找到下一个更大元素
    for num in nums2:
        while stack and stack[-1] < num:
            next_greater[stack.pop()] = num
        stack.append(num)
    
    # 栈中剩余元素没有下一个更大元素
    for num in stack:
        next_greater[num] = -1
    
    return [next_greater[num] for num in nums1]

# 示例
# nums1 = [4,1,2], nums2 = [1,3,4,2]
# 输出: [-1,3,-1]

变种

  • 503. 下一个更大元素 II(循环数组)

  • 556. 下一个更大元素 III

二、进阶应用题

3. 84. 柱状图中最大的矩形 ⭐⭐⭐⭐

题目:给定柱状图的高度,求能勾勒出的最大矩形的面积

def largestRectangleArea(heights):
    heights.append(0)  # 哨兵,确保所有柱子都会被处理
    stack = []  # 单调递增栈
    max_area = 0
    
    for i in range(len(heights)):
        # 当前柱子小于栈顶柱子高度
        while stack and heights[stack[-1]] > heights[i]:
            h = heights[stack.pop()]
            # 宽度计算:如果栈为空,说明左边所有柱子都更高
            w = i if not stack else i - stack[-1] - 1
            max_area = max(max_area, h * w)
        stack.append(i)
    
    heights.pop()  # 恢复原数组
    return max_area

# 示例
# 输入: [2,1,5,6,2,3]
# 输出: 10 (高度5,6对应的柱子)

算法思路

  1. 维护单调递增栈

  2. 遇到递减柱子时,计算以栈顶柱子为高的最大矩形

  3. 宽度 = 当前索引 - 栈顶下一个索引 - 1

可视化理解

高度: [2, 1, 5, 6, 2, 3]
索引:  0  1  2  3  4  5

以高度5为例:
左边第一个小于5的是索引1(高度1)
右边第一个小于5的是索引4(高度2)
宽度 = 4 - 1 - 1 = 2
面积 = 5 * 2 = 10

4. 42. 接雨水 ⭐⭐⭐⭐

题目:给定表示高度的非负整数数组,计算能接多少雨水

def trap(height):
    stack = []  # 单调递减栈,存储索引
    water = 0
    
    for i in range(len(height)):
        # 当前高度大于栈顶高度,形成凹槽
        while stack and height[stack[-1]] < height[i]:
            bottom = stack.pop()  # 凹槽底部
            
            if not stack:  # 没有左边界
                break
            
            left = stack[-1]  # 左边界
            h = min(height[left], height[i]) - height[bottom]
            w = i - left - 1
            water += h * w
        
        stack.append(i)
    
    return water

# 示例
# 输入: [0,1,0,2,1,0,1,3,2,1,2,1]
# 输出: 6

两种解法对比

方法

时间复杂度

空间复杂度

核心思想

单调栈

O(n)

O(n)

横向计算雨水

双指针

O(n)

O(1)

竖向计算雨水

三、变形与组合题

5. 85. 最大矩形 ⭐⭐⭐⭐

题目:给定仅包含 0 和 1 的二维矩阵,找出只包含 1 的最大矩形

def maximalRectangle(matrix):
    if not matrix:
        return 0
    
    m, n = len(matrix), len(matrix[0])
    heights = [0] * n
    max_area = 0
    
    for i in range(m):
        # 更新高度数组
        for j in range(n):
            if matrix[i][j] == '1':
                heights[j] += 1
            else:
                heights[j] = 0
        
        # 使用84题的单调栈解法
        max_area = max(max_area, largestRectangleArea(heights))
    
    return max_area

# 需要84题的largestRectangleArea函数

转换思路

  • 将二维问题转化为一维柱状图问题

  • 每一行作为柱状图的底部

  • 高度 = 连续1的个数

6. 901. 股票价格跨度 ⭐⭐

题目:设计一个算法,计算股票当前价格的价格跨度(当前价格小于或等于今天价格的最大连续日数)

class StockSpanner:
    def __init__(self):
        self.stack = []  # 单调递减栈,存储(价格, 跨度)
    
    def next(self, price: int) -> int:
        span = 1
        # 合并小于等于当前价格的跨度
        while self.stack and self.stack[-1][0] <= price:
            _, prev_span = self.stack.pop()
            span += prev_span
        self.stack.append((price, span))
        return span

# 示例
# 输入: [100, 80, 60, 70, 60, 75, 85]
# 输出: [1, 1, 1, 2, 1, 4, 6]

创新点

  • 栈中存储 (价格, 跨度)

  • 利用单调栈合并跨度

7. 402. 移掉K位数字 ⭐⭐⭐

题目:给定一个数字字符串,移除k位数字,使剩下的数字最小

def removeKdigits(num, k):
    stack = []  # 单调递增栈
    
    for digit in num:
        # 维护单调递增栈,但可以删除k次
        while k > 0 and stack and stack[-1] > digit:
            stack.pop()
            k -= 1
        stack.append(digit)
    
    # 如果还有k可以删除,从末尾删除
    stack = stack[:-k] if k else stack
    
    # 去除前导零
    result = ''.join(stack).lstrip('0')
    return result if result else '0'

# 示例
# 输入: num = "1432219", k = 3
# 输出: "1219"

贪心策略

  • 尽可能让高位数字小

  • 维护单调递增栈,遇到递减就删除

四、困难级别挑战题

8. 316. 去除重复字母 ⭐⭐⭐

题目:去除字符串中重复字母,使每个字母只出现一次,且返回结果的字典序最小

def removeDuplicateLetters(s):
    last_occurrence = {c: i for i, c in enumerate(s)}
    stack = []  # 单调递增栈
    seen = set()
    
    for i, c in enumerate(s):
        if c in seen:
            continue
        
        # 如果栈顶字符大于当前字符,且后面还会出现
        while stack and c < stack[-1] and i < last_occurrence[stack[-1]]:
            seen.remove(stack.pop())
        
        stack.append(c)
        seen.add(c)
    
    return ''.join(stack)

# 示例
# 输入: "bcabc"
# 输出: "abc"

关键技巧

  • 记录每个字符最后出现的位置

  • 维护单调递增栈

  • 使用集合记录栈中已有字符

9. 768. 最多能完成排序的块 II ⭐⭐⭐

题目:将数组分成最多的块,对每个块单独排序后,连接起来的结果与原数组排序后的结果相同

def maxChunksToSorted(arr):
    stack = []  # 每个块的最大值
    
    for num in arr:
        if not stack or num >= stack[-1]:
            stack.append(num)
        else:
            # 合并块
            max_val = stack.pop()
            while stack and num < stack[-1]:
                stack.pop()
            stack.append(max_val)
    
    return len(stack)

# 示例
# 输入: arr = [2,1,3,4,4]
# 输出: 4

思路

  • 栈中存储每个块的最大值

  • 遇到更小的数字时,需要合并前面的块

五、解题技巧总结

总结表格

问题

视角

找什么

栈类型

示例

每日温度

今天看未来

更高温度

递减栈

找更大

波峰浪谷

山峰看山谷

更低值

递增栈

找更小

接雨水

凹槽看边界

更高边界

递减栈

找更大

最大矩形

柱子看边界

更矮柱子

递增栈

找更小

关键区分

  • 如果当前元素是参考点(如波峰浪谷的j),要找比它的 → 递增栈

  • 如果当前元素是被比较点(如每日温度的今天),要找比它的 → 递减栈

模板选择

# 通用模板
def monotonic_stack_template(arr, mode='greater'):
    stack = []
    result = [-1] * len(arr)
    
    for i, val in enumerate(arr):
        if mode == 'greater':
            # 找下一个更大元素
            while stack and arr[stack[-1]] < val:
                idx = stack.pop()
                result[idx] = i  # 或 val
        else:
            # 找下一个更小元素
            while stack and arr[stack[-1]] > val:
                idx = stack.pop()
                result[idx] = i  # 或 val
        stack.append(i)
    
    return result

特殊处理技巧

技巧1:添加哨兵元素

# 在84题中,添加高度0确保所有柱子被处理
heights.append(0)

技巧2:存储额外信息

# 在901题中,存储(价格, 跨度)对
stack.append((price, span))

技巧3:处理循环数组

# 在503题中,遍历2n次
for i in range(2 * n):
    idx = i % n
    # ... 处理逻辑

六、练习建议

刷题顺序建议:

  1. 基础:496 → 739 → 901

  2. 进阶:84 → 42

  3. 变形:85 → 402 → 316

  4. 综合:768 → 1081(与316类似)

重点掌握:

  1. 84题(柱状图最大矩形)- 面试高频

  2. 42题(接雨水)- 多种解法对比

  3. 739题(每日温度)- 经典入门

【单调栈】入门技巧
作者
Shisuiyi
发表于
2025-12-10
License
CC BY-NC-SA 4.0

评论