算法题 摆动序列

LeetCode 376. 摆动序列

问题描述

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列

第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列

例如:

  • [1, 7, 4, 9, 2, 5] 是摆动序列,因为差值 (6, -3, 5, -7, 3) 是正负交替的。
  • [1, 4, 7, 2, 5] 不是摆动序列,因为前两个差值都是正数。
  • [1, 7, 4, 5, 5] 不是摆动序列,因为最后一个差值为零。

给定一个整数数组 nums,返回 nums 中作为 摆动序列最长子序列的长度

子序列可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

示例

输入: nums = [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列,各数之间的差值为 (6,-3,5,-7,3)

输入: nums = [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 最长摆动序列为 [1,17,10,13,10,16,8],各数之间的差值为 (16,-7,3,-3,6,-8)

输入: nums = [1,2,3,4,5,6,7,8,9]
输出: 2
解释: 任何包含两个元素的子序列都是摆动序列

约束条件

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000

算法思路

方法一:贪心算法

核心思想:摆动序列的本质是在序列的局部极值点(峰值和谷值)处进行选择。

关键

  1. 上升和下降状态:维护当前序列是处于上升状态还是下降状态
  2. 状态转换:只有当差值符号发生变化时,才增加序列长度
  3. 平坡处理:连续相等的元素可以忽略,只关心趋势变化

步骤

  1. 如果数组长度 ≤ 1,直接返回长度
  2. 初始化结果为 1(至少包含第一个元素)
  3. 计算第一个非零差值,确定初始状态
  4. 遍历数组,当差值符号与前一个非零差值符号相反时,结果加 1

状态定义

  • up:表示当前处于上升状态(前一个差值为正)
  • down:表示当前处于下降状态(前一个差值为负)

方法二:动态规划

核心思想:维护两个状态:

  • up[i]:以 nums[i] 结尾且最后一步是上升的最长摆动序列长度
  • down[i]:以 nums[i] 结尾且最后一步是下降的最长摆动序列长度

状态转移

  • 如果 nums[i] > nums[i-1]up[i] = down[i-1] + 1down[i] = down[i-1]
  • 如果 nums[i] < nums[i-1]down[i] = up[i-1] + 1up[i] = up[i-1]
  • 如果 nums[i] == nums[i-1]up[i] = up[i-1]down[i] = down[i-1]

方法三:简化贪心

核心思想:摆动序列的长度等于峰值数量 + 1

步骤

  1. 遍历数组,统计所有局部极大值和局部极小值的数量
  2. 结果 = 峰值数量 + 1

代码实现

方法一:贪心算法

class Solution {
    /**
     * 计算最长摆动序列长度
     * 
     * @param nums 整数数组
     * @return 最长摆动序列的长度
     */
    public int wiggleMaxLength(int[] nums) {
        // 边界情况处理
        if (nums == null || nums.length <= 1) {
            return nums.length;
        }
        
        // 初始化状态
        boolean isUp = false;   // 是否处于上升状态
        boolean isDown = false; // 是否处于下降状态
        int length = 1;         // 至少包含第一个元素
        
        // 从第二个元素开始遍历
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) {
                // 当前是上升趋势
                if (!isUp) {
                    // 如果之前不是上升状态,说明发生了状态转换
                    length++;
                    isUp = true;
                    isDown = false;
                }
            } else if (nums[i] < nums[i - 1]) {
                // 当前是下降趋势
                if (!isDown) {
                    // 如果之前不是下降状态,说明发生了状态转换
                    length++;
                    isDown = true;
                    isUp = false;
                }
            }
            // 相等的情况:状态保持不变,length不增加
        }
        
        return length;
    }
}

方法二:优化贪心

class Solution {
    /**
     * 优化贪心算法:只维护两个长度值
     * 
     * @param nums 整数数组
     * @return 最长摆动序列的长度
     */
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length <= 1) {
            return nums.length;
        }
        
        // up: 以升序结尾的最长摆动序列长度
        // down: 以降序结尾的最长摆动序列长度
        int up = 1, down = 1;
        
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) {
                // 上升:可以接在之前的下降序列后面
                up = down + 1;
            } else if (nums[i] < nums[i - 1]) {
                // 下降:可以接在之前的上升序列后面
                down = up + 1;
            }
            // 相等时,up和down都保持不变
        }
        
        return Math.max(up, down);
    }
}

方法三:动态规划

class Solution {
    /**
     * 动态规划
     * 
     * @param nums 整数数组
     * @return 最长摆动序列的长度
     */
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length <= 1) {
            return nums.length;
        }
        
        int n = nums.length;
        // up[i]: 以nums[i]结尾且最后一步上升的最长摆动序列长度
        // down[i]: 以nums[i]结尾且最后一步下降的最长摆动序列长度
        int[] up = new int[n];
        int[] down = new int[n];
        
        // 初始化
        up[0] = 1;
        down[0] = 1;
        
        for (int i = 1; i < n; i++) {
            if (nums[i] > nums[i - 1]) {
                up[i] = down[i - 1] + 1;
                down[i] = down[i - 1];
            } else if (nums[i] < nums[i - 1]) {
                down[i] = up[i - 1] + 1;
                up[i] = up[i - 1];
            } else {
                // 相等的情况
                up[i] = up[i - 1];
                down[i] = down[i - 1];
            }
        }
        
        return Math.max(up[n - 1], down[n - 1]);
    }
}

方法四:峰值计数

class Solution {
    /**
     * 峰值计数:统计局部极值点
     * 
     * @param nums 整数数组
     * @return 最长摆动序列的长度
     */
    public int wiggleMaxLength(int[] nums) {
        if (nums == null || nums.length <= 1) {
            return nums.length;
        }
        
        int count = 1; // 至少包含第一个元素
        int prevDiff = 0; // 前一个差值
        
        for (int i = 1; i < nums.length; i++) {
            int currDiff = nums[i] - nums[i - 1];
            
            // 当前差值与前一个差值符号相反时,说明遇到了峰值或谷值
            if ((currDiff > 0 && prevDiff <= 0) || (currDiff < 0 && prevDiff >= 0)) {
                count++;
                prevDiff = currDiff;
            }
            // 如果currDiff == 0,prevDiff保持不变
        }
        
        return count;
    }
}

算法分析

方法时间复杂度空间复杂度
贪心算法O(n)O(1)
优化贪心O(n)O(1)
动态规划O(n)O(n)
峰值计数O(n)O(1)

算法过程

nums = [1,17,5,10,13,15,10,5,16,8]

优化贪心:

初始: up = 1, down = 1

i=1: 17 > 1 → up = down + 1 = 2, down = 1
i=2: 5 < 17 → down = up + 1 = 3, up = 2  
i=3: 10 > 5 → up = down + 1 = 4, down = 3
i=4: 13 > 10 → up 不变 = 4 (因为连续上升)
i=5: 15 > 13 → up 不变 = 4 (因为连续上升)
i=6: 10 < 15 → down = up + 1 = 5, up = 4
i=7: 5 < 10 → down 不变 = 5 (因为连续下降)
i=8: 16 > 5 → up = down + 1 = 6, down = 5
i=9: 8 < 16 → down = up + 1 = 7, up = 6

结果: max(6, 7) = 7

对应的摆动序列:

[1, 17, 5, 15, 5, 16, 8][1, 17, 5, 13, 5, 16, 8]

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准摆动序列
    int[] nums1 = {1, 7, 4, 9, 2, 5};
    System.out.println("Test 1: " + solution.wiggleMaxLength(nums1)); // 6
    
    // 测试用例2:复杂摆动序列
    int[] nums2 = {1, 17, 5, 10, 13, 15, 10, 5, 16, 8};
    System.out.println("Test 2: " + solution.wiggleMaxLength(nums2)); // 7
    
    // 测试用例3:单调递增序列
    int[] nums3 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    System.out.println("Test 3: " + solution.wiggleMaxLength(nums3)); // 2
    
    // 测试用例4:单调递减序列
    int[] nums4 = {9, 8, 7, 6, 5, 4, 3, 2, 1};
    System.out.println("Test 4: " + solution.wiggleMaxLength(nums4)); // 2
    
    // 测试用例5:包含重复元素
    int[] nums5 = {1, 1, 1, 1, 1};
    System.out.println("Test 5: " + solution.wiggleMaxLength(nums5)); // 1
    
    // 测试用例6:两个元素
    int[] nums6 = {1, 2};
    System.out.println("Test 6: " + solution.wiggleMaxLength(nums6)); // 2
    
    // 测试用例7:单个元素
    int[] nums7 = {1};
    System.out.println("Test 7: " + solution.wiggleMaxLength(nums7)); // 1
    
    // 测试用例8:空数组
    int[] nums8 = {};
    System.out.println("Test 8: " + solution.wiggleMaxLength(nums8)); // 0
    
    // 测试用例9:先升后降
    int[] nums9 = {1, 2, 1, 2, 1};
    System.out.println("Test 9: " + solution.wiggleMaxLength(nums9)); // 5
    
    // 测试用例10:包含零的序列
    int[] nums10 = {0, 0, 0, 1, 0, 0, 2, 0};
    System.out.println("Test 10: " + solution.wiggleMaxLength(nums10)); // 5
}

关键点

  1. 摆动序列

    • 差值必须严格在正负之间交替
    • 相邻元素相等时,差值为0,不满足摆动条件
  2. 贪心策略

    • 在连续上升或下降的区间中,只需要保留端点
    • 局部极值点(峰值和谷值)构成了最长摆动序列
  3. 边界情况处理

    • 空数组:返回 0
    • 单元素:返回 1
    • 两元素且不等:返回 2
    • 全相等元素:返回 1
  4. 状态转换

    • 只有当趋势发生变化时才增加长度
    • 连续相同趋势时,长度保持不变

常见问题

  1. 为什么连续上升时up不增加?

    • 摆动序列要求交替变化,连续上升只能选择一个上升段
    • 贪心策略会选择最有利的端点
  2. 如何处理相等元素?

    • 相等元素的差值为0,既不是上升也不是下降
    • 在状态转换时不考虑相等的情况,保持当前状态
  3. 为什么结果是max(up, down)?

    • 最长摆动序列可能以升序结尾,也可能以降序结尾
    • 取两者中的最大值
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值