53、56、189 三题:每题按①题目分析 & 解题思路 / ②常见解法(附代码)及讲解 / ③知识点总结。
一、53. 最大子数组和

① 题目分析 & 解题思路
题目理解
给定整数数组,找连续的子数组,使得子数组所有元素相加的和最大,返回这个最大值。子数组至少有 1 个元素。
重点:连续,不能打乱元素顺序;数组存在负数。
思路方向
- 暴力枚举 :枚举起点
i向后累加每个j,记录每段连续子数组的和并更新最大值;双重循环 O(n²),长度 10⁵ 时超时。 - 动态规划(Kadane 卡登算法,最优)O(n) :定义
dp[i]为以nums[i]结尾的最大连续子数组和,dp[i] = max(nums[i], dp[i-1]+nums[i]):单独取当前元素或接到前面子数组后面,维护全局最大值。 - 进阶:分治法 O(n log n)。
② 常见解法及讲解
解法 1:Kadane 算法(动态规划,推荐,O(n),面试首选)
核心思想:遍历数组,维护 pre,表示以当前元素结尾的最大子数组和 :pre = max(nums[i], pre + nums[i])。
pre + nums[i]更大则接在前面子数组后面;否则前面拖累总和,从当前元素重新开始。
空间 O(1),一次遍历,能通过 10⁵ 的数据。
java
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0;
int maxAns = nums[0];
for (int x : nums) {
pre = Math.max(x, pre + x);
maxAns = Math.max(maxAns, pre);
}
return maxAns;
}
}
解法 2:暴力枚举(O(n²),只适合学习,数据超时)
枚举所有起点,向后连续累加,记录最大值。适合理解题目,不适合本题大数据。
java
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
int max = Integer.MIN_VALUE;
for (int i = 0; i < len; i++) {
int sum = 0;
for (int j = i; j < len; j++) {
sum += nums[j];
if (sum > max) {
max = sum;
}
}
}
return max;
}
}
解法 3:分治法(进阶要求,O(n log n))
思路:把数组分成左右两部分,最大子数组只有 3 种情况:
- 完全在左半区间;
- 完全在右半区间;
- 跨越中点,一部分在左,一部分在右。
递归分别求左最大值、右最大值、跨中点最大值,三者取最大。
java
class Solution {
public int maxSubArray(int[] nums) {
return getMax(nums, 0, nums.length - 1);
}
int getMax(int[] nums, int l, int r){
if(l == r) return nums[l];
int mid = (l + r)/2;
int leftMax = getMax(nums, l, mid);
int rightMax = getMax(nums, mid+1, r);
// 跨中点最大值
int crossMax = getCrossMax(nums,l,mid,r);
return Math.max(Math.max(leftMax,rightMax), crossMax);
}
int getCrossMax(int[] nums, int l, int mid, int r){
int leftSum = Integer.MIN_VALUE;
int sum = 0;
for(int i = mid; i >= l; i--){
sum += nums[i];
leftSum = Math.max(leftSum, sum);
}
int rightSum = Integer.MIN_VALUE;
sum = 0;
for(int i = mid+1; i <= r; i++){
sum += nums[i];
rightSum = Math.max(rightSum, sum);
}
return leftSum + rightSum;
}
}
③ 知识点总结
- 面试优先写 Kadane 算法,代码简短、效率最高;
- 暴力法用来理解题意,但数据量大就超时;
- 分治法是题目进阶要求,理解思路即可。
二、56. 合并区间

① 题目分析 & 解题思路
题目理解
给若干区间 [start, end],把所有重叠/相接的区间合并,最后返回不重叠的区间数组。
重叠判断规则:前一个区间的结束 >= 当前区间的起始,就需要合并。比如 [1,4] 和 [4,5],4>=4,需要合并成 [1,5]。关键点:输入区间不一定有序 ,像示例 3 输入 [[4,7],[1,4]],必须先按照区间的起点 start 从小到大排序,才能方便遍历合并。
整体思路
- 排序 :把所有区间按照区间左端点
start升序排列。排序之后区间按起点从小到大排好,只需依次比较相邻区间。 - 遍历合并 :新建结果集合。
- 先把排序后的第一个区间放入结果;
- 依次取后面每一个区间,拿结果里最后一个区间 和当前区间对比:
- 如果
结果最后区间的 end >= 当前区间 start:重叠,合并,更新结果最后区间的 end 为两者 end 的最大值; - 否则:不重叠,直接把当前区间加入结果集合。
- 如果
- 遍历结束,返回结果。
② 常见解法及讲解
解法:排序 + 一次遍历(标准解法,面试首选)
时间复杂度 O(n log n),主要消耗在排序;遍历是 O(n)。空间复杂度 O(log n)(排序栈空间,不算结果存储)。
java
import java.util.*;
class Solution {
public int[][] merge(int[][] intervals) {
// 1.按区间起点start从小到大排序
Arrays.sort(intervals, (a,b)->a[0]-b[0]);
// 结果列表
List<int[]> res = new ArrayList<>();
// 放入第一个区间
res.add(intervals[0]);
for(int i = 1; i < intervals.length; i++){
// 拿到结果里最后一个区间
int[] last = res.get(res.size()-1);
int curStart = intervals[i][0];
int curEnd = intervals[i][1];
if(last[1] >= curStart){
// 重叠,合并:更新末尾区间的右端点,取更大的end
last[1] = Math.max(last[1], curEnd);
}else{
// 不重叠,直接加入
res.add(intervals[i]);
}
}
// list转二维数组返回
return res.toArray(new int[res.size()][]);
}
}
拿示例 1 推演一遍:intervals = [[1,3],[2,6],[8,10],[15,18]]
- 排序后不变:
[[1,3],[2,6],[8,10],[15,18]]; res初始:[[1,3]];- i=1,当前区间
[2,6],last=[1,3],last[1]=3 >= curStart=2→ 重叠,last[1]=max(3,6)=6,res变成[[1,6]]; - i=2,当前区间
[8,10],last=[1,6],6<8 → 不重叠,add,res:[[1,6],[8,10]]; - i=3,当前区间
[15,18],last=[8,10],10<15 → 不重叠,add,res:[[1,6],[8,10],[15,18]],结束。
边界测试示例 2:[[1,4],[4,5]]
排序不变。res 初始 [[1,4]];i=1,curStart=4,last[1]=4,4>=4,合并,last[1]=max(4,5)=5 → [1,5]。
边界测试示例 3:[[4,7],[1,4]]
排序后变成 [[1,4],[4,7]],然后合并成 [1,7]。
易错点总结
- ❌ 忘记排序!输入区间顺序是乱的,不排序会直接判断错误。
- ❌ 合并右端点直接赋值成 curEnd,必须取 max 。例
[1,5]和[2,3],合并后右端点仍然是 5,不是 3。 - ❌ 判断条件写错,写成
last[1] > curStart,漏掉等于的情况,[1,4] [4,5]就不会合并。
③ 知识点总结
1. 贪心思想
排序后局部最优得到全局最优。
2. List<int\[\]> 与 int\[\]\[\] 的区别(以合并区间 Java 代码为例)
本题 List<int[]> res = new ArrayList<>(); 是集合 List,里面存的元素是一维数组 int[] ;int[][] intervals 是二维数组。
本质区别
int[][]二维数组:固定长度,连续内存 。数组里的每个元素又是一个int[]一维数组;长度创建后不能改变、不能直接新增,取值arr[i][0]下标访问。
plain
int[][] arr = new int[4][2]; // 固定4行,每行2个int。不能直接add一行
ArrayList<int[]>(List):动态长度,集合容器 。List 是容器,里面存放int[]一维数组;动态扩容、随时add,适合输出长度不确定的场景;取值res.get(i)[0]:先 get 第 i 个一维数组,再取下标。
plain
List<int[]> res = new ArrayList<>();
res.add(new int[]{1,6}); // 随时追加一行区间
对比表(结合合并区间场景)
| 特性 | 二维数组 int[][] |
List<int[]>(ArrayList) |
|---|---|---|
| 长度 | 创建时固定,不能直接新增 | 动态可变,add() 随时追加元素 |
| 适用场景 | 元素数量提前确定 | 结果个数未知(合并区间!) |
| 获取元素 | arr[i][0] |
res.get(i)[0] |
| 添加元素 | 不能直接 add;要新建数组拷贝 | res.add(一维数组),很方便 |
| 转为对方 | List → 二维数组:res.toArray(new int[res.size()][]) |
二维数组转 List:循环 add 每一行 |
回到【合并区间】为什么用 List<int\[\]>
结果个数事先不知道,无法预先创建 int[][],先用 List<int[]> 动态 add,最后一次性转成二维数组返回:
java
return res.toArray(new int[res.size()][]);
toArray(new int[res.size()][]) 的作用:把 List 里面所有 int[] 打包,生成一个二维数组 int[][],刚好匹配题目要求的返回类型。
举个直观小例子
java
// 二维数组(输入)
int[][] intervals = {{1,3},{2,6}};
// List<int[]>(用来存结果)
List<int[]> res = new ArrayList<>();
res.add(new int[]{1,6});
// 转二维数组返回
int[][] ans = res.toArray(new int[res.size()][]);
存放方式:一行一个独立一维数组
java
List<int[]> res = new ArrayList<>();
// add的是【单个一维数组】,一行一行分开放进去
res.add(new int[]{1,6});
res.add(new int[]{8,10});
res.add(new int[]{15,18});
此时内存结构:
plain
List容器
├─ 第0个元素 → int[]数组 [1,6]
├─ 第1个元素 → int[]数组 [8,10]
└─ 第2个元素 → int[]数组 [15,18]
✅ 每一个区间单独存放 ,互相独立;List<int[]> 里面放的是一个个独立的一维数组 int[],不是一次性把完整二维数组塞进去。
错误写法(不要把二维数组直接 add 进去)
plain
int[][] temp = {{1,6},{8,10}};
res.add(temp); // ❌ 这时候List的元素是一个二维数组int[][],类型不匹配!编译报错
取值怎么取
plain
int[] last = res.get(res.size()-1); // get拿到【其中一个一维数组】
int end = last[1]; // 取这个一维数组的第二个元素
res.get(i) 拿到的是一行区间(一维数组),不是整个二维数组。
引用提醒
int[] last = res.get(res.size() - 1); 这里拿到的是引用,不是拷贝 。last 和 res 里面存的是同一个一维数组对象,所以修改 last[1],List 里面保存的区间会跟着一起变,这正是我们想要的效果。
容易混淆点
❗ List<int[]> 不是二维 List ,不是 List<List<Integer>>:
List<int[]>:集合里面放数组(原生 int 数组,效率高,本题用这个);List<List<Integer>>:集合套集合,里面存 Integer 包装类,LeetCode 部分题目输入用这个,读写慢一点。
总结 :二维数组 int[][] 长度写死,不能随便加行;List<int[]> 动态变长,可以不断 add 一维数组,最后再转二维数组。
通俗比喻
List<int[]>:一个收纳盒,盒子里放了好几张卡片,每张卡片就是一个一维数组[start,end],可以随时往盒子里新增卡片;int[][]:一本装订好的小册子,页数固定,不能随便插入新卡片。
3. 时间复杂度 O(n log n) 拆解
整段代码由排序 + 一次 for 循环 两部分组成,总复杂度取两者里更大的那一个。
两部分分开算
Arrays.sort(intervals, (a,b) -> a[0]-b[0])排序:O(n log n) 。底层是双轴快速排序,平均 O(n log n):n 个元素约需 n × log₂n 次比较,n=10000 时 log₂(10000)≈14,约 14 万次比较。- for 循环遍历:O(n)。循环 n-1 次,每次 get、取值、判断、max、add 均为常数操作 O(1),整段 O(n)。
总时间复杂度怎么合并
多个串行步骤取最高阶:O(n log n)+O(n)=O(n log n);n 很大时 n log n 增长远快于 n,O(n) 可忽略,整体由排序主导。
空间复杂度(面试常问)
O(log n):排序过程递归调用栈占用的空间。注意:存储答案的 List 一般不计入算法额外空间开销(题目必须要返回这个结果)。
容易混淆的点
能不能做到 O(n)?不行。前提是数组必须无序:如果输入区间完全乱序,必须排序,排序下界就是 O(n log n),无法避免;只有区间已经提前排好序的时候,遍历才是 O(n)。
复杂度增长对比
| n | n log n | n |
|---|---|---|
| 10 | ~33 | 10 |
| 100 | ~664 | 100 |
| 10000 | ~140000 | 10000 |
n 越大,n log n 比 n 拉开的差距越大。
总结:合并区间算法最慢的步骤是排序,排序是 O(n log n);后面遍历只是线性 O(n),所以整体时间复杂度是 O(n log n)。
三、189. 轮转数组

① 题目分析 & 解题思路
题目理解
把数组向右轮转 k 个位置 。示例:nums = [1,2,3,4,5,6,7], k=3 → 输出 [5,6,7,1,2,3,4]。含义:每个元素往右移动 k 位,超出数组末尾的元素,绕回到数组头部。
关键:
- 数组长度
len,当k >= len时,轮转len次等于没动,所以真实偏移量是k % len; - 本题函数要求原地修改原数组 nums ,
void无返回值,不能 return 数组!
② 常见解法及讲解
解法 1:额外数组法(最简单好理解)
原理:利用公式 newIdx = (i+k) % n 放到新数组,最后拷贝回原数组。优点:逻辑简单,不容易写错;缺点:需要额外数组,空间 O(n),不满足进阶原地 O(1) 要求。
java
class Solution {
public void rotate(int[] nums, int k) {
int len = nums.length;
int[] m = new int[len];
for (int i = 0; i < len; i++) {
int location = (i + k) % len;
m[location] = nums[i];
}
// 把临时数组复制回原数组
for(int i = 0; i < len; i++){
nums[i] = m[i];
}
// void函数,不要return任何内容
}
}
复杂度:时间 O(n),空间 O(n)(用了额外数组)。
解法 2:数组反转法(面试重点!原地 O(1) 空间)
核心思路:右旋转 k 次 = 三次反转。例子 [1,2,3,4,5,6,7], k=3:
- 整体反转全部数组 →
[7,6,5,4,3,2,1]; - 反转前 k 个元素 →
[5,6,7,4,3,2,1]; - 反转后面剩下元素 →
[5,6,7,1,2,3,4]。
注意:先做 k = k % nums.length,防止 k 大于数组长度。
java
class Solution {
public void rotate(int[] nums, int k) {
k = k % nums.length;
// 三次反转
reverse(nums,0,nums.length-1);
reverse(nums,0,k-1);
reverse(nums,k,nums.length-1);
}
// 翻转数组 [left,right]区间
void reverse(int[] nums, int left, int right){
while(left < right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
}
时间 O(n),空间 O(1),原地算法,满足进阶要求,面试优先背这个。
解法 3:循环替换(环状替换,原地算法)
把元素看成环,逐个放到目标位置,记录替换次数,全部替换完成就停止。理解难度偏高,面试一般写反转法即可。
③ 知识点总结
1. 重点总结
- 函数标记
public void→ 不能 return 任何数据,本题是修改原数组 nums; - 公式
(i+k)%len本身是对的,但是缺了把临时数组拷贝回 nums 的步骤; - 进阶要求原地,优先记住三次反转这个经典技巧。
2. reverse 为什么不加 public 修饰
不加 public 不是语法强制,可以加,但没必要,工程上不推荐 。LeetCode 刷题一般直接省略,用默认访问权限(包访问权限)。
Java 4 种访问权限
| 修饰符 | 访问范围 |
|---|---|
public |
任何地方都能调用(别的类、别的包) |
protected |
本类、子类、同包 |
| 默认(什么都不写) | 同一个包里面才可以访问 |
private |
只能当前类内部使用 |
我们的 reverse 只是 Solution 类里面的辅助函数,只在 rotate 里面自己调用,不会在别的类调用。
三种写法对比
✅ 刷题常用(默认权限,推荐):
plain
void reverse(int[] nums, int left, int right){
while(left < right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
✅ 语法上完全允许写 public,能 AC,但是没必要:
plain
public void reverse(int[] nums, int left, int right){
// ...
}
❌ 写 private 也可以,更严谨(只能本类内部访问,最规范):
plain
private void reverse(int[] nums, int left, int right){
// ...
}
为什么刷题大家习惯不写 public
public对外暴露内部工具方法,reverse 不需要暴露;- 判题器只调用 public 的
rotate,reverse 仅供本类内部使用; - 内部辅助方法应缩小访问范围,优先
private,偷懒可省略修饰符。
简单理解:
rotate是判题器外部调用的入口,必须 public;reverse 只是 rotate 叫来帮忙的小弟,只在类内干活,不对外开放。
容易踩坑小提醒
如果把 reverse 写成 private,完全没问题,推荐 ;写 public,代码能跑,只是不符合封装思想;千万不要写 protected,刷题很少用。
精简总结
rotate:必须public,因为 LeetCode 的代码调用它(外部调用);reverse:仅类内部调用,推荐 private,或者省略修饰符;写 public 语法没错,但没必要。