算法刷题笔记:数组和矩阵的套路,看这篇就够了
第二篇:数组与矩阵
大家好,这是算法专栏的第二篇!今天我们一起来搞定数组和矩阵里最经典的题目。放心,我会把每道题的思路掰开揉碎讲,保证你看完也能自己写出来。
1. 最大子数组和
题目
给你一个整数数组 nums,找一个具有最大和的连续子数组,返回其最大和。
- 示例:
[-2,1,-3,4,-1,2,1,-5,4]→ 输出6(子数组[4,-1,2,1]的和最大)
思路过程
第一步:暴力解法(容易想到,但会超时)
// 枚举所有子数组的起点和终点
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int sum = 0;
for (int k = i; k <= j; k++) {
sum += nums[k];
}
max = Math.max(max, sum);
}
}
问题:时间复杂度 O(n³),数据量大必超时。
第二步:优化到 O(n²)
// 用前缀和思想,内层循环用一个变量累加
for (int i = 0; i < n; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
sum += nums[j]; // 累加当前子数组和
max = Math.max(max, sum);
}
}
问题:虽然比暴力好一点,但还是太慢。
第三步:Kadane 算法(最优解!)
核心思想:负数只会拖后腿,及时止损!
想象你是个老板,开始攒钱(累加和)。如果累加和变成负数了,说明前面的子数组全是拖油瓶,不如从当前元素重新开始。
// 用一个变量 cur 记录"当前连续子数组的和"
int cur = nums[0]; // 初始化为第一个元素
int max = nums[0]; // 全局最大和
for (int i = 1; i < nums.length; i++) {
// 两种选择:延续之前的子数组,或者从当前元素重新开始
cur = Math.max(cur + nums[i], nums[i]);
// 更新全局最大值
max = Math.max(max, cur);
}
return max;
手动模拟一遍:
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
i=0: cur=-2, max=-2 (只有自己)
i=1: cur=max(-2+1, 1)=1, max=1 (前面的负数不要了,从1重新开始)
i=2: cur=max(1-3, -3)=-2, max=1
i=3: cur=max(-2+4, 4)=4, max=4 (到4这里爆发了!)
i=4: cur=max(4-1, -1)=3, max=4
i=5: cur=max(3+2, 2)=5, max=5
i=6: cur=max(5+1, 1)=6, max=6 ← 最终答案!
i=7: cur=max(6-5, -5)=1, max=6
i=8: cur=max(1+4, 4)=5, max=6
代码实现
class Solution {
public int maxSubArray(int[] nums) {
// 边界处理
if (nums == null || nums.length == 0) {
return 0;
}
int cur = nums[0]; // 当前连续子数组的最大和
int max = nums[0]; // 全局最大和
// 从第二个元素开始遍历
for (int i = 1; i < nums.length; i++) {
// 状态转移:要么继续累加,要么从当前元素重新开始
// 如果 cur 是负数,加上当前元素只会更小,所以不如重新开始
cur = Math.max(cur + nums[i], nums[i]);
// 记录遍历过程中的最大和
max = Math.max(max, cur);
}
return max;
}
}
复杂度分析
- 时间复杂度:O(n),只遍历一遍数组
- 空间复杂度:O(1),只用了两个变量
一句话总结
遇到负数就"断舍离",及时放弃拖累你的累加和,从头再来的贪心策略就是 Kadane 算法的精髓。
2. 合并区间
题目
给一些闭合区间,合并所有重叠的区间。
- 示例:
[[1,3],[2,6],[8,10],[15,18]]→[[1,6],[8,10],[15,18]]
思路过程
拿到题目先想:怎么判断两个区间重叠?
- 区间 [a, b] 和 [c, d] 重叠 ⟺
c <= b(第二个区间的起点小于等于第一个区间的终点)
关键步骤:排序! 把区间按起点从小到大排序,这样重叠的区间就会排在一起,处理起来就简单多了。
// 1. 按起点排序
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
// 2. 遍历合并
int[] cur = intervals[0]; // 当前合并后的区间
for (int i = 1; i < intervals.length; i++) {
if (cur[1] >= intervals[i][0]) {
// 重叠了!合并:取终点较大的那个
cur[1] = Math.max(cur[1], intervals[i][1]);
} else {
// 不重叠,把当前区间加入结果,cur 更新为新区间
result.add(cur);
cur = intervals[i];
}
}
result.add(cur); // 别忘了最后一个
代码实现
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// 1. 按区间起点升序排序
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
// 用于存储合并后的区间
List<int[]> merged = new ArrayList<>();
// 2. 用一个变量记录当前合并区间
int[] cur = intervals[0];
merged.add(cur);
// 3. 遍历剩余区间
for (int i = 1; i < intervals.length; i++) {
int start = intervals[i][0];
int end = intervals[i][1];
// 如果当前区间起点 <= 合并区间的终点,说明重叠了
if (cur[1] >= start) {
// 合并:更新终点为较大值
cur[1] = Math.max(cur[1], end);
} else {
// 不重叠,开始新的合并区间
cur = intervals[i];
merged.add(cur);
}
}
return merged.toArray(new int[0][]);
}
}
复杂度分析
- 时间复杂度:O(n log n),排序是主要开销
- 空间复杂度:O(n),存储结果
一句话总结
先按起点排序,再用贪心逐个合并重叠区间——简单来说就是"排好队,扎堆处理"。
3. 轮转数组
题目
把数组元素向右轮转 k 个位置。
- 示例:
[1,2,3,4,5,6,7], k=3→[5,6,7,1,2,3,4]
思路过程
暴力解法: 创建新数组,把每个元素放到目标位置。时间 O(n),空间 O(n)。
能不能原地搞?试试三次翻转!
核心思想:把数组分成两部分,先整体翻转,再局部翻转。
以 [1,2,3,4,5,6,7], k=3 为例:
原始: [1,2,3,4,5,6,7]
目标: [5,6,7,1,2,3,4]
观察规律:
- 末尾 3 个 [5,6,7] 跑到前面了
- 前 4 个 [1,2,3,4] 跑到后面去了
翻转技巧:
1. 整体翻转: [7,6,5,4,3,2,1]
2. 前k个翻转: [5,6,7,4,3,2,1]
3. 后n-k翻转: [5,6,7,1,2,3,4] ← 完成!
代码实现
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k = k % n; // 防止 k 超过数组长度
if (k == 0) return; // 不用旋转
// 1. 整体翻转
reverse(nums, 0, n - 1);
// 2. 前 k 个翻转
reverse(nums, 0, k - 1);
// 3. 后 n-k 个翻转
reverse(nums, k, n - 1);
}
// 翻转数组指定区间的辅助方法
private 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),原地操作
一句话总结
三次翻转魔术:先打乱再局部还原,就能把末尾 k 个元素神奇地搬到前面来。
4. 除自身以外数组的乘积
题目
给你一个数组,返回一个新数组,其中每个位置是除自身以外所有元素的乘积。
- 要求:不能用除法
- 示例:
[1,2,3,4]→[24,12,8,6](分别是 2×3×4, 1×3×4, 1×2×4, 1×2×3)
思路过程
暴力解法: 对每个元素,遍历其他所有元素相乘。O(n²),太慢。
更好的思路:前后缀分解
对于位置 i,答案 = (i 左边所有元素的乘积) × (i 右边所有元素的乘积)
举例:[1, 2, 3, 4]
位置 0: 左边乘积=1(无) 右边乘积=2×3×4=24 → 24
位置 1: 左边乘积=1 右边乘积=3×4=12 → 12
位置 2: 左边乘积=1×2=2 右边乘积=4=4 → 8
位置 3: 左边乘积=1×2×3=6 右边乘积=1(无) → 6
空间优化:不用额外数组
结果数组先存前缀积,再反向遍历乘上后缀积。
int[] result = new int[n];
// 第一遍:计算前缀积(左边所有元素的乘积)
int left = 1;
for (int i = 0; i < n; i++) {
result[i] = left; // 先存,乘积不包括当前元素
left *= nums[i]; // 更新,为下一个元素准备
}
// 第二遍:反向遍历,乘上后缀积(右边所有元素的乘积)
int right = 1;
for (int i = n - 1; i >= 0; i--) {
result[i] *= right; // 左边乘积 × 右边乘积 = 最终答案
right *= nums[i]; // 更新,为上一个元素准备
}
代码实现
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] result = new int[n];
// ========== 第一步:计算每个位置左边的乘积 ==========
int left = 1;
for (int i = 0; i < n; i++) {
// result[i] 先存左边乘积(不包含自己)
result[i] = left;
// 更新 left,为下一个元素准备
left *= nums[i];
}
// ========== 第二步:反向遍历,乘上右边的乘积 ==========
int right = 1;
for (int i = n - 1; i >= 0; i--) {
// 左边乘积 × 右边乘积 = 最终答案
result[i] *= right;
// 更新 right,为上一个元素准备
right *= nums[i];
}
return result;
}
}
复杂度分析
- 时间复杂度:O(n),遍历两遍
- 空间复杂度:O(1)(结果数组不计入额外空间)
一句话总结
把"除自身外的乘积"拆成"左边乘积 × 右边乘积",两次遍历就能搞定。
5. 缺失的第一个正数
题目
给你一个未排序的整数数组,找出缺失的最小正整数。
- 示例:
[3,4,-1,1]→2(1 在数组中,但 2 缺失) - 示例:
[7,8,9,11,12]→1(1 不在数组中)
思路过程
关键洞察:
对于长度为 n 的数组,缺失的第一个正数一定在 [1, n+1] 范围内。
- 如果 1~n 都在数组中,答案就是 n+1
- 如果有缺失,答案在 1~n 之间
原地哈希:把数组当成哈希表用!
核心思想:如果数字 x 出现,就把它放到下标 x-1 的位置(前提是 x 在 1~n 范围内)。
for (int i = 0; i < n; i++) {
// 如果 nums[i] 在 [1, n] 范围内
// 就把它放到正确位置:交换到 nums[nums[i]-1] 处
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
swap(nums, i, nums[i] - 1);
}
}
// 最后再遍历一遍,第一个 nums[i] != i+1 的位置就是答案
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return n + 1; // 1~n 都存在
代码实现
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
// 核心思想:把数字 x 放到下标 x-1 的位置
for (int i = 0; i < n; i++) {
// 不断交换,直到当前数字在正确位置,或者超出范围
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
// 交换 nums[i] 和 nums[nums[i] - 1]
int correctPos = nums[i] - 1;
int temp = nums[i];
nums[i] = nums[correctPos];
nums[correctPos] = temp;
}
}
// 再次遍历,找到第一个不匹配的位置
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1; // 缺失的第一个正数
}
}
// 如果 1~n 都存在,答案就是 n+1
return n + 1;
}
}
复杂度分析
- 时间复杂度:O(n),每个数字最多交换两次
- 空间复杂度:O(1),原地操作
一句话总结
用原地哈希把数字放到"本该属于它的位置",最后扫一遍看谁"不在家"——那个空位+1就是答案。
6. 矩阵置零
题目
如果矩阵中某个元素为 0,则把该元素所在行和列全部置为 0。要求原地修改。
思路过程
朴素想法:用两个数组标记
boolean[] rows = new boolean[m];
boolean[] cols = new boolean[n];
// 遍历找到所有 0,标记对应行和列
// 再遍历置零
问题:空间复杂度 O(m+n),能不能更省?
原地标记:用矩阵本身记录!
用第一行和第一列作为"标记位":
matrix[i][0] = 0表示第 i 行需要置零matrix[0][j] = 0表示第 j 列需要置零
注意事项: 第一行和第一列既是"标记位"也是"被标记对象",所以要先单独记录它们原本是否含 0。
// 1. 先记录第一行和第一列原本是否含 0
boolean row0HasZero = false, col0HasZero = false;
for (int j = 0; j < n; j++) if (matrix[0][j] == 0) row0HasZero = true;
for (int i = 0; i < m; i++) if (matrix[i][0] == 0) col0HasZero = true;
// 2. 用第一行和第一列标记其他位置
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0; // 标记行
matrix[0][j] = 0; // 标记列
}
}
}
// 3. 根据标记置零(跳过第一行和第一列)
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 4. 最后处理第一行和第一列
if (row0HasZero) Arrays.fill(matrix[0], 0);
if (col0HasZero) {
for (int i = 0; i < m; i++) matrix[i][0] = 0;
}
代码实现
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
// 记录第一行和第一列原本是否含 0
boolean firstRowZero = false;
boolean firstColZero = false;
// 检查第一行
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// 检查第一列
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// 用第一行和第一列标记其他需要置零的位置
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0; // 标记第 i 行
matrix[0][j] = 0; // 标记第 j 列
}
}
}
// 根据标记置零(从 1 开始,避免覆盖第一行/列的标记)
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 处理第一行和第一列
if (firstRowZero) {
for (int j = 0; j < n; j++) matrix[0][j] = 0;
}
if (firstColZero) {
for (int i = 0; i < m; i++) matrix[i][0] = 0;
}
}
}
复杂度分析
- 时间复杂度:O(m×n),多次遍历但总共还是线性
- 空间复杂度:O(1),只用了几个布尔变量
一句话总结
用矩阵的"边边"当记事本,把需要置零的行和列先记下来,最后再统一处理——聪明地复用空间。
7. 螺旋矩阵
题目
给你一个 m×n 的矩阵,按顺时针螺旋顺序返回所有元素。
- 示例:
[[1,2,3],[4,5,6],[7,8,9]]→[1,2,3,6,9,8,7,4,5]
思路过程
模拟遍历:设定边界,逐层收缩!
想象成"剥洋葱",用四个边界指针:
top、bottom:上下边界left、right:左右边界
每剥一圈,按"右→下→左→上"的顺序遍历一圈,然后边界向内收缩。
遍历顺序:
→→→→
← ↓
↑ ← ←
(每步后边界收缩)
终止条件:边界相遇
while (left <= right && top <= bottom) {
// 1. 从左到右遍历上边
for (int j = left; j <= right; j++) res.add(matrix[top][j]);
top++;
// 2. 从上到下遍历右边
for (int i = top; i <= bottom; i++) res.add(matrix[i][right]);
right--;
// 3. 从右到左遍历下边(要检查是否还有)
if (top <= bottom) {
for (int j = right; j >= left; j--) res.add(matrix[bottom][j]);
bottom--;
}
// 4. 从下到上遍历左边(要检查是否还有)
if (left <= right) {
for (int i = bottom; i >= top; i--) res.add(matrix[i][left]);
left++;
}
}
代码实现
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int m = matrix.length; // 行数
int n = matrix[0].length; // 列数
// 四个边界指针
int top = 0, bottom = m - 1;
int left = 0, right = n - 1;
while (left <= right && top <= bottom) {
// 1. 从左到右,遍历上边
for (int col = left; col <= right; col++) {
result.add(matrix[top][col]);
}
top++; // 上边界向下收缩
// 2. 从上到下,遍历右边
for (int row = top; row <= bottom; row++) {
result.add(matrix[row][right]);
}
right--; // 右边界向左收缩
// 3. 从右到左,遍历下边(需要确认还有剩余)
if (top <= bottom) {
for (int col = right; col >= left; col--) {
result.add(matrix[bottom][col]);
}
bottom--; // 下边界向上收缩
}
// 4. 从下到上,遍历左边(需要确认还有剩余)
if (left <= right) {
for (int row = bottom; row >= top; row--) {
result.add(matrix[row][left]);
}
left++; // 左边界向右收缩
}
}
return result;
}
}
复杂度分析
- 时间复杂度:O(m×n),每个元素访问一次
- 空间复杂度:O(1),除了输出数组
一句话总结
用四个边界指针"框住"当前层,顺时针绕一圈后边界收缩,层层剥洋葱直到全部遍历完。
8. 旋转图像
题目
把 n×n 矩阵顺时针旋转 90 度,要求原地修改。
- 示例:
[[1,2,3],[4,5,6],[7,8,9]]→[[7,4,1],[8,5,2],[9,6,3]]
思路过程
两步走:转置 + 翻转
观察旋转前后的对应关系:
旋转前: 旋转后:
1 2 3 7 4 1
4 5 6 → 8 5 2
7 8 9 9 6 3
可以发现:
- 原位置 (i,j) 旋转后到了 (j, n-1-i)
- 等价于:先转置(关于主对角线对称交换),再水平翻转
第一步:转置
1 2 3 1 4 7
4 5 6 → 2 5 8 (matrix[i][j] 和 matrix[j][i] 交换)
7 8 9 3 6 9
第二步:水平翻转
1 4 7 7 4 1
2 5 8 → 8 5 2 (每行首尾交换)
3 6 9 9 6 3
代码实现
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// ========== 第一步:转置(关于主对角线镜像)==========
// 只需要遍历上三角(不包括对角线)
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 交换 matrix[i][j] 和 matrix[j][i]
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// ========== 第二步:水平翻转(每行首尾交换)==========
for (int i = 0; i < n; i++) {
int left = 0, right = n - 1;
while (left < right) {
int temp = matrix[i][left];
matrix[i][left] = matrix[i][right];
matrix[i][right] = temp;
left++;
right--;
}
}
}
}
复杂度分析
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
一句话总结
旋转 90° = 转置(对角线镜像)+ 水平翻转(左右交换),两步搞定!
9. 搜索二维矩阵 II
题目
在一个行列分别升序的矩阵中搜索目标值。
- 示例:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
- 找 5 → true,找 20 → false
思路过程
关键观察:四个角的特点
| 位置 | 特点 | 能否作为起点 |
|---|---|---|
| 左上角 | 行最小、列最小 | ❌ 无法排除 |
| 右下角 | 行最大、列最大 | ❌ 无法排除 |
| 右上角 | 行最大、列最小 | ✅ 可以排除 |
| 左下角 | 行最小、列最大 | ✅ 可以排除 |
从右上角出发:
右上角元素是当前行的最大值、当前列的最小值:
- 如果
当前值 == target→ 找到! - 如果
当前值 > target→ 这一列都不行,排除列,向左 - 如果
当前值 < target→ 这一行都不行,排除行,向下
每次比较都能排除一整行或一整列!
int row = 0; // 从第一行开始
int col = n - 1; // 从最后一列开始(右上角)
while (row < m && col >= 0) {
if (matrix[row][col] == target) return true;
if (matrix[row][col] > target) {
col--; // 当前值太大,向左移动(排除这一列)
} else {
row++; // 当前值太小,向下移动(排除这一一行)
}
}
return false;
代码实现
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return false;
}
int m = matrix.length; // 行数
int n = matrix[0].length; // 列数
// 从右上角开始搜索
int row = 0;
int col = n - 1;
while (row < m && col >= 0) {
int current = matrix[row][col];
if (current == target) {
// 找到了!
return true;
} else if (current > target) {
// 当前值大于目标值,说明这一列都不行(因为列是升序的)
col--; // 向左移动
} else {
// 当前值小于目标值,说明这一行都不行(因为行是升序的)
row++; // 向下移动
}
}
return false;
}
}
复杂度分析
- 时间复杂度:O(m + n),最多走完一行 + 一列
- 空间复杂度:O(1)
一句话总结
右上角是"十字路口":比它大就向左走(排除一列),比它小就向下走(排除一行),每步都能排除一片区域。
10. 回文子串
题目
给定一个字符串,返回其中回文子串的个数。
- 示例:
"aaa"→6(分别是 a, a, a, aa, aa, aaa)
思路过程
中心扩展法:枚举每个可能的回文中心!
回文串的特点是"两边对称",所以可以从中心向两边扩展。
中心有两种情况:
- 单中心:
aba,中心是 'b' - 双中心:
aa,中心是两个 'a' 之间
枚举中心:
a b a
↑ ↑ ↑
| 中心 |
单中心(1个字符) 双中心(2个字符)
int count = 0;
for (int i = 0; i < s.length(); i++) {
// 1. 以单字符为中心向两边扩展
count += expandAroundCenter(s, i, i);
// 2. 以双字符为中心向两边扩展
count += expandAroundCenter(s, i, i + 1);
}
// 辅助函数:从中心向两边扩展
int expandAroundCenter(String s, int left, int right) {
int count = 0;
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
count++; //找到一个回文串
left--; //向左扩展
right++; //向右扩展
}
return count;
}
为什么这样不重不漏?
- 每个回文串都有唯一的"中心"
- 单中心回文串对应奇数长度
- 双中心回文串对应偶数长度
- 枚举所有中心,就能找到所有回文串
代码实现
class Solution {
public int countSubstrings(String s) {
int count = 0;
int n = s.length();
// 枚举每个位置作为回文中心
for (int i = 0; i < n; i++) {
// 情况1:以单字符为中心(奇数长度回文)
count += expandAroundCenter(s, i, i);
// 情况2:以双字符为中心(偶数长度回文)
count += expandAroundCenter(s, i, i + 1);
}
return count;
}
// 从中心向两边扩展,统计以 (left, right) 为中心的所有回文串
private int expandAroundCenter(String s, int left, int right) {
int count = 0;
// 满足条件:两边没越界,且字符相等(是回文)
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
count++; // 找到一个回文串,计数 +1
left--; // 向左扩展
right++; // 向右扩展
}
return count;
}
}
复杂度分析
- 时间复杂度:O(n²),每个中心最多扩展 O(n)
- 空间复杂度:O(1)
一句话总结
每个回文串都有一个"对称中心",枚举所有可能的中心(单个字符或两个字符之间),向两边扩展找所有回文。
总结
今天这些题覆盖了数组和矩阵的经典技巧:
| 题号 | 题目 | 核心技巧 |
|---|---|---|
| 53 | 最大子数组和 | Kadane 算法:负数断舍离 |
| 56 | 合并区间 | 先排序,再贪心合并 |
| 189 | 轮转数组 | 三次翻转魔术 |
| 238 | 除自身乘积 | 前后缀分解 |
| 41 | 缺失的第一个正数 | 原地哈希 |
| 73 | 矩阵置零 | 用边边做标记 |
| 54 | 螺旋矩阵 | 边界收缩模拟 |
| 48 | 旋转图像 | 转置+翻转 |
| 240 | 搜索二维矩阵 | 从右上角出发 |
| 647 | 回文子串 | 中心扩展法 |
下期预告:链表专题!跟着我一起刷算法题,咱们下期见!
如果觉得有用,点个赞再走呗~ 有问题评论区见!