LeetCode Hot 100 · 矩阵篇
矩阵类题目在面试手撕里属于「看着简单、一写就乱」的重灾区:边界下标、原地约束、遍历顺序,任何一处细节没想清楚就会写出死循环或越界。本分类四道题——73 矩阵置零、54 螺旋矩阵、48 旋转图像、240 搜索二维矩阵 II——分别训练四种高频套路:就地标记、边界收缩、转置翻转、Z 字搜索。吃透这四招,矩阵题基本可以横着走。
#73 矩阵置零
题意
给定一个 m×n 矩阵,若某个元素为 0,则把它所在的整行和整列全部置为 0,要求原地完成。
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
题目本质:利用第一行和第一列作为标记位,避免申请额外空间。先单独记录第一行/第一列本身是否含 0,再用第一行/列去记录「其余行列」的 0 信息,最后逆向处理。
现实类比:工厂质检标记——先在车间墙上(第一行/第一列)记下哪条流水线出了问题,全部排查完后再根据墙上的标记批量把对应行列清零。墙本身是否也要清零,得先单独留个纸条(r0/c0)记下来。
难点与易错点
- 不能边遍历边置零:如果发现
matrix[i][j]==0就立刻把第 i 行、第 j 列清零,你会把后面尚未检查的元素也抹成 0,于是「新产生的 0」被误当成「原本的 0」,导致把不该清的行列也清了。必须先完整标记,再统一清零。 - 第一行和第一列会被污染:一旦用
matrix[i][0]和matrix[0][j]当标记位,第一行/列原来的 0 信息就被覆盖了。所以必须先单独记录firstRowHasZero和firstColHasZero,放到最后再处理,顺序不能颠倒。 - 清零时要跳过第一行/列:根据标记清零内部区域时,循环从
1开始,避免破坏标记本身;否则标记还在后面要用时已经被抹掉。 - 边界情况:单行(m=1)或单列(n=1)矩阵,
matrix[0][0]同时属于第一行和第一列,标记逻辑仍成立但要多想一步;空矩阵需提前判空。
解法一:暴力 / 直观(标记数组)
用一个行数组 + 一个列数组记录哪些行/列需要清零,第二次遍历统一执行。
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean[] zeroRow = new boolean[m]; // 记录哪些行需要置零
boolean[] zeroCol = new boolean[n]; // 记录哪些列需要置零
// 第一次遍历:只标记,不动手清零
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
zeroRow[i] = true;
zeroCol[j] = true;
}
}
}
// 第二次遍历:根据标记统一置零
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (zeroRow[i] || zeroCol[j]) {
matrix[i][j] = 0;
}
}
}
}
}
复杂度:两次遍历各 O(m×n),总时间 O(m×n);空间 O(m+n) 存两个标记数组。
瓶颈:额外 O(m+n) 空间。题目要求「原地」,若面试官追问「能不能做到 O(1) 额外空间」,就需要把标记信息塞回矩阵自身。
解法二:优化 / 最优(首行首列当标记,O(1) 空间)
不额外开数组,把第一行、第一列复用成标记位,再用两个布尔变量记录边界本身的状态。
class Solution {
public void setZeroes(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
boolean firstRowHasZero = false; // 第一行本身是否含 0
boolean firstColHasZero = false; // 第一列本身是否含 0
// 检查第一列是否含 0
for (int i = 0; i < rows; i++) {
if (matrix[i][0] == 0) {
firstColHasZero = true;
break;
}
}
// 检查第一行是否含 0
for (int j = 0; j < cols; j++) {
if (matrix[0][j] == 0) {
firstRowHasZero = true;
break;
}
}
// 用第一行/列记录 [1..m-1][1..n-1] 范围内的 0 信息
for (int i = 1; i < rows; i++) {
for (int j = 1; j < cols; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0; // 第 i 行需要置零
matrix[0][j] = 0; // 第 j 列需要置零
}
}
}
// 根据第一列标记,将对应列置零(跳过第一行)
for (int j = 1; j < cols; j++) {
if (matrix[0][j] == 0) {
for (int i = 1; i < rows; i++) {
matrix[i][j] = 0;
}
}
}
// 根据第一行标记,将对应行置零(跳过第一列)
for (int i = 1; i < rows; i++) {
if (matrix[i][0] == 0) {
Arrays.fill(matrix[i], 0);
}
}
// 最后处理第一行/列本身
if (firstColHasZero) {
for (int i = 0; i < rows; i++) {
matrix[i][0] = 0;
}
}
if (firstRowHasZero) {
Arrays.fill(matrix[0], 0);
}
}
}
> **💭 思考**:O(1) 空间怎么记录「哪些行列要置零」?一步步想——暴力用两个标记数组是 O(m+n) 空间,题目要求原地。想省空间,只能把标记信息**塞回矩阵自身**:既然第一行和第一列本来就要被清零,不如复用它们当「标记位」,记录其余行列的 0 信息。但这就带来一个坑:第一行/列自己原来有没有 0,一旦被当标记位就会覆盖掉,所以必须先单独记下 `firstRowHasZero` / `firstColHasZero`,放到最后再处理。这个「复用已有位置当标记 + 先记边界本身」的思想,正是 O(1) 空间的关键。
复杂度逐步推导:
- 检查第一列:O(m) 步;
- 检查第一行:O(n) 步;
- 标记内部区域:遍历
(m-1)×(n-1)个元素,O((m-1)(n-1)); - 按标记清零列、清零行:各 O((m-1)(n-1));
- 最后处理第一列 O(m)、第一行 O(n)。
以上六段加起来,主项是 O(m×n),所以时间 O(m×n)。空间上,只申请了两个布尔变量,标记位复用了矩阵已有的第一行/第一列,额外空间 O(1)。
多解法对比
| 解法 | 时间 | 空间 | 适用场景 |
|---|---|---|---|
| 标记数组(暴力) | O(m×n) | O(m+n) | 思路直白,先写出来保底 |
| 首行首列标记(最优) | O(m×n) | O(1) | 面试要求原地、O(1) 空间的硬约束 |
本题约束明确要求「原地算法」,所以必须选 O(1) 空间的方案。暴力的时间其实已经无法再优化(每个元素至少要看一遍),能省的就是空间,而「复用矩阵自身的第一行第一列当标记」就是这题的核心考点——面试官等的就是这句。
CodeTop 变体
- 高频真实变体:「矩阵置零」是字节、美团手撕的高频题,最常追问的就是上面这道——从 O(m+n) 压到 O(1) 空间,以及「第一行第一列为什么要先单独记」。
- 同套路延伸题:LeetCode 289 生命游戏。同样是「就地标记 + 不能立即改动」的问题,用「状态位」(如用 2 表示『由活变死』、3 表示『由死变活』)避免额外空间,与本题「复用已有位置做标记」是同一思想的不同包装。
- 变体追问:若矩阵元素允许为「0 和 1 之外的任意整数」,无法靠「是否为 0」判断是原始 0 还是标记,就需要像 289 那样引入额外状态码(例如用极值
Integer.MIN_VALUE或偏移量)区分「原始 0」和「被标记的 0」。
#54 螺旋矩阵
题意
给定一个 m×n 矩阵,按顺时针螺旋顺序返回其中所有元素。
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
题目本质:四个边界 + 方向收缩问题。维护上下左右四条边界,依次遍历「上边 → 右边 → 下边 → 左边」,每遍历完一条边就收缩对应边界,直到边界交叉为止。
现实类比:蚊香盘绕——从最外圈开始顺时针绕,绕完一圈就把外圈「剥掉」,继续绕内圈,直到没有更多圈可绕。
难点与易错点
- 每遍历完一条边都要立刻检查边界:不检查就继续下一条边,在非方形矩阵(如 1×n、n×1、扁平矩阵)里会重复输出或越界。标准写法是每次移动边界后紧跟
if (++t > b) break;这类判断。 - 「收缩」的时机和方向:上边遍历完
t++(上边界下移),右边遍历完r--(右边界左移),下边b--,左边l++,四条边收缩方向不同,写反一条结果就错。 - 循环终止条件:不能用
l<=r && t<=b简单包一层 for,因为每走完一条边边界都可能交叉,必须边遍历边判断;否则会多输出最后一行/列。 - 空矩阵:
matrix.length == 0或matrix[0].length == 0要提前返回,否则访问matrix[0]越界。
解法一:暴力 / 直观(访问标记 + 方向模拟)
用方向数组模拟「右→下→左→上」的转向,配合 visited 数组判断是否该拐弯。思路最接近「人在格子里走」,不容易错,但占空间。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean[][] visited = new boolean[m][n];
List<Integer> res = new ArrayList<>();
// 方向顺序:右、下、左、上
int[] dr = {0, 1, 0, -1};
int[] dc = {1, 0, -1, 0};
int r = 0, c = 0, dir = 0; // 当前位置与当前方向
for (int k = 0; k < m * n; k++) {
res.add(matrix[r][c]);
visited[r][c] = true;
int nr = r + dr[dir], nc = c + dc[dir];
// 下一步越界或已访问,则顺时针转向
if (nr < 0 || nr >= m || nc < 0 || nc >= n || visited[nr][nc]) {
dir = (dir + 1) % 4;
nr = r + dr[dir];
nc = c + dc[dir];
}
r = nr;
c = nc;
}
return res;
}
}
复杂度:每个元素恰好访问一次,时间 O(m×n);visited 数组占 O(m×n)。
瓶颈:额外 O(m×n) 的访问标记数组。对于结果本身就是 O(m×n) 的题目,其实可以省掉这个数组,用「边界收缩」做到 O(1) 额外空间。
解法二:优化 / 最优(四边界收缩,O(1) 额外空间)
维护 l, r, t, b 四个边界,每走完一条边就收缩一条,边遍历边判越界。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int l = 0, r = matrix[0].length - 1; // 左右边界
int t = 0, b = matrix.length - 1; // 上下边界
int total = (r + 1) * (b + 1); // 元素总数
Integer[] result = new Integer[total];
int idx = 0;
while (true) {
// 遍历上边:从左到右
for (int i = l; i <= r; i++) result[idx++] = matrix[t][i];
if (++t > b) break; // 上边界下移,越界则结束
// 遍历右边:从上到下
for (int i = t; i <= b; i++) result[idx++] = matrix[i][r];
if (--r < l) break; // 右边界左移
// 遍历下边:从右到左
for (int i = r; i >= l; i--) result[idx++] = matrix[b][i];
if (--b < t) break; // 下边界上移
// 遍历左边:从下到上
for (int i = b; i >= t; i--) result[idx++] = matrix[i][l];
if (++l > r) break; // 左边界右移
}
return Arrays.asList(result);
}
}
复杂度逐步推导:
- 一圈四条边各遍历一次,整个过程中矩阵里的每个元素只被访问且仅被访问一次——因为每走完一条边就把那条边界收缩,已走过的元素不会再落到区间内;
- 因此总访问次数 = 元素总数 = m×n,时间 O(m×n);
- 额外空间只有
l, r, t, b, idx几个变量,结果数组属于题目要求的返回值,不计入额外空间,所以额外空间 O(1)。
多解法对比
| 解法 | 时间 | 空间 | 适用场景 |
|---|---|---|---|
| 方向模拟 + visited | O(m×n) | O(m×n) | 首次写、怕写乱边界时保底 |
| 四边界收缩(最优) | O(m×n) | O(1) | 面试标准答案,边界收缩是考点 |
本题时间上两个解法都是 O(m×n)(每个元素必须访问一次,无优化空间),比拼的就是空间与代码干净度。「四边界收缩」把空间压到 O(1),且是面试官最想看到的写法——它直观体现了「剥洋葱」的收缩思想。
CodeTop 变体
- 同套路延伸题(必刷):LeetCode 59 螺旋矩阵 II,给一个正整数 n,要求生成 n×n 螺旋矩阵。做法与本篇几乎对称:同样的四边界收缩,只是把「读」改成「写」。
- 进阶延伸:LeetCode 885 螺旋矩阵 III,从任意起点开始按螺旋展开,本质仍是方向数组 + 边界扩张,可用本篇的「方向模拟」思路扩展。
- 面试追问:字节、腾讯手撕常问「非方形矩阵(m≠n)会不会错」——考察你是否理解「每走完一条边必须判断边界交叉」这个关键点;还会追问「能否 O(1) 额外空间」,即把 visited 方案升级到边界收缩。
#48 旋转图像
题意
给定一个 n×n 的二维矩阵,将其顺时针旋转 90 度,要求原地修改,不能另开矩阵。
输入:[[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
题目本质:先沿主对角线转置,再水平翻转。转置让「行变列」,水平翻转完成顺时针 90° 旋转。两步都可原地完成,用异或交换避免临时变量。
现实类比:转动魔方——先沿对角线折叠(行列互换),再左右翻转,合起来就等价于把整个图像顺时针旋转了 90°。
难点与易错点
- 直接找下标映射容易晕:顺时针 90° 的映射是
matrix[i][j] → matrix[j][n-1-i],直接一轮轮套环旋转容易写错。「转置 + 翻转」两步把复杂映射拆成两个简单操作,是这题的精髓。 - 转置只遍历主对角线下三角:转置时内层循环必须
j < i(或j > i),只交换对角线一侧;如果j从 0 遍历到 n,会交换两次又换回来,白做。 - 水平翻转只遍历前半列:翻转每行时内层
j < n/2,只交换左右对称的元素;遍历到 n 会重复翻转回到原样。 - 异或交换的坑:
a ^= b; b ^= a; a ^= b在a和b是同一个元素(同一内存位置)时会把值清零。本解法里转置有j < i、翻转有j < n/2,保证两个位置必然不同,所以安全;但面试时要能讲出这个前提。 - 逆时针 vs 顺时针别搞混:本题是顺时针;若面试官让逆时针 90°,则是「先转置再垂直翻转」(或「先水平翻转再转置」),方向不同。
解法一:暴力 / 直观(辅助矩阵)
按顺时针映射公式,把结果写进一个新矩阵,再拷贝回原矩阵。
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
int[][] tmp = new int[n][n];
// 顺时针 90°:新位置 (i, j) 来自原位置 (n-1-j, i)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
tmp[i][j] = matrix[n - 1 - j][i];
}
}
// 拷贝回原矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
matrix[i][j] = tmp[i][j];
}
}
}
}
复杂度:一次映射 + 一次拷贝,各 O(n²),总时间 O(n²);辅助矩阵占 O(n²)。
瓶颈:额外 O(n²) 空间。题目要求「原地」,这个方案直接违反约束,只能作为推导映射公式的中间步骤,不能当最终答案。
解法二:优化 / 最优(转置 + 水平翻转,O(1) 空间)
先沿主对角线转置,再对每一行做水平翻转,全程原地。
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 第一步:沿主对角线转置(交换 matrix[i][j] 和 matrix[j][i])
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
// 异或交换,无需临时变量(i != j,两个位置必不同)
matrix[i][j] ^= matrix[j][i];
matrix[j][i] ^= matrix[i][j];
matrix[i][j] ^= matrix[j][i];
}
}
// 第二步:水平翻转每一行(左右镜像)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n / 2; j++) {
matrix[i][j] ^= matrix[i][n - 1 - j];
matrix[i][n - 1 - j] ^= matrix[i][j];
matrix[i][j] ^= matrix[i][n - 1 - j];
}
}
}
}
> **💭 思考**:为什么旋转 90° 要拆成「转置 + 水平翻转」两步,而不是直接套坐标映射?一步步想——直接映射 `matrix[i][j] → matrix[j][n-1-i]` 是对的,但一轮轮套环旋转极容易写错边界。而「转置」让行列互换、「水平翻转」完成左右镜像,这两步各自都是极简单的原地操作,合起来恰好等价于顺时针 90°——把一次复杂旋转拆成两个简单操作,边界清晰、不易翻车,这正是面试官想听到的「拆解」思路。
复杂度逐步推导:
- 转置只交换主对角线一侧的元素,共
n(n-1)/2次交换; - 水平翻转每行交换
n/2次,共 n 行,即n × n/2次交换; - 两者相加:
n(n-1)/2 + n²/2 = (n² - n + n²)/2 = n² - n/2,主项是 n²,所以时间 O(n²); - 整个过程中没有申请任何与 n 相关的额外空间,只用了循环变量,额外空间 O(1)。
多解法对比
| 解法 | 时间 | 空间 | 适用场景 |
|---|---|---|---|
| 辅助矩阵(暴力) | O(n²) | O(n²) | 推导下标映射、快速验证思路 |
| 转置 + 水平翻转(最优) | O(n²) | O(1) | 原地约束下的标准答案 |
本题的 n² 个元素每个都要动一次,时间 O(n²) 无法再降,真正的约束是原地。「转置 + 翻转」把一次复杂的坐标旋转,拆成两个各自简单、且都能原地完成的操作,空间降到 O(1),是面试官必问的标准解法。
CodeTop 变体
- 高频真实变体:字节、腾讯、美团手撕常问「原地旋转,不用额外矩阵」——核心就是「转置 + 翻转」两步分解;追问「为什么不能一次交换到位」或「异或交换在什么情况下会出 bug」。
- 同套路延伸题:LeetCode 48 逆时针 90° 变体(非独立题,是面试口头改编)——顺序改为「先转置,再垂直翻转」(即翻转上下行);「旋转 180°」则等于做两次水平翻转 + 两次垂直翻转(或直接
matrix[i][j] ↔ matrix[n-1-i][n-1-j])。 - 同套路延伸题:LeetCode 566 重塑矩阵、867 转置矩阵,训练「行 ↔ 列」的映射直觉,是理解「转置」这一步的简单前置题。
#240 搜索二维矩阵 II
题意
给定一个 m×n 矩阵,特性是每行从左到右升序、每列从上到下升序,要求编写高效算法判断目标值 target 是否存在。
输入:matrix = [[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]], target = 5
输出:true
题目本质:从左下角(或右上角)出发的 Z 字搜索。以右上角为例:该元素比同行左边都大、比同列下方都小,相当于一棵二叉搜索树的根节点——根据与 target 的大小关系,每次能排除一整行或一整列。
现实类比:地图寻宝——站在右上角,目标比当前值小就往左走,比当前值大就往下走,每一步都能排除一整行或一整列,直到命中或走出边界。
难点与易错点
- 出发角必须选对:只有右上角(或左下角)同时具备「一行最大、一列最小」的性质,才保证每次比较能确定性地排除一行或一列。左上角(最小)和右下角(最大)都做不到——比它大/小时无法判断该往哪走。
- 移动方向别写反:从右上角出发时,
matrix[i][j] > target说明当前值偏大,要上移i--(排除当前行);< target则右移j++(排除当前列)。若从左下角出发,方向正好相反,混用必错。 - 越界条件:
while (i >= 0 && j < n)两边都要写全,漏写一边会导致数组越界。 - 与「每行首元素大于上一行末元素」的矩阵(LeetCode 74)区分:本题的「行尾、行首之间没有严格大小关系」,所以不能像 74 那样把整个矩阵当成一维数组二分,只能按行/列逐次排除。
解法一:暴力 / 直观(逐元素 / 每行二分)
最直接的想法是双重循环扫描每个元素;稍加优化则利用「每行升序」对每一行做二分。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
// 逐行二分:只利用「每行升序」,未利用「每列升序」
for (int[] row : matrix) {
int l = 0, r = row.length - 1;
while (l <= r) {
int mid = (l + r) >>> 1;
if (row[mid] == target) return true;
else if (row[mid] < target) l = mid + 1;
else r = mid - 1;
}
}
return false;
}
}
(纯暴力双重循环则是 O(m×n),思路更直白但更慢,此处以每行二分为「直观优化」。)
复杂度:对 m 行各做一次二分,每行 O(log n),总时间 O(m log n);空间 O(1)。
瓶颈:只利用了「每行升序」,完全浪费了「每列升序」这个更关键的条件,因此还不是最优。既然每列也有序,就可以做到 O(m + n)。
解法二:优化 / 最优(右上角 Z 字搜索,O(m+n))
从右上角出发,根据大小关系逐次排除一行或一列。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
// 从右上角出发:该位置是当前行最大值、当前列最小值
int i = 0; // 行指针,从第一行开始向下
int j = matrix[0].length - 1; // 列指针,从最后一列开始向左
while (i < matrix.length && j >= 0) {
if (matrix[i][j] > target) {
j--; // 当前值偏大,排除当前列,左移
} else if (matrix[i][j] < target) {
i++; // 当前值偏小,排除当前行,下移
} else {
return true; // 找到目标
}
}
return false; // 越界,未找到
}
}
> **💭 思考**:行列都有序,为什么是「从右上角出发」,而不是逐行二分或从左下/左上出发?一步步想——逐行二分只利用了「行有序」、浪费了「列有序」,O(m log n) 还不够;而要想一次比较就排除尽量多,需要找一个「同时具备行、列信息」的点:右上角元素是「当前行最大、当前列最小」,相当于二叉搜索树的根——比它大就排除整行下移、比它小就排除整列左移,每次比较都能砍掉一行或一列,于是 O(m + n)。左上角是最小值,比它大时既可能右移也可能下移,排除不了任何一行一列,所以出发角必须选对。
(注:源材料里给出的是「从左下角出发、行上移/列右移」的等价写法,两者对称,任选其一即可。)
复杂度逐步推导:
- 每一步要么
i++(行指针下移),要么j--(列指针左移); i最多从 0 增加到 m(最多 m 步),j最多从 n-1 减到 -1(最多 n 步);- 最坏情况下两条指针各走到头,总步数 ≤ m + n,时间 O(m + n);
- 只用了
i, j两个指针,空间 O(1)。
多解法对比
| 解法 | 时间 | 空间 | 适用场景 |
|---|---|---|---|
| 纯暴力双重循环 | O(m×n) | O(1) | 无脑保底,必被追问优化 |
| 每行二分 | O(m log n) | O(1) | 只想到行有序时的折中方案 |
| 右上角 Z 字搜索(最优) | O(m + n) | O(1) | 同时利用行、列有序的标准答案 |
在 m、n 都较大时,O(m + n) 远优于 O(m×n) 和 O(m log n)。本题约束(行列各自有序)决定了「二分整块矩阵」不可行,而 Z 字搜索恰好能把每次比较的价值最大化——一次比较排除一整行或一整列,因此选它作为最优解。
CodeTop 变体
- 同套路延伸题(必刷):LeetCode 74 搜索二维矩阵。它的约束更强(每行升序,且下一行首元素 > 上一行末元素),因此可以把矩阵当成一维有序数组整体二分。面试官常把 74 和 240 放在一起问,考察你是否能分清两种矩阵的约束差异、对应不同解法。
- 高频真实变体:字节、腾讯手撕常追问「为什么从右上角或左下角出发,从左上角为什么不行」——本质是让你讲清「该点是二叉搜索树的根」这个洞察。
- 延伸思路:若矩阵极大、需要多路归并,可联想到 378 有序矩阵中第 K 小的元素,同样利用「行列有序」做值域二分或堆,是本题思想的进阶。
本分类小结
| 题号 | 题目 | 核心套路 | 一句话心法 |
|---|---|---|---|
| 73 | 矩阵置零 | 就地标记 | 用第一行第一列当标记位,先记边界本身,再逆向清零 |
| 54 | 螺旋矩阵 | 边界收缩 | 四边界每走完一条就收缩一条,边遍历边判越界 |
| 48 | 旋转图像 | 转置 + 翻转 | 把一次复杂旋转拆成两个简单原地操作 |
| 240 | 搜索二维矩阵 II | Z 字搜索 | 从右上角出发,每次比较排除一行或一列 |
矩阵题万变不离其宗:看清「边界」与「方向」,想清「每次操作能否确定性地推进」。把这四道题手撕到不看题面也能写出来,矩阵分类即可通关。
章末提问
- #73 为什么不能边遍历边置零,必须「先标记再统一清零」?——结论:因为边遍历边置零会把「新产生的 0」误当成「原本的 0」,把不该清的行列也清了。必须先完整记录所有 0 的位置,再统一执行。
- #48 为什么「转置 + 翻转」能实现顺时针 90°,直接套
matrix[i][j] → matrix[j][n-1-i]不行吗?——结论:映射公式本身是对的,但一轮轮套环旋转极易写错;转置 + 翻转把一次复杂旋转拆成两个各自简单、且都能原地完成的操作,边界清晰、不易翻车。 - #240 为什么只能从右上角或左下角出发,从左上角出发为什么不行?——结论:因为右上角同时是「一行最大、一列最小」,每次比较能确定性地排除一行或一列;左上角是最小值,比它大时无法判断该右移还是下移,排除不了任何一行或一列。