最长湍流子数组 图解题解
这道题到底在问什么
- 输入
- arr=[9,4,2,10,7,8,8,1,9]
- 输出
- 5 (4 大 2 小 10 大 7 小 8,比较符交替)
- 输入
- arr=[4,8,12,16]
- 输出
- 2 (一路上升不翻转,最多取相邻两个)
- 输入
- arr=[100]
- 输出
- 1 (单个元素自成一段)
最优解:为什么这么做
一句话答案:LeetCode 978 最长湍流子数组,找升降符号交替的最长段。双状态动态规划一遍扫:up 记这步在升、down 记这步在降的最长长度,升接旧 down、降接旧 up 交替续,相等归 1,答案取全程最大,时间 O(n)、空间 O(1)。
最长湍流子数组,这道题到底在找什么
给一个整数数组 arr,找最长的「湍流」子数组,返回长度。湍流就是相邻两数一升一降交替:大、小、大、小,或小、大、小、大,一旦相等或连续同向,交替就断。子数组要连续、不能跳挑。比如 arr=[4,8,12,16] 一路上升,最多取相邻两个得 2;arr=[100] 单数一段得 1。
为什么不能把每一段子数组都截出来验一遍
连续子数组有 O(n²) 段(大 O 记号,描述规模变大时操作数怎么涨),每段还要 O(n) 验符号是否交替,合起来 O(n³),稍长就跑不动。而且大量重叠:短段 4、2、10、7 验过,长段接上 8 又从头查一遍。既然长段就是短段接一个数,就不必每段重验,只盯「以当前数结尾、还在生长的湍流段有多长」往后接即可。
一个升态一个降态,为什么非得分开记两个长度
这里用动态规划(以当前数结尾的湍流段长算一次存 up/down,下一步直接接)。只记一个「当前最长湍流段」不够:下一步能不能接,取决于当前段结尾那步是升还是降。
所以拆成两个状态(记扫到当前为止两种局面各自的最长长度):up 是以当前数结尾、最后一步上升的最长段长,down 是最后一步下降的。两个都从 1 起,单数就是长度 1 的段。
升接旧 down、降接旧 up,两个状态怎么互相接力
扫到新数先和前一个比。这一步上升(前一个小于当前),要交替,前一段结尾得是下降,新 up 接旧 down:up = 旧 down + 1,同向的 down 归 1。这步下降反过来,新 down = 旧 up + 1,up 归 1。两数相等既不升也不降,湍流断掉,up、down 一起归 1、重数。每处理完一个数,用 up、down 刷新全局最长答案。
落到代码上,升态记成 f、降态记成 g,都从 1 起;每步先用 ff、gg 暂存新值再一起赋回,免得先改的盖掉要读的旧值。
把题面例子的 up、down 亲手走一遍
拿 arr=[9,4,2,10,7,8,8,1,9] 走一遍:起手 up=down=ans=1。扫 4 比 9 小是降,down = 旧 up + 1 = 2,ans=2;扫 2 仍降,down = 2;扫 10 转升,up = 旧 down + 1 = 3,ans=3;扫 7 又降,down = 旧 up + 1 = 4,ans=4;扫 8 转升,up = 旧 down + 1 = 5,ans=5;再扫 8 相等,硬断,up、down 全归 1,ans 留 5;扫 1 降,down = 2;扫 9 升,up = 旧 down + 1 = 3。全程最大 5,就是 4、2、10、7、8,符号大、小、大、小交替。
相等那步忘把 up、down 一起归 1,湍流为什么会假装没断
扫一遍、每数只比一次,时间 O(n);只留 up、down、ans 三个量轮换,空间 O(1),不用开二维表。
相等是最容易漏的硬断:符号一断,up、down 必须同时归 1、只清当前段,忘了归 1 会让后面错把断点前的长度接着算。ans 别跟着清,它记全程见过的最长,两个 8 打断后仍停在 5,每断就重置会把攒下的最长丢光。边界:单数返回 1,单调或全相等最多 2 或 1。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这套「升就 up 接旧 down、降就 down 接旧 up、相等全归 1,每步刷新答案」,下面每一帧都在套它。
- 4开局:第 0 个元素 9 自己就是一段长度 1 的湍流段(绿色)。up 和 down 都初始化成 1,从第 1 个起才有「前一个」可比。
- 5扫到第 1 个 4,和前一个 9 比。绿色是到上一个为止还在生长的湍流段(长度 1),此刻 up=1、down=1。看着是下降,下一帧看 down 怎么接。
- 64 比 9 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 2;up 归 1。刷新答案 ans=2。
- 7扫到第 2 个 2,和前一个 4 比。绿色是到上一个为止还在生长的湍流段(长度 2),此刻 up=1、down=2。看着是下降,下一帧看 down 怎么接。
- 82 比 4 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 2;up 归 1。还没超过 ans=2,答案不动。
- 9扫到第 3 个 10,和前一个 2 比。绿色是到上一个为止还在生长的湍流段(长度 2),此刻 up=1、down=2。看着是上升,下一帧看 up 怎么接。
- 1010 比 2 大,这一步是上升。湍流要交替,所以新的 up 接在「上一个结尾是下降」的段后面:up = 旧 down + 1 = 3;同向接不上的 down 归 1。这个 3 比旧答案大,刷新 ans=3。
- 11扫到第 4 个 7,和前一个 10 比。绿色是到上一个为止还在生长的湍流段(长度 3),此刻 up=3、down=1。看着是下降,下一帧看 down 怎么接。
- 127 比 10 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 4;up 归 1。刷新答案 ans=4。
- 13扫到第 5 个 8,和前一个 7 比。绿色是到上一个为止还在生长的湍流段(长度 4),此刻 up=1、down=4。看着是上升,下一帧看 up 怎么接。
- 148 比 7 大,这一步是上升。湍流要交替,所以新的 up 接在「上一个结尾是下降」的段后面:up = 旧 down + 1 = 5;同向接不上的 down 归 1。这个 5 比旧答案大,刷新 ans=5。
- 15扫到第 6 个 8,和前一个 8 比。绿色是到上一个为止还在生长的湍流段(长度 5),此刻 up=5、down=1。两个数一样大,怕是要断,下一帧见分晓。
- 168 和 8 一样大,既不算上升也不算下降,湍流彻底断在这里。up 和 down 同时归 1,从第 6 个重新数。注意 ans 仍保留着 5,断开只清空当前段,绝不抹掉历史最长。
- 17扫到第 7 个 1,和前一个 8 比。绿色是到上一个为止还在生长的湍流段(长度 1),此刻 up=1、down=1。看着是下降,下一帧看 down 怎么接。
- 181 比 8 小,这一步是下降。反过来,新的 down 接在「上一个结尾是上升」的段后面:down = 旧 up + 1 = 2;up 归 1。还没超过 ans=5,答案不动。
- 19全部扫完。一路上 up 和 down 此消彼长,最长的一段湍流是高亮的这 5 个:4、2、10、7、8,比较符大、小、大、小完整交替。后面 8、8 相等把湍流打断,所以没能更长。答案 5。
- 20回放这段赢家,看比较符怎么一步步翻转。先单看第 1 个 4,长度 1。
- 214 到 2 是下降,比较符是「大」,和前一对正式开头,交替成立,长度到 2。
- 222 到 10 是上升,比较符是「小」,和前一对正好相反,交替成立,长度到 3。
- 2310 到 7 是下降,比较符是「大」,和前一对正好相反,交替成立,长度到 4。
- 247 到 8 是上升,比较符是「小」,和前一对正好相反,交替成立,长度到 5。比较符一路 大、小、大、小 交替,五个元素,答案就是 5。
⚠️ 容易写错的地方
✗ 错:相等也想往下接
✓ 对:相等时 up、down 同时归 1
湍流要严格升降交替,8、8 相等直接打断,接不上
✗ 错:方向接反:上升去接旧 up
✓ 对:上升接旧 down,下降接旧 up
要交替,所以上升必须续在「上一步是下降」的段后面,接反就不是湍流了
✗ 错:ans 初值写成 0
✓ 对:ans 初值是 1
单个元素本身就是长度 1 的湍流段,初值 0 会让单元素答案错成 0
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxTurbulenceSize(self, arr: List[int]) -> int:
ans = f = g = 1
for a, b in pairwise(arr):
ff = g + 1 if a < b else 1
gg = f + 1 if a > b else 1
f, g = ff, gg
ans = max(ans, f, g)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxTurbulenceSize(vector<int>& arr) {
int ans = 1, f = 1, g = 1;
for (int i = 1; i < arr.size(); ++i) {
int ff = arr[i - 1] < arr[i] ? g + 1 : 1;
int gg = arr[i - 1] > arr[i] ? f + 1 : 1;
f = ff;
g = gg;
ans = max({ans, f, g});
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxTurbulenceSize(int[] arr) {
int ans = 1, f = 1, g = 1;
for (int i = 1; i < arr.length; ++i) {
int ff = arr[i - 1] < arr[i] ? g + 1 : 1;
int gg = arr[i - 1] > arr[i] ? f + 1 : 1;
f = ff;
g = gg;
ans = Math.max(ans, Math.max(f, g));
}
return ans;
}
}复杂度
时间
O(n)
从头到尾扫一遍,每个元素只看一次
空间
O(1)
只用 up、down、ans 三个变量,状态滚动复用
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长湍流子数组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么非要两个状态,只记一个「当前最长湍流长度」不行吗?+
不行。能不能把当前数接进去,取决于上一段结尾那一步是升还是降,一个数字记不下这个方向信息。拆成 up(结尾在升)和 down(结尾在降)两个状态,正好把两种结尾局面分开存:这一步在升就只能接旧 down、在降就只能接旧 up,方向天然对上。只留一个值会丢掉「结尾朝哪」,下一步该不该接就判不了。
两数相等时,为什么 up 和 down 都要归 1,而不是保持不变?+
相等既不算上升也不算下降,交替在这里被彻底打断,之前那段湍流不能再往后延,所以以当前这个数结尾的升态、降态都只剩它自己,长度归 1、从头重数。注意归 1 的只是这两个当前状态,记录全局最长的 ans 不动,它保存的是历史上见过的最长段,断开一次不该把之前的战果抹掉。
这题和最长递增子序列那类 DP 有什么不同?+
最长递增子序列(LeetCode 300 最长递增子序列)可以跳着挑、只要后一个更大就行,是子序列问题;本题要的是连续子数组,且相邻符号必须交替,一断就得重来。所以它不需要往前扫一整段找最优,只靠紧挨的前一步升还是降,就能用 up、down 两个状态 O(1) 地推着往前更新,整体一遍 O(n) 扫完,比 O(n²) 的最长递增子序列更省。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长湍流子数组 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。