在LeetCode数组类题目中,「原地操作」是高频考点,也是提升解题难度的关键。它要求我们不借助额外空间,直接修改输入数据,既考验对数组索引的敏感度,也容易因细节失误踩坑。今天我们聚焦第48题「旋转图像」,拆解题干要求,详解最优原地解法(即你提供的TypeScript代码),帮你吃透核心逻辑、避开常见误区,轻松拿下这道经典题目。
一、题干核心要求(必看)
题目给出一个 n×n 的二维矩阵 matrix 表示一幅图像,要求我们将其顺时针旋转90度,且必须满足以下核心约束:
-
原地修改:禁止使用另一个矩阵存储旋转后的结果,只能直接操作输入的
matrix; -
边界说明:
n的范围是 [1, 20],矩阵元素为整数,无需处理边界外异常,但需保证旋转后索引不越界、元素位置正确。
举个直观例子:输入3×3矩阵 [[1,2,3],[4,5,6],[7,8,9]],旋转后需得到 [[7,4,1],[8,5,2],[9,6,3]]。
二、最优原地解法(你的代码,逐行解析)
你提供的代码是「按圈旋转、4元素循环交换」的最优实现------逻辑直观、无冗余、效率拉满(时间复杂度 O(n²),空间复杂度 O(1),完全满足原地要求)。下面逐行拆解,让你清楚每一步的作用和底层逻辑。
typescript
/**
Do not return anything, modify matrix in-place instead.
*/
function rotate(matrix: number[][]): void {
const side = matrix.length;
if (side === 1) return;
const laps = Math.floor(side / 2);
for (let lap = 0; lap < laps; lap++) {
const start = lap;
const end = side - lap - 1;
for (let index = start; index < end; index++) {
// 保存当前位置的初始值(顶部边)
const tempTop = matrix[start][index];
// 1. 左侧边 -> 顶部边(对应位置)
matrix[start][index] = matrix[side - 1 - index][start];
// 2. 底部边 -> 左侧边(对应位置)
matrix[side - 1 - index][start] = matrix[end][side - 1 - index];
// 3. 右侧边 -> 底部边(对应位置)
matrix[end][side - 1 - index] = matrix[index][end];
// 4. 顶部边初始值 -> 右侧边(对应位置)
matrix[index][end] = tempTop;
}
}
}
第一步:边界处理与圈数计算
typescript
const side = matrix.length;
if (side === 1) return;
const laps = Math.floor(side / 2);
这3行是解题的基础,主要解决「无需旋转」和「旋转多少圈」的问题:
-
side = matrix.length:获取矩阵的边长(n×n矩阵,边长即n); -
if (side === 1) return:边界优化------1×1矩阵旋转后还是自身,无需任何操作,直接返回,避免无效循环; -
laps = Math.floor(side / 2):计算需要旋转的圈数。核心逻辑:矩阵旋转是「从外到内,一圈一圈旋转」,n为奇数时,中心元素旋转后位置不变,无需旋转,因此圈数为n的一半(向下取整)。
举例说明:4×4矩阵(偶数),圈数=2(外圈、内圈);3×3矩阵(奇数),圈数=1(仅外圈,中心元素5无需旋转)。
第二步:外层循环------遍历每一圈
typescript
for (let lap = 0; lap < laps; lap++) {
const start = lap;
const end = side - lap - 1;
// 内层循环:处理当前圈的元素
}
外层循环控制「旋转哪一圈」,每循环一次,向内推进一圈,关键是确定当前圈的「边界索引」:
-
start = lap:当前圈的「起始索引」(行和列一致)。比如第0圈(最外圈),start=0;第1圈(内圈),start=1,以此类推; -
end = side - lap - 1:当前圈的「结束索引」(行和列一致)。比如4×4矩阵,第0圈end=3(最右/最下边界),第1圈end=2(内圈边界);3×3矩阵,第0圈end=2。
有了start和end,就确定了当前圈的范围:行和列都从start到end,构成一个正方形的圈。
第三步:内层循环------处理当前圈的元素(核心交换逻辑)
typescript
for (let index = start; index < end; index++) {
// 保存当前位置的初始值(顶部边)
const tempTop = matrix[start][index];
// 1. 左侧边 -> 顶部边(对应位置)
matrix[start][index] = matrix[side - 1 - index][start];
// 2. 底部边 -> 左侧边(对应位置)
matrix[side - 1 - index][start] = matrix[end][side - 1 - index];
// 3. 右侧边 -> 底部边(对应位置)
matrix[end][side - 1 - index] = matrix[index][end];
// 4. 顶部边初始值 -> 右侧边(对应位置)
matrix[index][end] = tempTop;
}
这是整个代码的核心,也是原地旋转的关键------每一圈的元素,按「顶部边→右侧边→底部边→左侧边」的顺序,4个对应位置循环交换。我们用「临时变量保存起始值」,避免赋值时覆盖原数据,逐句解析:
1. 确定交换的4个核心位置
以当前圈的「顶部边元素 matrix[start][index]」为起点,这4个位置的对应关系的核心逻辑是:顺时针旋转90度,每个元素的位置都会转移到它的「顺时针相邻边」的对应位置,具体对应如下:
-
顶部边:
matrix[start][index]------ 要移动到「右侧边」的对应位置; -
左侧边:
matrix[side-1-index][start]------ 要移动到「顶部边」的当前位置; -
底部边:
matrix[end][side-1-index]------ 要移动到「左侧边」的对应位置; -
右侧边:
matrix[index][end]------ 要移动到「底部边」的对应位置。
2. 4步循环赋值(关键,避免覆盖)
赋值顺序不能乱,必须从「左侧边→顶部边」开始,最后将顶部边的初始值赋给右侧边,原因是:我们用tempTop保存了顶部边的初始值,若先修改顶部边,会导致后续赋值丢失原始数据。
-
const tempTop = matrix[start][index]:保存顶部边当前元素的初始值,防止后续赋值覆盖; -
matrix[start][index] = matrix[side-1-index][start]:将左侧边的对应元素,移动到顶部边的当前位置; -
matrix[side-1-index][start] = matrix[end][side-1-index]:将底部边的对应元素,移动到左侧边的对应位置; -
matrix[end][side-1-index] = matrix[index][end]:将右侧边的对应元素,移动到底部边的对应位置; -
matrix[index][end] = tempTop:将顶部边的初始值(tempTop),移动到右侧边的对应位置。
用3×3矩阵举例,直观理解交换过程
输入3×3矩阵:[[1,2,3],[4,5,6],[7,8,9]],side=3,laps=1,仅旋转最外圈(start=0,end=2),index从0遍历到1(不包含end=2):
当index=0时:
-
tempTop = matrix[0][0] = 1(保存顶部边初始值);
-
matrix[0][0] = matrix[2][0] = 7(左侧边→顶部边);
-
matrix[2][0] = matrix[2][2] = 9(底部边→左侧边);
-
matrix[2][2] = matrix[0][2] = 3(右侧边→底部边);
-
matrix[0][2] = tempTop = 1(顶部边初始值→右侧边);
-
此时矩阵变为:
[[7,2,1],[4,5,6],[9,8,3]]。
当index=1时:
-
tempTop = matrix[0][1] = 2(保存顶部边初始值);
-
matrix[0][1] = matrix[1][0] = 4(左侧边→顶部边);
-
matrix[1][0] = matrix[2][1] = 8(底部边→左侧边);
-
matrix[2][1] = matrix[1][2] = 6(右侧边→底部边);
-
matrix[1][2] = tempTop = 2(顶部边初始值→右侧边);
-
最终矩阵变为:
[[7,4,1],[8,5,2],[9,6,3]],旋转完成。
三、常见误区避坑(新手必看)
很多人实现原地旋转时,容易踩坑导致结果错误或索引越界,结合这道题和你的代码,总结3个高频误区:
误区1:索引颠倒(最致命)
二维矩阵 matrix[x][y] 中,x 是行号(垂直方向,从上到下递增),y 是列号(水平方向,从左到右递增)。新手容易把行和列颠倒,比如误将 matrix[side-1-index][start] 写成 matrix[start][side-1-index],导致元素赋值到错误位置。
误区2:赋值顺序错误(覆盖原始数据)
若不先保存顶部边的初始值(tempTop),或颠倒赋值顺序(比如先将顶部边的值赋给右侧边),会导致后续赋值时,原始数据被覆盖,最终旋转结果错误。你的代码通过「先保存、再从左到右赋值」完美避开了这个坑。
误区3:圈数计算错误
有人会直接用 laps = side / 2(不向下取整),当side为奇数时,会多循环一次(比如3×3矩阵,laps=1.5,循环2次),导致中心元素被错误修改。正确写法是 Math.floor(side / 2),确保奇数边长时,中心元素不被处理。
四、拓展:另一种简洁解法(转置+反转)
除了你的「按圈交换」思路,还有一种更简洁的原地解法------「先转置矩阵,再反转每一行」,代码更短,适合面试时快速书写,这里也提供TypeScript实现,供你拓展思路:
typescript
function rotate(matrix: number[][]): void {
const n = matrix.length;
// 第一步:转置矩阵(行变列,列变行)
for (let i = 0; i < n; i++) {
for (let j = i; j < n; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
// 第二步:反转每一行(完成顺时针旋转90度)
for (let i = 0; i < n; i++) {
matrix[i].reverse();
}
}
逻辑说明:转置矩阵后,每一行的元素顺序恰好是旋转后元素的逆序,反转每一行即可得到顺时针旋转90度的结果。比如3×3矩阵,转置后为 [[1,4,7],[2,5,8],[3,6,9]],反转每一行后就是目标结果。
对比你的解法:这种方法代码更简洁,但需要理解「转置+反转」与「顺时针旋转90度」的等价性,对新手不够友好;而你的解法逻辑更直观,贴合旋转的本质,更容易理解和调试,推荐新手优先掌握。
五、刷题总结
LeetCode 48题「旋转图像」的核心是「原地操作」,你的代码已经是最优解法,总结关键点:
-
核心思路:按圈旋转,从外到内,每一圈的4个对应位置循环交换;
-
关键技巧:用临时变量保存起始值,避免赋值覆盖,赋值顺序不能乱;
-
效率优势:时间复杂度
O(n²)(遍历所有元素一次),空间复杂度O(1)(无额外空间),完全满足题目要求; -
避坑重点:明确矩阵行和列的索引含义,正确计算旋转圈数,避免赋值顺序错误。
最后提醒:刷题时,建议把你的代码复制到LeetCode编辑器,测试不同用例(偶数n、奇数n),亲手调试每一步的索引变化,才能真正吃透逻辑,下次遇到类似的原地旋转题目,就能快速上手~