LeetCode Hot100 刷题笔记 · 普通数组专题

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


一、53. 最大子数组和

① 题目分析 & 解题思路

题目理解

给定整数数组,找连续的子数组,使得子数组所有元素相加的和最大,返回这个最大值。子数组至少有 1 个元素。

重点:连续,不能打乱元素顺序;数组存在负数。

思路方向

  1. 暴力枚举 :枚举起点 i 向后累加每个 j,记录每段连续子数组的和并更新最大值;双重循环 O(n²),长度 10⁵ 时超时。
  2. 动态规划(Kadane 卡登算法,最优)O(n) :定义 dp[i] 为以 nums[i] 结尾的最大连续子数组和,dp[i] = max(nums[i], dp[i-1]+nums[i]):单独取当前元素或接到前面子数组后面,维护全局最大值。
  3. 进阶:分治法 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 种情况:

  1. 完全在左半区间;
  2. 完全在右半区间;
  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 从小到大排序,才能方便遍历合并。

整体思路

  1. 排序 :把所有区间按照区间左端点 start 升序排列。排序之后区间按起点从小到大排好,只需依次比较相邻区间。
  2. 遍历合并 :新建结果集合。
    • 先把排序后的第一个区间放入结果;
    • 依次取后面每一个区间,拿结果里最后一个区间 和当前区间对比:
      • 如果 结果最后区间的 end >= 当前区间 start:重叠,合并,更新结果最后区间的 end 为两者 end 的最大值;
      • 否则:不重叠,直接把当前区间加入结果集合。
  3. 遍历结束,返回结果。

② 常见解法及讲解

解法:排序 + 一次遍历(标准解法,面试首选)

时间复杂度 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. 排序后不变:[[1,3],[2,6],[8,10],[15,18]];
  2. res 初始:[[1,3]];
  3. i=1,当前区间 [2,6],last=[1,3],last[1]=3 >= curStart=2 → 重叠,last[1]=max(3,6)=6,res 变成 [[1,6]];
  4. i=2,当前区间 [8,10],last=[1,6],6<8 → 不重叠,add,res:[[1,6],[8,10]];
  5. 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]。

易错点总结

  1. ❌ 忘记排序!输入区间顺序是乱的,不排序会直接判断错误。
  2. ❌ 合并右端点直接赋值成 curEnd,必须取 max 。例 [1,5] 和 [2,3],合并后右端点仍然是 5,不是 3。
  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 循环 两部分组成,总复杂度取两者里更大的那一个。

两部分分开算
  1. 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 万次比较。
  2. 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 位,超出数组末尾的元素,绕回到数组头部。

关键:

  1. 数组长度 len,当 k >= len 时,轮转 len 次等于没动,所以真实偏移量是 k % len;
  2. 本题函数要求原地修改原数组 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:

  1. 整体反转全部数组 → [7,6,5,4,3,2,1];
  2. 反转前 k 个元素 → [5,6,7,4,3,2,1];
  3. 反转后面剩下元素 → [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. 重点总结

  1. 函数标记 public void → 不能 return 任何数据,本题是修改原数组 nums;
  2. 公式 (i+k)%len 本身是对的,但是缺了把临时数组拷贝回 nums 的步骤;
  3. 进阶要求原地,优先记住三次反转这个经典技巧。

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
  1. public 对外暴露内部工具方法,reverse 不需要暴露;
  2. 判题器只调用 public 的 rotate,reverse 仅供本类内部使用;
  3. 内部辅助方法应缩小访问范围,优先 private,偷懒可省略修饰符。

简单理解:

  • rotate 是判题器外部调用的入口,必须 public;reverse 只是 rotate 叫来帮忙的小弟,只在类内干活,不对外开放。
容易踩坑小提醒

如果把 reverse 写成 private,完全没问题,推荐 ;写 public,代码能跑,只是不符合封装思想;千万不要写 protected,刷题很少用。

精简总结
  • rotate:必须 public,因为 LeetCode 的代码调用它(外部调用);
  • reverse:仅类内部调用,推荐 private,或者省略修饰符;写 public 语法没错,但没必要。
相关推荐
职豚求职小程序刷题1 小时前
国投证券笔试测评题型揭秘
笔记
q27551300421 小时前
AS8132 DP转HDMI 4K60(420)被动式 国产性价比AS方案
linux·算法·硬件架构·dash
安全指北针1 小时前
密态计算:数据 “可用不可见” 的底层技术解析
人工智能·算法
Jmyd01232 小时前
3D 扫描仪选型技术拆解:精度、点云与软件链路
人工智能·算法·3d·三维数字化
xxxiugou1232 小时前
链表算法题精讲:从两数相加到K组翻转
数据结构·c++·算法·链表
梦帮科技2 小时前
量子纠缠网络与量子隐形传态攻防:量子中继器信道串扰、纠缠态保真度降解与量子路由劫持
开发语言·网络·人工智能·算法·自动化·php
老王IT2 小时前
C语言语法基础语法介绍
c语言·开发语言·算法
cfm_29142 小时前
Leaf-segment号段模式
java
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 发布圈子动态:数据库表和接口文档介绍
java·数据库·交友