摆动序列 图解题解
这道题到底在问什么
- 输入
- nums=[1,7,4,9,2,5]
- 输出
- 6 (整个数组就是升降交替的)
最优解:为什么这么做
一句话答案:LeetCode 376 摆动序列:求相邻差正负交替的最长子序列长度,贪心数方向翻转——每翻一次方向长度加一、平和同方向跳过,一遍扫完,时间 O(n)、空间 O(1)。
摆动序列到底指什么,要返回什么
摆动序列指这样的子序列:相邻两个数的差严格地正负交替,一个升一个降来回摆。子序列是从原数组里删掉任意几个数、剩下的按原顺序排。题目给一个数组 nums,问最长的摆动子序列有多长。题面例子 nums=[1,7,4,9,2,5],整个数组本身就升降升降,答案是 6。
把所有子序列都试一遍,为什么不行
子序列能删任意个数,[1,7,4,9,2,5] 这 6 个数就有 2 的 6 次方种删法,把每一种都拿出来检查是不是摆动、再挑最长的,数组一长这个数量就爆炸,根本扫不完。得找个只扫一遍的办法。
为什么只数方向翻转的转折点就够
一段连续同方向(比如连着上升)的数里,只有两头的极值对摆动有用,中间那些台阶留着不会让交替变多,删掉也不破坏前后的正负交替。所以把每段同方向压成一个转折点,剩下的峰谷交替出现,就是最长的摆动子序列——落到操作上,只要在差的符号由正翻负、或由负翻正时把长度加一就够了。
这样贪着数不会亏:每段留住最靠后的那个峰或谷,等于给后面留了最大的回旋余地,接下来无论要接升还是接降都最容易接上。中间被删的台阶换成任何一个,后面能接的长度都不会更长。
两个变量 length 和 prev 怎么配合
落到代码上只要两个变量。length 从 1 起步,因为第一个数天然是子序列的起点;prev 记上一段的方向,初值 0 表示还没定方向。从第二个数起,算相邻差 diff=nums[i]-nums[i-1]:diff>0 且 prev≤0,说明刚翻成升,length 加一、prev 记成 1;diff<0 且 prev≥0,说明刚翻成降,length 加一、prev 记成 -1;diff 等于 0 或和上一段同方向,就跳过、prev 不动。
[1,7,4,9,2,5] 一步步数出 6
拿题面的 [1,7,4,9,2,5] 走一遍。length=1、prev=0。到 7,diff=6>0 且 prev≤0,翻成升,length=2、prev=1。到 4,diff=-3<0 且 prev≥0,翻成降,length=3、prev=-1。到 9,diff=5>0,又翻成升,length=4、prev=1。到 2,diff=-7<0,翻成降,length=5、prev=-1。到 5,diff=3>0,翻成升,length=6。整个数组升降交替,答案就是 6。
哪几步一疏忽就把长度算多
差是 0 的那一步得当平台阶跳过,一旦把它算成一次方向,长度会凭空多一格。同方向连着几个台阶也只在第一次翻转时加过一次,后面的台阶再加就重复计了。prev 的初值也不能设成 1 或 -1,设死了方向,开头第一个峰或谷会被误判漏掉,从 0 起才让第一步自由定向。单个数或全部相等的数组,长度就是 1。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这条「只数方向真正改变的峰和谷,平的和同方向的跳过」——下面每一帧都在套它。
- 4第一个数 1 永远算摆动子序列的起点,长度从 1 起步。现在方向还没确定,等下一个数来定升还是降。
- 5算出差 16,本段方向是「升 ↑」。和上一段方向「(还没定方向)」比一比:不一样,方向要翻转了。
- 6方向真的翻转了!把 nums[1]=17 选作新的峰(局部最高)(绿色高亮),摆动长度涨到 2。
- 7算出差 -12,本段方向是「降 ↓」。和上一段方向「升 ↑」比一比:不一样,方向要翻转了。
- 8方向真的翻转了!把 nums[2]=5 选作新的谷(局部最低)(绿色高亮),摆动长度涨到 3。
- 9算出差 5,本段方向是「升 ↑」。和上一段方向「降 ↓」比一比:不一样,方向要翻转了。
- 10方向真的翻转了!把 nums[3]=11 选作新的峰(局部最高)(绿色高亮),摆动长度涨到 4。
- 11算出差 3,本段方向是「升 ↑」。和上一段方向「升 ↑」比一比:一样,是同方向的台阶。
- 12同方向的延续,跳过 nums[4],不进子序列,长度仍是 4。
- 13算出差 2,本段方向是「升 ↑」。和上一段方向「升 ↑」比一比:一样,是同方向的台阶。
- 14同方向的延续,跳过 nums[5],不进子序列,长度仍是 4。
- 15算出差 -5,本段方向是「降 ↓」。和上一段方向「升 ↑」比一比:不一样,方向要翻转了。
- 16方向真的翻转了!把 nums[6]=10 选作新的谷(局部最低)(绿色高亮),摆动长度涨到 5。
- 17算出差 -5,本段方向是「降 ↓」。和上一段方向「降 ↓」比一比:一样,是同方向的台阶。
- 18同方向的延续,跳过 nums[7],不进子序列,长度仍是 5。
- 19算出差 11,本段方向是「升 ↑」。和上一段方向「降 ↓」比一比:不一样,方向要翻转了。
- 20方向真的翻转了!把 nums[8]=16 选作新的峰(局部最高)(绿色高亮),摆动长度涨到 6。
- 21算出差 -8,本段方向是「降 ↓」。和上一段方向「升 ↑」比一比:不一样,方向要翻转了。
- 22方向真的翻转了!把 nums[9]=8 选作新的谷(局部最低)(绿色高亮),摆动长度涨到 7。
- 23整趟扫完,绿色高亮的 7 个数 [1, 17, 5, 11, 10, 16, 8] 就是一条最长摆动子序列,长度 7。一次遍历搞定,时间 O(n)。
⚠️ 容易写错的地方
✗ 错:把平的(差 0)当成一次方向
✓ 对:差 0 要跳过,方向不变
平台阶不交替,算进去会虚增长度
✗ 错:同方向连续都各加一次
✓ 对:同方向只在第一次翻转时加一
连续上升只算一个上升段,多个台阶不重复计
✗ 错:prev 初值设成 +1 或 -1
✓ 对:prev 要从 0(未定)起
设死方向会让第一步误判,错过开头的峰/谷
完整代码(Python / C++ / Java)
Python
def wiggleMaxLength(nums):
if len(nums) < 2:
return len(nums)
length = 1 # nums[0] 起点
prev = 0 # 上一段方向 +1/-1/0
for i in range(1, len(nums)):
diff = nums[i] - nums[i-1]
if diff > 0 and prev <= 0: # 翻成升
length += 1; prev = 1
elif diff < 0 and prev >= 0: # 翻成降
length += 1; prev = -1
# 平 或 同方向 → 跳过
return lengthC++
int wiggleMaxLength(vector<int>& nums){
if(nums.size() < 2) return nums.size();
int length = 1, prev = 0; // prev:+1/-1/0
for(int i = 1; i < (int)nums.size(); i++){
int diff = nums[i] - nums[i-1];
if(diff > 0 && prev <= 0){ length++; prev = 1; }
else if(diff < 0 && prev >= 0){ length++; prev = -1; }
// diff==0 或 同方向 → 跳过
}
return length;
}Java
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length < 2) return nums.length;
int length = 1, prev = 0; // prev:+1/-1/0
for (int i = 1; i < nums.length; i++) {
int diff = nums[i] - nums[i - 1];
if (diff > 0 && prev <= 0) { length++; prev = 1; }
else if (diff < 0 && prev >= 0) { length++; prev = -1; }
// diff == 0 或 同方向 → 跳过
}
return length; // 摆动子序列长度
}
}复杂度
时间
O(n)
一次遍历,每个数只看一次相邻差
空间
O(1)
只用 length 和 prev 两个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 摆动序列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
除了贪心,摆动序列还有别的解法吗?+
有动态规划写法:记两个量,一个表示以当前位置结尾、最后一步是上升的最长摆动长度,另一个表示最后一步是下降的。转移看 nums[i] 与 nums[i-1] 的大小,遇到上升就从「上一步下降」的那个加一,遇到下降就从「上一步上升」的那个加一。这两个量各自只需一个变量滚动更新,时间也是 O(n)。贪心其实是它的等价简化版,两者本质都在数方向变化的次数,只是贪心更直观。
为什么只在方向翻转时取峰谷、中间的数全跳过,不会漏掉更长的答案?+
在一段连续同方向(或相等)的数里,留住最靠后的那个极值点最优。中间的台阶删掉不影响前后的正负交替,交替次数一个不少;而留最靠后的极值,等于把峰抬到最高、谷压到最低,给后面接下一段留了最大的余地,后续无论升降都最容易接上。所以每次只在方向翻转时取峰或谷,一定不会比别的取法短。
相邻两个数相等(差为 0)该怎么处理?+
差 0 不构成任何方向,当平台阶跳过、prev 保持不动就行。如果把相等也算成一次方向,长度会虚增:比如 [1,1,1] 的正确答案是 1,可一旦把两次相等各当成一次方向变化,就会误算成 3。所以判断时只认严格的 diff>0 和 diff<0,等于 0 的那一步不加长度、也不改 prev。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 摆动序列 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。