73. 矩阵置零
这道 LeetCode 第 73 题 “矩阵置零” (Set Matrix Zeroes) 是一个经典的数组处理问题。我来用更接地气的方式为你拆解。
1. 生活案例:办公室的“流感隔离”
想象你在一家开放式办公室工作,座位排成了一个 m×n 的矩形方阵。
- 突发状况: 某些员工不幸得了流感(元素为 0)。
- 防疫规定: 为了安全,只要某个人得了流感,他所在的整一排(行)和所在的整一列的所有同事都必须回家隔离(全部置为 0)。
- 难点: 规定要求“原地办公”。你不能新开一间办公室来排座位,必须在现有的办公室里完成这个标记过程。
2. 代码解析(带生活化注释)
你提供的代码采用的是 “记小本本”法。虽然题目要求“原地(In-place)”,但你现在的解法使用了额外的空间(两个数组)来记录哪些行和列需要被“隔离”。
这是逻辑非常清晰的一种做法。以下是完整的代码和对应的生活化解释:
JavaScript
/**
* @param {number[][]} matrix
* @return {void} 不要返回任何内容,而是原地修改矩阵。
*/
var setZeroes = function(matrix) {
let n = matrix.length; // 办公室有多少排(行)
let m = matrix[0].length; // 每一排有多少个座位(列)
// 【准备阶段】:拿两张便签纸记录
// row 记录哪一排有人生病,col 记录哪一列有人生病
let row = new Array(n).fill(false);
let col = new Array(m).fill(false);
// 【第一轮检查】:巡视整个办公室
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
// 如果发现 (i, j) 位置的员工生病了
if (matrix[i][j] === 0) {
row[i] = true; // 在便签上记下:第 i 排全员要隔离
col[j] = true; // 在便签上记下:第 j 列全员要隔离
}
}
}
// 【第二轮执行】:根据便签纸下达隔离通知
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
// 只要你所在的这一排【或者】这一列在便签上有记录
if (row[i] || col[j]) {
matrix[i][j] = 0; // 那么你也得回家隔离(置零)
}
}
}
return matrix; // 任务完成,办公室布局已更新
};
3. 进阶思考:为什么要“记小本本”?
你可能会问: “为什么不直接一边检查,一边把整行整列变 0 呢?”
- 陷阱: 如果你发现第 1 行第 2 列有个 0,立刻把第一行和第二列全变 0,那么等到你检查第 1 行第 3 列时,你会以为这里原本也是 0,结果导致整个办公室最后可能全都被隔离了(发生连锁反应)。
- 解决: 所以必须先标记,等大家都检查完了,再统一执行。
4. 复杂度分析
- 时间复杂度: O(m×n)。你需要把办公室的所有座位都看两遍。
- 空间复杂度: O(m+n)。你额外用了两张便签纸(
row和col数组)。
54. 螺旋矩阵
LeetCode 第 54 题 “螺旋矩阵” (Spiral Matrix) 是面试中的常客。这道题考察的不是复杂的算法,而是你对边界条件的控制能力。
1. 生活案例:吃“卷心菜”或“贪吃蛇”
想象你正在吃一棵方方正正的卷心菜,或者在玩一个不能撞墙的贪吃蛇游戏:
- 规则: 你必须沿着最外圈顺时针绕圈。
- 动作: 每当你剥掉(走过)最外层的一排或一列,这层就“消失”了,你的活动范围就会向内收缩一圈。
- 结束: 直到中间没有菜叶可以剥,或者蛇没地方可以走为止。
2. 代码解析(带生活化注释)
这段代码的核心逻辑就是定义了 四个边界(上下左右),并随着遍历不断地“推墙”。
JavaScript
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function(matrix) {
// 【设定边界】:就像给卷心菜画好四个边的界限
let left = 0;
let right = matrix[0].length - 1;
let top = 0;
let bottom = matrix.length - 1;
let arr = []; // 你的口袋,用来装剥下来的菜叶
// 只要四个边界还没交叉,就继续绕圈
while (left <= right && top <= bottom) {
// 1. 【从左往右】:走最上面的那一横排
for (let i = left; i <= right; i++) {
arr.push(matrix[top][i]);
}
top++; // 顶部的墙往里推一格,这排已经“剥掉”了
// 2. 【从上往下】:走最右边的那一纵列
for (let i = top; i <= bottom; i++) {
arr.push(matrix[i][right]);
}
right--; // 右边的墙往里推一格
// 注意:由于上面推了墙,这里需要判断一下是否还有剩下的菜
if (top <= bottom) {
// 3. 【从右往左】:走最下面的那一横排
for (let i = right; i >= left; i--) {
arr.push(matrix[bottom][i]);
}
bottom--; // 底部的墙往上提一格
}
if (left <= right) {
// 4. 【从下往上】:走最左边的那一纵列
for (let i = bottom; i >= top; i--) {
arr.push(matrix[i][left]);
}
left++; // 左边的墙往右挪一格
}
}
return arr; // 返回剥下来的所有元素
};
3. 核心难点:为什么要有 if 判断?
在代码的第 23 行和第 30 行,你看到了两个 if 判断。这是为了防止重复采摘。
- 场景: 假设你的矩阵只有 3 行 2 列。当你走完“左向右”和“上向下”后,可能已经把所有元素都拿完了。
- 后果: 如果不加
if (top <= bottom),程序会回头再走一次“右向左”,哪怕那一层已经被top++给剥离了,这就会导致结果里出现重复的数据。
4. 总结
- 空间复杂度: 。除了存储答案的
arr外,只用了四个边界变量(不计入输出空间)。 - 时间复杂度: 。矩阵里的每个“座位”你都只坐了一次。
48. 旋转图像
LeetCode 第 48 题 “旋转图像” (Rotate Image) 是矩阵题目中最巧妙的一道。它要求顺时针旋转 90 度,且必须**原地(In-place)**修改。
你的代码采用的是一种非常经典的“两步走”策略:对角线翻转 + 左右镜像。这种方法比直接硬转四个角要好理解得多。
1. 生活案例:翻转“合照地毯”
想象你地上铺了一块 n×n 的方形地毯,上面印着大家的合照。你想把地毯顺时针转 90 度,但地毯太重了,你没法直接抓着角抡一圈。
于是你决定分两步完成:
- “对折”: 先沿着地毯的主对角线(左上角到右下角的那条线)把地毯对折,交换两边的图案。
- “左右互换”: 再把地毯的左半边和右半边像折书页一样对调。
当你完成这两步,你会神奇地发现,照片已经完美地顺时针旋转了 90 度!
2. 代码解析(带生活化注释)
JavaScript
/**
* @param {number[][]} matrix
* @return {void}
*/
var rotate = function(matrix) {
let n = matrix.length;
// --- 第一步:对角线大交换(转置矩阵) ---
// 想象沿着从左上到右下的 45 度斜线,把两边的元素对调
for (let i = 0; i < n; i++) {
// 注意 j 从 i 开始,只处理对角线的一侧,否则换过去又换回来了
for (let j = i; j < n; j++) {
// 交换 matrix[i][j] 和 matrix[j][i]
let temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// --- 第二步:每一行左右“大调个”(镜像翻转) ---
// 现在的图案是斜着的,我们需要把每一行反转过来
for (let i = 0; i < n; i++) {
// 把第 i 行像照镜子一样反过来
// 例如 [1, 2, 3] 变成 [3, 2, 1]
matrix[i].reverse();
}
return matrix;
};
3. 为什么这个技巧有效?
我们可以通过一个简单的数学规律来看:
- 原始位置: (row,col)
- 顺时针旋转 90 度后: 应该去到 (col,n−1−row)
你的两步走拆解:
- 对角线翻转: (row,col)→(col,row)
- 左右翻转: (col,row)→(col,n−1−row) 结果完全一致! 这种拆解把复杂的旋转变成了简单的线性对调。
4. 复杂度与性能
- 时间复杂度: O(n2)。虽然分了两步,但每一步都只遍历了矩阵常数次,总操作数依然和像素点(元素)总数成正比。
- 空间复杂度: O(1)。你没有借用任何额外的“大仓库”(新数组),只用了一个
temp变量做临时中转,完全符合题目要求的“原地旋转”。
240. 搜索二维矩阵 II
这道 LeetCode 第 240 题 “搜索二维矩阵 II” (Search a 2D Matrix II) 考察的是对有序矩阵性质的理解。你的代码实现非常棒,这在算法界有一个很酷的名字,叫 “Z字形查找” 。
1. 生活案例:在“阶梯书架”上找书
想象你在一个图书馆里,书架上的书摆放非常有规律:
- 水平看: 每一排书,从左到右高度都在增加。
- 垂直看: 每一列书,从上到下高度也都在增加。
你想找一本高度为 target 的书。如果你从“右上角”开始看:
- 如果你眼前的这本书太高了,根据规律,它下面的书肯定更高,所以这一列都没戏了,你得往左看 (
col--)。 - 如果你眼前的这本书太矮了,根据规律,它左边的书肯定更矮,所以这一排都没戏了,你得往下看 (
row++)。
2. 代码解析(带生活化注释)
这段代码之所以高效,是因为它利用了矩阵的特殊布局,把二维搜索变成了像在十字路口做决策一样。
JavaScript
/**
* @param {number[][]} matrix
* @param {number} target
* @return {boolean}
*/
var searchMatrix = function(matrix, target) {
// 【安检】:如果办公室(矩阵)是空的,直接报“没找到”
if(!matrix || matrix.length==0 || matrix[0].length==0) return false;
let m = matrix.length; // 总共有多少排
let n = matrix[0].length; // 总共有多少列
// 【起点选择】:我们站在右上角 (第一排,最后一列)
// 为什么选右上角?因为这里是“大小的分水岭”:往左变小,往下变大。
let row = 0;
let col = n - 1;
// 只要还在办公室范围内,就继续找
while (row < m && col >= 0) {
if (matrix[row][col] === target) {
// 1. 运气真好,刚好是我们要的书!
return true;
} else if (matrix[row][col] > target) {
// 2. 当前书太高(大)了,这一列下面的肯定更高。
// 放弃这一列,往左挪一格。
col--;
} else {
// 3. 当前书太矮(小)了,这一排左边的肯定更矮。
// 放弃这一排,往下挪一格。
row++;
}
}
// 走出了边界还没找到,说明真的没有
return false;
};
3. 为什么不选“左上角”或“右下角”?
这是一个非常关键的问题:
- 左上角: 往右是变大,往下也是变大。如果
target比它大,你不知道该往哪走。 - 右下角: 往左是变小,往上也是变小。如果
target比它小,你同样会陷入纠结。 - 右上角(或左下角): 它们是**“平衡点”**。一个方向变大,另一个方向变小。这种“非此即彼”的确定性,让我们每走一步都能排除掉整整一行或一列。
4. 复杂度分析
-
时间复杂度: 。
你最坏的情况是从右上角走到左下角,也就是把行和列都跨越了一遍。比起暴力搜索的 ,这简直是降维打击(尤其是矩阵很大的时候)。
-
空间复杂度: 。
除了几个用来指路的变量,没有浪费任何额外空间。