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 <= 10000 <= nums[i] <= 1000
算法思路
方法一:贪心算法
核心思想:摆动序列的本质是在序列的局部极值点(峰值和谷值)处进行选择。
关键:
- 上升和下降状态:维护当前序列是处于上升状态还是下降状态
- 状态转换:只有当差值符号发生变化时,才增加序列长度
- 平坡处理:连续相等的元素可以忽略,只关心趋势变化
步骤:
- 如果数组长度 ≤ 1,直接返回长度
- 初始化结果为 1(至少包含第一个元素)
- 计算第一个非零差值,确定初始状态
- 遍历数组,当差值符号与前一个非零差值符号相反时,结果加 1
状态定义:
up:表示当前处于上升状态(前一个差值为正)down:表示当前处于下降状态(前一个差值为负)
方法二:动态规划
核心思想:维护两个状态:
up[i]:以nums[i]结尾且最后一步是上升的最长摆动序列长度down[i]:以nums[i]结尾且最后一步是下降的最长摆动序列长度
状态转移:
- 如果
nums[i] > nums[i-1]:up[i] = down[i-1] + 1,down[i] = down[i-1] - 如果
nums[i] < nums[i-1]:down[i] = up[i-1] + 1,up[i] = up[i-1] - 如果
nums[i] == nums[i-1]:up[i] = up[i-1],down[i] = down[i-1]
方法三:简化贪心
核心思想:摆动序列的长度等于峰值数量 + 1
步骤:
- 遍历数组,统计所有局部极大值和局部极小值的数量
- 结果 = 峰值数量 + 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
}
关键点
-
摆动序列:
- 差值必须严格在正负之间交替
- 相邻元素相等时,差值为0,不满足摆动条件
-
贪心策略:
- 在连续上升或下降的区间中,只需要保留端点
- 局部极值点(峰值和谷值)构成了最长摆动序列
-
边界情况处理:
- 空数组:返回 0
- 单元素:返回 1
- 两元素且不等:返回 2
- 全相等元素:返回 1
-
状态转换:
- 只有当趋势发生变化时才增加长度
- 连续相同趋势时,长度保持不变
常见问题
-
为什么连续上升时up不增加?
- 摆动序列要求交替变化,连续上升只能选择一个上升段
- 贪心策略会选择最有利的端点
-
如何处理相等元素?
- 相等元素的差值为0,既不是上升也不是下降
- 在状态转换时不考虑相等的情况,保持当前状态
-
为什么结果是max(up, down)?
- 最长摆动序列可能以升序结尾,也可能以降序结尾
- 取两者中的最大值
212

被折叠的 条评论
为什么被折叠?



