目录

题目描述

376. 摆动序列

题意分析

给一个整数数组,要在里面挑出一个子序列(可以删元素,但保留的元素必须维持原有先后顺序),使得这个子序列中相邻两数的差正负交替,求这种子序列的最大长度。

定义上有两处容易读漏。其一,差值为 0 既不算正也不算负,所以相等的相邻元素不能同时留在子序列里。其二,长度为 1 的序列没有差值,题目约定它也是摆动序列;长度为 2 且两数不等的序列同样是摆动序列。

约束信号:数组长度上限 1000,元素范围 $[0, 1000]$。规模不大,$O(n^2)$ 也能过,但题目进阶明确要求 $O(n)$ 一趟解决,说明存在只依赖相邻关系的递推。

边界情况:只有一个元素时答案为 1;所有元素全部相等时,最多只能留一个,答案仍是 1;元素单调递增或递减时,只能留首尾两个,答案是 2。

解法:峰谷贪心

核心思路

摆动序列真正关心的不是差值大小,而是非零差值的符号能否在正、负之间交替。因此可以把数组看成若干段连续上升或连续下降的趋势,最优子序列只需保留每段交界处的「峰」和「谷」。

为什么趋势中间的点可以丢掉?假设一段一直上升,在其中多选两个元素只会得到两个同为正的差,无法增加摆动次数。把已选的末尾替换成这段最右侧、也是最大的元素,不会破坏前一次上升,反而更容易与后面的较小元素形成下降。连续下降时对称地保留更小的谷点。这个替换过程不减少任何可行解的长度,所以只数峰谷就能得到最优解。

实现时用 prevDiff 记录上一个已纳入答案的非零差值方向,answer 从 1 开始。当前差值为正且 prevDiff <= 0 时,说明遇到第一次上升或从下降转为上升;当前差值为负且 prevDiff >= 0 时则对称。只有这两种情况才让答案加一,差值为 0 直接忽略。

正确性:上面的替换论证说明,任意最优解都能在不变短的前提下,被改造成只包含首元素和峰谷的子序列;算法恰好在每次趋势反转时选中一个新峰或新谷,构造出同样长的可行解。因此它既不会少选,也不可能超过最优值。

解题步骤

  1. 初始化 answer = 1prevDiff = 0。单个元素已是长度为 1 的摆动序列,0 表示尚未出现有效方向。
  2. 从下标 1 开始,计算相邻差 diff = nums[i] - nums[i - 1]
  3. diff > 0 && prevDiff <= 0,当前元素形成第一段上升或新峰,令 answer++,并记录这次正差值。
  4. diff < 0 && prevDiff >= 0,当前元素形成第一段下降或新谷,同样累加答案并更新 prevDiff
  5. 若差值为 0,或者与 prevDiff 同号,不增加长度:前者不是摆动,后者只是把当前趋势的峰或谷向后推。
  6. 遍历结束后返回 answer

例如 nums = [1,7,7,4,9,2,5],非零差值方向依次为「正、负、正、负、正」,中间的零差值被忽略。算法选出 [1,7,4,9,2,5],答案为 6。

代码实现

class Solution {
    public int wiggleMaxLength(int[] nums) {
        int answer = 1;
        int prevDiff = 0;
        for (int i = 1; i < nums.length; i++) {
            int diff = nums[i] - nums[i - 1];
            if ((diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0)) {
                answer++;
                prevDiff = diff;
            }
        }
        return answer;
    }
}
func wiggleMaxLength(nums []int) int {
    answer, prevDiff := 1, 0
    for i := 1; i < len(nums); i++ {
        diff := nums[i] - nums[i-1]
        if (diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0) {
            answer++
            prevDiff = diff
        }
    }
    return answer
}

复杂度分析

  • 时间复杂度:$O(n)$。只顺序扫描一次数组。
  • 空间复杂度:$O(1)$。只使用答案、当前差值和上一个有效差值等常数变量。

关键点总结

  • 把问题转换为「统计非零差值的符号变化」,就能从子序列枚举转向线性扫描。
  • 贪心选峰谷的依据是替换论证:上升段保留更大的末尾,下降段保留更小的末尾,都不会让后续选择变差。
  • prevDiff 只需要保留上一个有效方向;连续同号的差值不会增加摆动长度。
  • 零差值既不是上升也不是下降,必须跳过,且不能清空之前的有效方向。
  • 面试时先讲「单调段内中间点无贡献」,再讲「保留更极端的末尾」,就能完整回答贪心选择为什么正确。

易错点总结

  • 把零差值当成一次摆动[1,1,1] 的正确答案是 1,不能用 >=<= 把相等元素归入上升、下降。
  • 遇到相等元素就把 prevDiff 清零[1,3,3,5] 会把同一段上升计数两次,错误返回 3,正确答案是 2。
  • 忽略第一个非零差值:若判断条件写成 prevDiff < 0prevDiff > 0,初值 0 无法进入任一分支,[1,2] 会错误返回 1。
  • 连续同向时也累加答案[1,2,3] 只能选首尾两个元素;两个正差值不交替,答案不是 3。
  • 把子序列误当成连续子数组[1,17,5,10,13,15,10,5,16,8] 可以删去中间的同向元素得到长度 7,不能只统计最长连续摆动段。

相似题目

题目 难度 考察点
122. 买卖股票的最佳时机 II 中等 同样识别波峰波谷,但求差值累加而非计数
300. 最长递增子序列 中等 单一方向约束,需贪心加二分才能降到对数
845. 数组中的最长山脉 中等 要求连续子数组且只允许先升后降一次
53. 最大子数组和 中等 滚动 DP 求和的最值,状态只有一维