数组基础与常用操作
二维数组的花式遍历
这类题的共同点
旋转、螺旋、对角线看起来是三件事,但解法都归结为同一句话: 找到那个坐标关系,剩下的是两层循环。
难的从来不是写循环,是想清楚「哪些格子该被同等对待」。
顺时针旋转 90°:先转置,再左右翻转
直接推每个元素的新位置容易绕晕。拆成两步就不用想了:
function rotate(matrix) {
const n = matrix.length;
// 第一步:沿主对角线转置(行列互换)
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) { // 🚨 j 从 i+1 开始
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
// 第二步:每一行左右翻转
for (const row of matrix) row.reverse();
}
🚨 j 必须从 i + 1 开始,不能从 0。从 0 开始的话每一对都会被交换两次,
转置被自己抵消了。
⚠️ 但结果不是「矩阵原封不动」 —— 第二步的行反转照常执行, 所以你拿到的是一个水平镜像:
[[1,2,3], 正确(顺时针 90°) [[7,4,1], j 从 0 开始 [[3,2,1],
[4,5,6], → [8,5,2], → [6,5,4],
[7,8,9]] [9,6,3]] [9,8,7]]
↑ 只做了行反转
🚨 这个区别很重要:结果看起来确实变了,所以不会有「代码没执行」的警觉, 只会觉得「转出来的方向不太对」,容易往「是不是该逆时针」上面去查。
⚠️ n = 1 时两种写法结果相同,n = 2 才看得出来。各规模的暴露率:
n 1 2 3 4 5
暴露率 0.0% 66.5% 96.2% 99.9% 100%
📌 逆时针 90° 只要把第二步换成上下翻转(matrix.reverse())。
两个方向的差别就这一处,不用记两套。
螺旋遍历:四个边界向内收
function spiralOrder(matrix) {
if (matrix.length === 0) return [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
const res = [];
while (top <= bottom && left <= right) {
for (let j = left; j <= right; j++) res.push(matrix[top][j]); // →
top++;
for (let i = top; i <= bottom; i++) res.push(matrix[i][right]); // ↓
right--;
// 🚨 这两个判断不能省
if (top <= bottom) {
for (let j = right; j >= left; j--) res.push(matrix[bottom][j]); // ←
bottom--;
}
if (left <= right) {
for (let i = bottom; i >= top; i--) res.push(matrix[i][left]); // ↑
left++; // 🚨 是 left++
}
}
return res;
}
这题有两个独立的坑,而且它们要用不同形状的矩阵才测得出来。
坑一:中间那两个 if 不能省
走完「上边」和「右边」之后,边界可能已经收缩到重叠了; 不判断就直接走「下边」,会把刚走过的那一行再走一遍:
[[1,2,3]] 单行 不判断 → [1,2,3,2,1] 多了两个
[[1],[2],[3]] 单列 不判断 → [1,2,3,2] 多了一个
[[1,2],[3,4]] 2×2 不判断 → [1,2,4,3] ✅ 恰好正确
📌 只用方阵测,这个坑暴露不出来。 单行和单列是必测用例。
🚨 坑二:最后一步是 left++,不是 top++
四条边分别收缩四个边界:上边收 top、右边收 right、
下边收 bottom、左边收 left。
写成 top++ 的话,top 在一轮里被加了两次,
于是奇数边长矩阵的最中心那个元素会被跳过:
[[1,2,3],[4,5,6],[7,8,9]]
正确 → [1,2,3,6,9,8,7,4,5]
top++ → [1,2,3,6,9,8,7,4] ← 少了中心的 5
⚠️ 这个坑恰好躲过了上面那三个测试用例 —— 单行、单列、2×2 全都正确, 因为它们没有「中心」。要 3×3 才暴露。
⭐ 两个坑加起来的教训:这道题至少要测 单行 + 单列 + 3×3。 把两种错法逐个喂给这四组用例,抓到的情况是:
单行 单列 2×2 3×3
坑一(省掉 if) ✅ ✅ ❌ ❌
坑二(写成 top++) ❌ ❌ ❌ ✅
⚠️ 注意 2×2 一个坑都抓不到。它在这里的作用不是「抓错」, 而是当反面教材 —— 说明为什么「我拿方阵测过了」不能让人放心。 👉 真正互补的是「非方阵」(单行、单列)和「奇数边长」(3×3)这两个维度。
对角线遍历:i + j 和 i - j 是常数
这是二维题里最好用的一个观察:
- 副对角线方向(↙↗)上,
i + j是常数 - 主对角线方向(↘↖)上,
i - j是常数
于是「按对角线分组」只要拿 i + j 当 key:
function diagonalGroups(matrix) {
const groups = new Map();
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[0].length; j++) {
const k = i + j;
if (!groups.has(k)) groups.set(k, []);
groups.get(k).push(matrix[i][j]);
}
}
return [...groups.keys()].sort((a, b) => a - b).map((k) => groups.get(k));
}
⭐ 判断棋盘上两个皇后是否互相攻击,用的也是这一条:
同一条对角线 ⇔ i+j 相等或 i-j 相等。
N 皇后的剪枝就是这么写的。
矩阵置零:原地标记的经典陷阱
题意:某个格子是 0,就把它所在的整行整列都置零。
🚨 最直觉的写法是错的 —— 边遍历边置零,产生的新 0 会被后面的遍历读到、 当成原本就存在的 0,于是清零像传染病一样扩散:
[[1,1,1], 正确 [[1,0,1], 边扫边置零 [[1,0,0],
[1,0,1], → [0,0,0], → [0,0,0],
[1,1,1]] [1,0,1]] [0,0,0]]
4 个 1 只剩 1 个
⚠️ 注意左上角那个 1 活下来了 —— 因为遍历是从左上往右下走的,
污染只会往「还没扫到」的方向扩散。所以症状不是「全变 0」而是
「左上角残留一小块」,看起来更像是某个边界条件写错了。
正确做法是先记录、后统一处理:
function setZeroes(matrix) {
const m = matrix.length, n = matrix[0].length;
const rows = new Set(), cols = new Set();
for (let i = 0; i < m; i++)
for (let j = 0; j < n; j++)
if (matrix[i][j] === 0) { rows.add(i); cols.add(j); }
for (let i = 0; i < m; i++)
for (let j = 0; j < n; j++)
if (rows.has(i) || cols.has(j)) matrix[i][j] = 0;
}
⭐ 这个「先扫描收集,再统一修改」的两遍模式,是所有 「原地修改、但新值会干扰后续判断」问题的通解。 同一个套路在封闭岛屿那题里也出现过 —— 先淹掉边界上的岛,再统计。
📌 空间可以优化到 O(1)(借第一行第一列当标记位),但那一版的边界处理很啰嗦。 面试里先写 O(m+n) 这版、再口头说明能优化到 O(1),是更稳的打法。
练习
勾选记录做过哪些,0 / 5 道。进度只存在这台设备的浏览器里;同一道题在别篇勾过,这里也会显示已做。
- 48. 旋转图像中等转置 + 翻转;注意 j 从 i+1 开始
- 54. 螺旋矩阵中等四边界收缩;必测单行、单列、3×3
- 59. 螺旋矩阵 II中等反过来按螺旋填数
- 73. 矩阵置零中等先扫描收集、再统一修改
- 498. 对角线遍历中等i+j 为常数
⚖️ 这里只给题号、官方标题和链接,不复述题面 —— 平台的题面文字有版权。 「这题考什么」写的是它对应本篇的哪个点,那部分是本站自己的内容。