题目
给定一个 ( n \times n ) 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须 在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例 1:

lua
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:

lua
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
一、读题
一句话复述 :将一个 ( n \times n ) 的方阵原地顺时针旋转 90°,不能借助额外矩阵。
关键约束高亮:
- ( n \times n ) 方阵(行数 = 列数),这是旋转能简化的前提;
- 原地修改 (
Do not return anything),意味着空间复杂度必须为 ( O(1) )(不计递归栈); - 矩阵元素类型为整数,无特殊范围限制。
二、前置知识
| 序号 | 知识点 | 说明 |
|---|---|---|
| 1 | 矩阵转置 | 沿主对角线(左上→右下)翻转,即 matrix[i][j] ↔ matrix[j][i] |
| 2 | 水平翻转(行内逆序) | 每行左右对称交换,即 matrix[i][j] ↔ matrix[i][n-1-j] |
| 3 | 原地交换技巧 | 利用 ES6 解构赋值 [a, b] = [b, a] 无需临时变量,简洁且安全 |
非常见 API 表格:
| API | 作用 | 参数 | 返回值 | 使用场景 |
|---|---|---|---|---|
Math.floor(n / 2) |
向下取整 | 数值 | 整数 | 行内翻转时确定只需遍历前半列 |
解构赋值 [a, b] = [b, a] |
交换两个变量值 | 左右均为数组/类数组 | undefined |
原地交换矩阵元素,无需 temp |
三、思路推导(核心)
暴力解(不满足题意)
最容易想到的是新建矩阵 :观察可知,旋转后 new[j][n-1-i] = old[i][j],即第 i 行第 j 列的元素会跑到第 j 行倒数第 i 列。
javascript
var rotate = function(matrix) {
const n = matrix.length;
const newMatrix = Array.from({ length: n }, () => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
newMatrix[j][n - 1 - i] = matrix[i][j];
}
}
// 拷贝回去(但这不算严格原地,且额外 O(n^2) 空间)
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix[i][j] = newMatrix[i][j];
}
}
};
- 时间复杂度:( O(n^2) )(两轮遍历,常数倍忽略);
- 空间复杂度:( O(n^2) )(额外矩阵)。
代入最大数据量(LeetCode 通常 ( n \le 20 ) 或更大),若 ( n=1000 ),则额外需要 100 万个元素的存储空间,内存可能不足,且违背题意。
优化动机 ------ 为什么能想到"转置 + 水平翻转"?
思维演进路径:
- 观察旋转前后坐标变化 :
(i, j)→(j, n-1-i)。 - 拆解这个变换 :能否分解为两个更简单的操作?
- 先做转置 :
(i, j)→(j, i); - 再做水平翻转 :
(j, i)→(j, n-1-i)。 - 两步合起来正好是
(i, j)→(j, n-1-i)。
- 先做转置 :
- 为什么这样想 :因为转置和翻转都是原地可完成的操作,且逻辑简单------转置只需遍历对角线一侧,翻转只需每行对折交换。这种"将复杂变换拆成两个已知简单操作"是算法设计的常见套路。
核心几何直觉:顺时针旋转 90° = 沿主对角线翻折 + 左右翻折(或者先上下翻折再转置,两种等价)。
手把手模拟(以 3×3 为例)
矩阵初始:
| 步骤 | 操作 | 矩阵状态 |
|---|---|---|
| 0 | 原始 | \[1,2,3,4,5,6,7,8,9] |
| 1 | 转置:交换 (0,1)↔(1,0) |
\[1,4,3,2,5,6,7,8,9] |
| 2 | 转置:交换 (0,2)↔(2,0) |
\[1,4,7,2,5,6,3,8,9] |
| 3 | 转置:交换 (1,2)↔(2,1) |
\[1,4,7,2,5,8,3,6,9] |
| 4 | 水平翻转第 0 行:交换 (0,0)↔(0,2) |
\[7,4,1,2,5,8,3,6,9] |
| 5 | 水平翻转第 1 行:交换 (1,0)↔(1,2) |
\[7,4,1,8,5,2,3,6,9] |
| 6 | 水平翻转第 2 行:交换 (2,0)↔(2,2) |
\[7,4,1,8,5,2,9,6,3] ✅ |
四、代码与避坑
完整代码
javascript
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var rotate = function(matrix) {
const n = matrix.length;
// 1. 转置(沿主对角线翻转)
// 为什么 j 从 i+1 开始?因为只需遍历上三角,避免重复交换
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
// 2. 每行左右翻转
// 为什么 j < Math.floor(n / 2)?因为只需遍历每行前半部分,避免重复交换
for (let i = 0; i < n; i++) {
for (let j = 0; j < Math.floor(n / 2); j++) {
[matrix[i][j], matrix[i][n - 1 - j]] = [matrix[i][n - 1 - j], matrix[i][j]];
}
}
};
边界攻防表
| 边界场景 | ❌ 错误写法 | ✅ 正确写法 | 原因 |
|---|---|---|---|
| n = 1(单元素矩阵) | 内外层循环正常执行,j < n 进入转置,j < Math.floor(n/2) 不执行 |
转置时 j = i+1 = 1,内层不执行;水平翻转 j < 0 不执行 |
对角线无元素可交换,行内无需翻转 |
| n = 2(最小非平凡) | 转置时 j 从 i+1 开始,正确交换一次;水平翻转 j < 1 交换一次 |
正确 | 注意双重循环的边界条件 |
| 偶数 n(如 n=4) | 水平翻转时 j < n/2 即 j < 2,交换 (0,3) 和 (1,2),正确覆盖全部 |
正确 | 偶数行中心是两个元素,需交换 n/2 对 |
五、举一反三
面试追问
-
逆时针旋转 90° 怎么做?
→ 先转置,再上下翻转 (每列上下交换),即
(i,j)→(n-1-j, i)。 -
旋转 180° 怎么做?
→ 先上下翻转,再左右翻转;或者直接
(i,j)↔(n-1-i, n-1-j)原地交换,只需遍历四分之一矩阵。 -
如果矩阵不是方阵(( m \times n )) ,能否原地旋转?
→ 不能严格原地,因为旋转后维度变为 ( n \times m ),形状改变,必须借助新矩阵。
-
如果要求旋转后输出新的矩阵,但原矩阵保留 (非原地)?
→ 直接用暴力解的新建矩阵即可,代码更简单,无需转置翻转。
相似题型
| 题目 | 差异点 | 解法变化 |
|---|---|---|
| LeetCode 54. 螺旋矩阵 | 按螺旋顺序遍历输出,不需要修改原矩阵 | 用四个边界指针收缩,属于模拟题 |
| LeetCode 59. 螺旋矩阵 II | 生成一个 ( n \times n ) 的螺旋矩阵 | 按顺序填充数字,同样用边界指针 |
| 剑指 Offer 29. 顺时针打印矩阵 | 同 54 题,但要求返回一维数组 | 边界模拟,注意空矩阵处理 |
核心思路迁移:矩阵变换类问题,优先考虑"分解为转置 + 翻转"的组合,因为这两种操作原地成本低,易于实现。
一句话总结 :顺时针旋转 90° = 转置 + 水平翻转,原地完成,时间复杂度 ( O(n^2) ),空间复杂度 ( O(1) )。遇到类似矩阵变换,先想能否拆解为简单操作的组合。