题目描述

✅ 456. 132 模式

image-20260928214635936

image-20260928214635937

题意分析

寻找 i < j < k 且 nums[i] < nums[k] < nums[j] 的三个元素。它们不要求连续,“1、3、2”表示三个值从小到大的相对排名,并不是数值本身。

如果右侧已经有一对下标递增、数值递减的“3、2”,只需在它们左侧找到比“2”更小的“1”。因此从右向左扫描,先维护右侧已经配好的“3、2”,再判断当前值能否补全模式。

解法:从右向左维护单调栈

核心思路

[!blue]

middle 保存已经确认的“2”候选中的最大值:它在右侧出现过,并且在它左边、当前扫描位置右边,已经出现过一个更大的“3”。若当前 nums[i] < middle,这三个数既满足数值关系,也天然满足 i < j < k,可以立即返回 true。在已经配好“3”的候选中,“2”越大,越容易在左侧找到比它小的值,所以只保留最大者即可。

另用栈保存右侧尚未被弹出的值,栈从底到顶非递增。当前值 x 大于栈顶时,将栈顶弹出:x 位于弹出值的左边且更大,因此 x 可以当“3”,弹出值可以当“2”。持续弹出所有小于 x 的栈顶,弹出值从小到大排列,最后一个就是本轮最优的“2”;随后把 x 入栈,恢复非递增顺序。

代码直接用弹出值覆盖 middle,不会丢失更大的旧候选。原因是:尚未返回时,栈内所有值都不小于已有的 middle;当前值也已通过 x < middle 的检查。新弹出的值因而不会小于旧 middle,剩余栈值以及新入栈的 x 又都不小于新的 middle,这个性质会一直保持。

也不会漏掉合法模式。对任意合法的 i < j < k,处理 j 时,若 nums[k] 仍在栈内,它及其上方更小或相等的元素都会被 nums[j] 弹出,使 middle 至少达到 nums[k];若它已经出栈,则此前已经确认过不小于它的候选。middle 之后不会变小,所以扫描到 i 时必然能发现模式,或早已发现另一个模式。

解题步骤

  1. 初始化空栈和尚未设置的 middle,从数组末尾向左扫描。
  2. 若 middle 已存在且 nums[i] < middle,当前元素可作为“1”,返回 true。
  3. 当 nums[i] > stack.top 时持续弹栈,把弹出值记为新的 middle;当前元素与它构成“3、2”。
  4. 将 nums[i] 入栈,继续向左。
  5. 扫描结束仍未命中则返回 false。

Java 用 Integer.MIN_VALUE 表示尚无候选,它小于题目允许的所有元素;Go 用 hasMiddle 区分候选是否有效,避免默认值 0 干扰负数。少于三个元素时不可能完成两次角色确认,循环自然返回 false。

代码实现

class Solution {
    public boolean find132pattern(int[] nums) {
        Deque<Integer> stack = new ArrayDeque<>();
        int middle = Integer.MIN_VALUE;

        for (int i = nums.length - 1; i >= 0; i--) {
            // 此前已存在右侧的大、中组合,当前更小值可补成一三二。
            if (nums[i] < middle) {
                return true;
            }

            while (!stack.isEmpty() && nums[i] > stack.peek()) {
                // 当前值作为三,弹出值作为二,继续弹出以获得更有利的二。
                middle = stack.pop();
            }

            stack.push(nums[i]);
        }

        return false;
    }
}
func find132pattern(nums []int) bool {
    stack := make([]int, 0)
    middle := 0
    hasMiddle := false

    for i := len(nums) - 1; i >= 0; i-- {
        // 此前已存在右侧的大、中组合,当前更小值可补成一三二。
        if hasMiddle && nums[i] < middle {
            return true
        }

        for len(stack) > 0 && nums[i] > stack[len(stack)-1] {
            // 当前值作为三,弹出值作为二,继续弹出以获得更有利的二。
            middle = stack[len(stack)-1]
            hasMiddle = true
            stack = stack[:len(stack)-1]
        }
        stack = append(stack, nums[i])
    }
    return false
}

复杂度分析

  • 时间复杂度:$O(n)$。每个元素至多入栈一次、出栈一次,内层循环总弹栈次数不超过 $n$。
  • 空间复杂度:$O(n)$。例如原数组从左到右非递减时,逆序扫描不会弹栈,栈可能保存全部元素。

关键点总结

[!green]

  • 先让当前值尝试充当“1”,再让它为更左侧的元素建立“3、2”候选。
  • middle 不是任意后缀元素,而是已经有左侧更大值与之配对的“2”。
  • 单调栈负责发现候选,middle 负责保留最有利的候选,两者作用不同。

易错点总结

[!yellow]

  • 改为从左向右套同样逻辑,无法保证“2”位于“3”之后。
  • middle 用 0 初始化且不区分是否有效,会在全负数数组中误报。
  • 弹栈条件和命中判断都要严格比较;相等的两个值不能承担模式中的不同排名。
  • 只弹一次而不用 while,栈可能失去单调性并错过更大的“2”候选。

相似题目

题目 难度 关联与区别
503. 下一个更大元素 II 中等 同样用单调栈消除被更优元素覆盖的候选,但本题栈弹出的值还承担132中间项的角色。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/35191906
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!