LeetCode Hot 100 题解 · 普通数组篇
- 本专题收录 LeetCode Hot 100 中所有 普通数组 相关题目。
- 每道题提供最直接、最容易理解的解题思路,包含详细注释的代码实现,方便笔试和面试复习。
📑 目录
| 题号 | 题目 | 难度 | 核心思路 |
|---|---|---|---|
| 53 | 53.最大子数组和 | 🟠中等 | dp[i] = 以 i 结尾的最大子数组和 :转移只有两种------接上前面的(dp[i-1] + x)或自己另起炉灶(x),取大者。前面的和是负的就果断舍弃 (只会拖累)。答案取所有 dp[i] 的最大值,不是 dp[n-1]。空间压掉就是"贪心"写法。 |
| 56 | 56.合并区间 | 🟠中等 | 按左端点排序 + 一次扫描 :排序后只有右端点需要合并判断------当前区间的左端点若 ≤ 上一结果的右端点,二者重叠,右端点取 max 即可(覆盖型相交,不是首尾相接);否则区间彻底分开,上一结果安全出栈。 |
| 189 | 189.轮转数组 | 🟠中等 | 三次翻转 :整体翻转 → 翻前 k 个 → 翻后 n-k 个。右移 k 位本质是把尾部 k 个数和前部 n-k 个数换块 ,而"换块"恰好等价于"整体翻 + 各自翻"。原地 O(1) 空间,绕开 O(n) 额外数组。 |
| 238 | 238.除自身以外数组的乘积 | 🟠中等 | 前后缀分解 :res[i] = 左边所有数的积 × 右边所有数的积。从左扫一遍顺手把前缀积写进 res,再从右扫一遍用一个变量 suf 滚动携带后缀积,乘进去。全程不出现除法 (0 会把除法炸掉),O(1) 额外空间。 |
| 41 | 41.缺失的第一个正数 | 🔴困难 | 原地哈希(把数组自己当哈希表) :长度 n 的数组,答案必在 [1, n+1] 里------1..n 各回各家各找各妈(把值 v 换到下标 v-1),谁家没主人谁就是缺失者,全齐则是 n+1。答案范围被数组长度框死 是突破口,排序 O(n log n)、开哈希表 O(n) 空间都因此能被原地置换替代。 |
53.最大子数组和
题目链接 :53.最大子数组和
题目描述 :给你一个整数数组 nums(元素可正可负可为 0 ),找出和最大的连续子数组 (至少含一个元素),返回这个最大和。比如 [-2,1,-3,4,-1,2,1,-5,4] 答案是 6(子数组 [4,-1,2,1])。
思路一:动态规划(以 i 结尾的最大和,负前缀果断舍弃)
连续子数组的题目可以考虑dp
核心思想:
- 暴力枚举所有子数组是
O(n²)。观察问题特征:想知道"以i结尾的最优解",只需要知道"以i-1结尾的最优解"------多走一步只会多一个决策 ,这是线性DP的标准信号。 - 定义
dp[i]= 以i结尾 的最大子数组和(注意这个"以 i 结尾"的限定,它是转移能成立的关键)。转移只有两个选项:- 接着前面的延伸:
dp[i-1] + nums[i]; - 前面的和是负的,接了只亏不赚,自己另起炉灶 :
nums[i]。 - 取两者较大值------本质就是
dp[i-1] < 0时舍弃前缀。
- 接着前面的延伸:
- 答案是
max(dp[0..n-1])而不是dp[n-1]:最优子数组可能结束在任何位置,"以 i 结尾"只是求解手段,不是答案的形状。 dp[i]只依赖dp[i-1],一个滚动变量就够,数组不用开。
时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,单趟扫描。
空间复杂度 : O ( 1 ) O(1) O(1),滚动变量代替 dp 数组。
O(n)空间写法:
java
class Solution {
public int maxSubArray(int[] nums) {
// 类似寻找最大最小值的题目,初始值一定要定义成理论上的最小最大值
int ans = Integer.MIN_VALUE;
int n = nums.length;
// dp[i]表示nums中以nums[i]结尾的最大子序和
int[] dp = new int[n];
dp[0] = nums[0];
ans = dp[0];
for (int i = 1; i < n; ++i) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); // 选或不选前面所有
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
O(1)空间写法:
java
class Solution {
public int maxSubArray(int[] nums) {
// pre: 以当前元素结尾的最大子数组和(滚动替代 dp 数组)
int pre = 0;
int ans = Integer.MIN_VALUE; // 元素可能全为负,不能用 0 初始化
for (int x : nums) {
pre = Math.max(pre + x, x); // 接上前缀 or 另起炉灶,前缀为负自然被舍弃
ans = Math.max(ans, pre); // 最优子数组可能结束在任何位置,每步都结算
}
return ans;
}
}
同一个转移换身衣服就是贪心写法:
sum += x; if (sum < 0) sum = 0;每步用sum更新答案------sum < 0就清零,等价于dp里"负前缀舍弃"。两版完全等价,面试讲dp版,因果链最清楚。
贪心C++写法:
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 类似寻找最大最小值的题目,初始值一定要定义成理论上的最小最大值
int result = INT_MIN;
int numsSize = int(nums.size());
int sum = 0;
for (int i = 0; i < numsSize; i++) {
sum += nums[i];
result = max(result, sum);
// 如果sum < 0,重新开始找子序串
if (sum < 0) {
sum = 0;
}
}
return result;
}
};
思路二:分治(最大子段和拆成三块:左半、右半、横跨中线)
核心思想:
- 递归地想:
[left, right]的最大子数组和只有三种可能------完全落在左半[left, mid]、完全落在右半[mid+1, right]、横跨中线mid。前两种递归求解,第三种单独算,三者取最大。 - 横跨中线的情况最讲究:这样的子数组一定同时包含
nums[mid]和nums[mid+1],所以它 = 左半边"以mid结尾"的最大子数组和 + 右半边"以mid+1开头"的最大子数组和------两个方向各扫一遍,起点终点被钉死,问题退化成两趟简单累加。 - "以 i 结尾/开头"这个限定和思路一的
dp是同一个技巧:把自由端点钉死,最优解才可增量计算。 - 单题用它杀鸡用牛刀(常数大、代码长),但它是线段树维护区间最大子段和的原型------面试当加分项提一句。
时间复杂度 : O ( n log n ) O(n \log n) O(nlogn),其中 n 为数组长度,T(n) = 2T(n/2) + O(n)。
空间复杂度 : O ( log n ) O(\log n) O(logn),递归栈深度。
java
public class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
return maxSubArraySum(nums, 0, len - 1);
}
private int maxCrossingSum(int[] nums, int left, int mid, int right) {
// 一定会包含 nums[mid] 这个元素
int sum = 0;
int leftSum = Integer.MIN_VALUE;
// 左半边包含 nums[mid] 元素,最多可以到什么地方
// 走到最边界,看看最值是什么
// 计算以 mid 结尾的最大的子数组的和
for (int i = mid; i >= left; i--) {
sum += nums[i];
if (sum > leftSum) {
leftSum = sum;
}
}
sum = 0;
int rightSum = Integer.MIN_VALUE;
// 右半边不包含 nums[mid] 元素,最多可以到什么地方
// 计算以 mid+1 开始的最大的子数组的和
for (int i = mid + 1; i <= right; i++) {
sum += nums[i];
if (sum > rightSum) {
rightSum = sum;
}
}
return leftSum + rightSum;
}
private int maxSubArraySum(int[] nums, int left, int right) {
if (left == right) {
return nums[left];
}
int mid = left + (right - left) / 2;
return max3(maxSubArraySum(nums, left, mid),
maxSubArraySum(nums, mid + 1, right),
maxCrossingSum(nums, left, mid, right));
}
private int max3(int num1, int num2, int num3) {
return Math.max(num1, Math.max(num2, num3));
}
}
56.合并区间
题目链接 :56.合并区间
题目描述 :给你一堆区间 intervals[i] = [start, end],把所有有重叠的区间合并成一个 ,返回不重叠的区间列表。比如 [[1,3],[2,6],[8,10],[15,18]] → [[1,6],[8,10],[15,18]]。
思路一:按左端点排序 + 一次扫描(只有右端点需要判断)
核心思想:
- 区间乱序时,一个新区间可能和前面任何一个重叠,没法增量处理。按左端点升序排序 之后局面完全变了:扫到区间
i时,它前面所有区间的左端点都 ≤ 它的左端点------它能重叠的,只可能是"当前合并结果"的最后一个区间,更早的早就定型了。 - 于是一次扫描即可:当前区间
[s, e]和结果尾[ts, te]比:s <= te:重叠(注意是 ≤ ,端点相接如[1,4]和[4,5]也要合并),尾区间右端点扩张成max(te, e)------取 max 是因为新区间可能整个被尾区间包住 (如[1,10]后跟[2,3]),右端点不能变小;s > te:彻底分开,尾区间出结果,[s, e]成为新的尾。
- 每个区间进出结果各一次,加上排序就是全部开销。
时间复杂度 : O ( n log n ) O(n \log n) O(nlogn),其中 n 为区间数,排序占大头;扫描本身 O ( n ) O(n) O(n)。
空间复杂度 : O ( log n ) O(\log n) O(logn),排序的栈空间(不计返回值)。
java写法:
java
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) {
return new int[0][2];
}
// 按左端点升序:保证当前区间只可能和结果尾重叠
Arrays.sort(intervals, new Comparator<int[]>() {
public int compare(int[] interval1, int[] interval2) {
return interval1[0] - interval2[0];
}
});
List<int[]> ans = new ArrayList<int[]>();
for (int i = 0; i < intervals.length; ++i) {
// 彻底分开,新区间
if (ans.size() == 0 || intervals[i][0] > ans.get(ans.size() - 1)[1]) {
ans.add(new int[] { intervals[i][0], intervals[i][1] });
} else {
// 与结果尾重叠:只扩张右端点,取 max 防止新区间被尾完全包住
ans.get(ans.size() - 1)[1] = Math.max(ans.get(ans.size() - 1)[1], intervals[i][1]);
}
}
return ans.toArray(new int[ans.size()][]);
// return ans.toArray(new int[0][]);
}
}
ans.toArray(new int[0][])为什么对:toArray的参数不是用来装结果的,只用来指明返回类型 (不传会返回Object[],强转int[][]编译都过不了)。传入数组够装就直接用它;不够装(长度 0 必然不够)就由ArrayList自己 new 一个长度正确的int[][]装进去返回。所以new int[0][]和new int[res.size()][]结果完全一样,前者还省去先问size()再分配这一步,是惯用写法。
高频出错点:合并条件写当前区间左端点 < 待合并区间右端点(漏掉端点相接)或右端点直接赋当前区间右端点都是错误的。记住两条:≤才算重叠;右端点永远取max。
189.轮转数组
题目链接 :189.轮转数组
题目描述 :把数组 nums 整体向右轮转 k 个位置 (尾部绕回头部),原地 修改。比如 [1,2,3,4,5,6,7]、k = 3 → [5,6,7,1,2,3,4]。要求空间 O(1)------一个额外数组直接拷贝 O(n) 空间的写法在此题是被点名禁止的。
思路一:三次翻转(原地换块)
线代知识一样,类似矩阵翻转,或者离散数学里面的布尔矩阵的相关运算定律。
核心思想:
- 右移
k(先k %= n)后,数组变成[n-k .. n-1]在前、[0 .. n-k-1]在后------本质是把尾部k个数和前部n-k个数换了块,两块内部顺序不变。 - 直接换块要
O(n)空间。巧的是翻转恰好能完成换块 :先整体翻转(两块换位了,但块内是逆序的),再分别把两块各自翻转回来(块内顺序复原)。三步都是原地交换,O(1)空间。 - 以
[1,2,3,4,5,6,7]、k = 3为例:- 整体翻:
[7,6,5,4,3,2,1] - 翻前
k=3个:[5,6,7,4,3,2,1] - 翻后
n-k=4个:[5,6,7,1,2,3,4]✓
- 整体翻:
时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,三次翻转共约 2n 次交换。
空间复杂度 : O ( 1 ) O(1) O(1),纯原地交换。
java
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n; // k 可能大于 n,轮转 n 次等于没转
reverse(nums, 0, n - 1); // 1. 整体翻转:两块换位(块内暂为逆序)
reverse(nums, 0, k - 1); // 2. 翻前 k 个:恢复前块内部顺序
reverse(nums, k, n - 1); // 3. 翻后 n-k 个:恢复后块内部顺序
}
// 原地翻转 [l, r]
private void reverse(int[] nums, int l, int r) {
while (l < r) {
int tmp = nums[l];
nums[l] = nums[r];
nums[r] = tmp;
l++;
r--;
}
}
}
思路二:环状替换(每个数直接跳到终点)
核心思想 :从下标 start 出发,把 nums[start] 直接送去它轮转后的终点 (start + k) % n,被顶出来的数再送去它 的终点......如此接力,直到绕回 start,一圈完成。一圈不一定覆盖所有下标(gcd(n, k) 圈才覆盖完),外层用一个 count 数搬动了几个数,搬满 n 个就收工 。纯原地 O(1) 空间,但下标接力容易绕晕,笔试不如翻转版稳。
时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,每个下标恰好被访问一次。
空间复杂度 : O ( 1 ) O(1) O(1)。
java
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n;
if (k == 0) {
return; // 没得转,gcd 也不需要,直接收工
}
int count = 0; // 已就位的元素个数,搬满 n 个结束(防止 gcd 圈死循环)
for (int start = 0; count < n; start++) { // 一圈回不到 start 就换新的起点再来一圈
int cur = start;
int prev = nums[start];
do {
int next = (cur + k) % n; // prev 的最终归宿
int tmp = nums[next]; // 先取出被顶出来的数
nums[next] = prev; // 送 prev 就位
prev = tmp; // 被顶出来的继续接力
cur = next;
count++;
} while (cur != start); // 绕回起点,这一圈结束
}
}
}
两版怎么选:翻转版胜在步骤可复述 (翻、翻、翻三步走完),环状替换胜在每一步都是"直接放对位置"。面试先讲翻转版,讲得清;环状替换当"还有个原地一步到位的做法"提一嘴。
思路三:新开数组
思路二:环状替换(每个数直接跳到终点)
核心思想:对于这种移位类的题目,增加长度或者新开数组是最直接的方法,可以减少思考空间。
时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,每个下标恰好被访问一次。
空间复杂度 : O ( n ) O(n) O(n)。
java版写法:
java
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
int[] newArr = new int[n];
for (int i = 0; i < n; ++i) {
newArr[(i + k) % n] = nums[i];
}
System.arraycopy(newArr, 0, nums, 0, n);
}
}
C++版写法:
cpp
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k % n;
nums.resize(n + k);
for (int i = n - 1; i >= 0; --i) { // 整体右移
nums[i + k] = nums[i];
}
for (int i = 0; i < k;
++i) { // 超出的元素赋值(超出的元素必然是[n-k,n-1],共k个)
nums[i] = nums[i + n];
}
nums.resize(n);
}
};
238.除自身以外数组的乘积
题目链接 :238.除自身以外数组的乘积
题目描述 :给你数组 nums,返回 res,其中 res[i] 等于 nums 中除 nums[i] 之外所有元素的乘积 。要求不使用除法 、O(n) 时间。比如 [1,2,3,4] → [24,12,8,6]。数组可能含 0,正是"不让用除法"的原因------用总积除会遇到 0 除或除 0。
思路一:前后缀分解(左边乘积 × 右边乘积)
核心思想:
- 除自身以外的乘积天然拆成两半:
ans[i] = (i 左边所有数的积) × (i 右边所有数的积)------i自己被左右两半精确地排除在外。 - 左半用一个从左到右的扫描算前缀积,右半对称。关键的空间优化:前缀积直接写进
ans,第二趟从右往左扫时不再开数组,用一个变量suf滚动携带"右边所有数的积",边走边乘进ans。额外空间压到O(1)(res不算)。 - 两趟都是纯乘法,全程不出现除法 ,0 不 0 的完全不关心------每个
res[i]都是从头乘出来的,没有"总积 ÷ 自己"这种会被 0 炸掉的操作。
时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,两趟扫描。
空间复杂度 : O ( 1 ) O(1) O(1),除返回数组外只用了 suf 一个变量。
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
ans[0] = 1; // ans[0] 左边没数,左边积是 1(乘法单位元)
// 第一趟从左到右:ans[i] = nums[0..i-1] 的积(i 左边的全部)
for (int i = 1; i < n; i++) {
ans[i] = ans[i - 1] * nums[i - 1]; // 前缀积递推,i 自己故意不乘进去
}
// 第二趟从右到左:suf = nums[i+1..n-1] 的积(i 右边的全部),滚动维护不开数组
int suf = 1;
for (int i = n - 1; i >= 0; i--) {
ans[i] *= suf; // 左半 × 右半,nums[i] 两边都没碰它
suf *= nums[i]; // 把 nums[i] 计入后缀,供下一轮(i-1)使用
}
return ans;
}
}
这里借用k神题解里面的图会很好理解:k神题解

前后缀分解是个通用套路,Hot 100 里反复出现:42.接雨水 思路二的
leftMax/rightMax是它(一侧最大值版),这题是"一侧乘积"版。凡是res[i]只由"i 左边的聚合"和"i 右边的聚合"拼出来、又不能包含i自己的题,先想前后缀。
41.缺失的第一个正数
题目链接 :41.缺失的第一个正数
题目描述 :给你一个未排序的整数数组 nums(元素可正可负可为 0,可能有重复),找出没有出现在数组中的最小正整数 。要求 O(n) 时间、O(1) 空间。比如 [1,2,0] 答案是 3,[3,4,-1,1] 答案是 1,[7,8,9,11,12] 答案是 1。O(1) 空间这条限制是题眼:不许开哈希表、不许排序,逼你把数组自己当哈希表用。
思路一:原地哈希(置换回家,谁家没主人谁缺失)
核心思想:
- 先把答案的搜索范围框死:长度为
n的数组,答案一定落在[1, n+1]。n个位置最多装下1..n这n个正数,装齐了答案才是n+1;只要1..n里缺一个,答案就是那个缺的。于是≤ 0和> n的数全是无关人员,看都不用看。 - 既不能开哈希表又想知道"
1..n谁没来",就让数组自己当哈希表 :约定下标i的家放值i+1。遍历每个位置,只要nums[i]是合法住户(在[1, n]内)且还没到家(nums[nums[i]-1] != nums[i]),就把它换到它家去;换回来的新数如果是合法住户也没到家,接着送,直到当前位置来的无关人员/已就位者为止。 - 这样每个数最多被送回家一次(到家后
nums[nums[i]-1] == nums[i]永远成立,不会再换),总交换次数 ≤n,均摊O(n)。判"到家"用值相等而不是下标相等,天然跳过重复值------后来的重复住户发现家里已有同名主人,就不折腾了。 - 全部归位后扫一遍:第一个
nums[i] != i+1的位置,i+1就是答案;全对上了说明1..n齐了,答案是n+1。
时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度------内层 while 的总交换次数不超过 n(每个值最多被送回家一次),均摊到外层每轮 O(1)。
空间复杂度 : O ( 1 ) O(1) O(1),全程原地交换,数组自己就是哈希表。
java
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
//【不变量:处理完下标 i 后,原始数组前 i+1 个位置上出现过的、值在 [1,n] 内的数 v 都已在家(nums[v-1] == v);且到家永久成立------只有 v 自己会看 v-1 这个家,值相等判重会拒绝一切打扰】
for (int i = 0; i < n; i++) {
// nums[i] 是合法住户(值在 [1,n])且它家那位不是它自己,就送它回家
// 值相等判重:重复值发现家里已有同名主人就停手,避免死循环
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
int j = nums[i] - 1; // nums[i] 的家
int tmp = nums[j];
nums[j] = nums[i]; // 送 nums[i] 回家
nums[i] = tmp; // 换回来的数留在 i 处,下一轮继续送
}
}
// 找第一个下标与住户编号不匹配的就是所要找的数
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) { // 下标 i 的家(该放 i+1)空着
return i + 1; // 第一个空的家,主人就是缺失的最小正数
}
}
return n + 1; // 1..n 全齐,只能往 n+1 找
}
}
高频出错点:
while条件若写成nums[nums[i]-1] != nums[i]之外的下标判断(比如记i是否处理过),遇到重复值会死循环 ------[1,1]里第二个1永远想回家但家里已有1。判"到家"必须用值相等 ,重复值停在原地即可。另外先算j = nums[i] - 1存下来再交换:先动nums[i]再用nums[i]-1当下标,取到的已是换进来的新数。
思路二:打标记(正负号当 BitSet 用)
核心思想 :同样吃"1..n 框定答案范围"这个洞察,但不动粗换位置,改就地做标记:
- 第一遍把无关人员(
≤ 0)统一改成n+1(清场); - 第二遍遇到值
v ∈ [1, n],就把下标v-1上的数标成负数 ------"v来过了"; - 第三遍找第一个还是正数的下标
i,i+1就是缺失者。用正负号当 bit、下标当桶,数组身兼数值和标记位两职。比置换好想一点,但要扫三遍、且改写原始数据的方式更粗暴。
时间复杂度 : O ( n ) O(n) O(n),其中 n 为数组长度,三趟扫描。
空间复杂度 : O ( 1 ) O(1) O(1),原地打标记。
java
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
// 第一步:预处理,把非正整数换成n+1(超出范围,不影响标记)
if (nums[i] <= 0) {
nums[i] = n + 1; // 清场:无关人员统一改成 n+1,后面只会对 [1,n] 打标记
}
}
// 第二步:标记存在性(逻辑归位)
for (int i = 0; i < n; i++) {
int v = Math.abs(nums[i]); // 取绝对值:标记可能已被之前的操作改成负数【补上缺失的取绝对值!】
if (v <= n) { // 只处理1~n
// 把v对应的位置标记为负数,表示v存在(逻辑归位)
nums[v - 1] = -Math.abs(nums[v - 1]); // 把 v 对应的桶标成负:"v 来过了"
}
}
// 第三步:找第一个未标记的位置(正数)
for (int i = 0; i < n; i++) {
if (nums[i] > 0) { // 桶 i 没被打过标记 → i+1 从没出现过
return i + 1;
}
}
// 所有1~len都存在,返回len+1
return n + 1; // 1..n 全打过标记
}
}
两版怎么选:置换版是这题的"官方灵魂"(一次把数送到位,扫完即答案),打标记版胜在不用处理换回来的接力数 、循环都是平的。笔试求稳选打标记(不容易死循环),面试先讲置换("原地哈希"这个名头更响)。共同的前提洞察只有一个:长度 n 框死了答案 ∈ 1, n+1。
总结:普通数组五题是五种"把
O(n²)暴力压到O(n)的手法"------53.最大子数组和 靠以 i 结尾的dp(负前缀舍弃)把枚举终点变成转移一步;56.合并区间 靠排序换有序性 (左端点有序后只需和结果尾比较,重叠判断从"任意对"变"相邻对");189.轮转数组 靠翻转等价换块 (数学性质替代额外空间);238.除自身以外数组的乘积 靠前后缀分解 (i的答案 = 左聚合 × 右聚合,避开除法和包含自己的问题);41.缺失的第一个正数 靠原地哈希 (长度 n 框死答案 ∈[1, n+1],把数组当下标哈希表,置换回家或打标记找缺失)。核心一句话:数组题先找"只看一边/相邻就能决策"的结构,找到了O(n)就到手。