一、题目描述
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入: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 == matrix.length == matrix[i].length
- 1 <= n <= 20
- -1000 <= matrix[i][j] <= 1000
二、解题思路总览
核心思想:两次翻转代替旋转
顺时针旋转 90 度可以通过两次翻转实现:
- 方法一:先上下翻转,再按主对角线转置
- 方法二:先按主对角线转置,再左右翻转
旋转与翻转的数学关系:
顺时针旋转 90 度 = 上下翻转 + 主对角线转置
顺时针旋转 90 度 = 左右翻转 + 副对角线转置
为什么可以用翻转代替旋转?
观察旋转前后的坐标映射关系:
旋转前 (i, j) → 旋转后 (j, n-1-i)
两步翻转的合成效果:
1. 上下翻转: (i, j) → (n-1-i, j)
2. 转置: (n-1-i, j) → (j, n-1-i)
最终结果正好是顺时针旋转 90 度!
| 方法 | 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 两次翻转(本题) | 上下翻转 + 转置 | O(n^2) | O(1) |
| 原地旋转公式 | 直接用公式计算目标位置 | O(n^2) | O(1) |
| 辅助矩阵 | 额外开一个矩阵 | O(n^2) | O(n^2) |
三、完整代码
cpp
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 第一次翻转:上下翻转(沿着水平中线)
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < n; j++) {
swap(matrix[i][j], matrix[n - i - 1][j]);
}
}
// 第二次翻转:按主对角线转置
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
};
四、算法流程图
4.1 整体旋转流程
输入:matrix(n x n 方阵)
[Step 1] 获取矩阵大小
n = matrix.size()
|
v
[Step 2] 第一次翻转:上下翻转
|
v
for i = 0 to n/2 - 1:
for j = 0 to n - 1:
swap(matrix[i][j], matrix[n-i-1][j])
|
v
[Step 3] 第二次翻转:按主对角线转置
|
v
for i = 0 to n - 1:
for j = 0 to i - 1:
swap(matrix[i][j], matrix[j][i])
|
v
[Step 4] 【返回】matrix 已在原地修改
4.2 第一次翻转:上下翻转流程
操作:以水平中线为轴,上下翻转
对应关系:matrix[i][j] ↔ matrix[n-i-1][j]
例如 n=3:
i=0: matrix[0][j] ↔ matrix[2][j] (第0行和第2行交换)
i=1: matrix[1][j] ↔ matrix[1][j] (第1行和自己交换,实际跳过)
具体执行:
初始:[[1,2,3],
[4,5,6],
[7,8,9]]
i=0, j=0,1,2:
swap(matrix[0][0], matrix[2][0]) → swap(1, 7)
swap(matrix[0][1], matrix[2][1]) → swap(2, 8)
swap(matrix[0][2], matrix[2][2]) → swap(3, 9)
结果:[[7,8,9],
[4,5,6],
[1,2,3]]
4.3 第二次翻转:转置流程
操作:沿主对角线交换,即 matrix[i][j] ↔ matrix[j][i]
只遍历上三角:i > j 的区域
具体执行:
初始(来自上一步):[[7,8,9],
[4,5,6],
[1,2,3]]
i=0: j=0 → 跳过(i==j不需要交换)
i=1: j=0 → swap(matrix[1][0], matrix[0][1]) → swap(4, 8) → [[7,4,9],[8,5,6],[1,2,3]]
j=1 → 跳过
i=2: j=0 → swap(matrix[2][0], matrix[0][2]) → swap(1, 9) → [[7,4,1],[8,5,6],[9,2,3]]
j=1 → swap(matrix[2][1], matrix[1][2]) → swap(2, 6) → [[7,4,1],[8,5,2],[9,6,3]]
j=2 → 跳过
结果:[[7,4,1],
[8,5,2],
[9,6,3]]
4.4 完整执行过程示例
输入:[[1,2,3],
[4,5,6],
[7,8,9]]
【第一步:上下翻转】
翻转后:[[7,8,9],
[4,5,6],
[1,2,3]]
【第二步:按主对角线转置】
转置后:[[7,4,1],
[8,5,2],
[9,6,3]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
验证:正确!
4.5 翻转顺序验证(先转置后左右翻转)
输入:[[1,2,3],
[4,5,6],
[7,8,9]]
【第一步:按主对角线转置】
转置后:[[1,4,7],
[2,5,8],
[3,6,9]]
【第二步:左右翻转(镜像)】(不常用,了解即可)
左右翻转后:[[7,4,1],
[8,5,2],
[9,6,3]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
验证:结果相同!
五、逐行解析
5.1 第一次翻转:上下翻转
cpp
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < n; j++) {
swap(matrix[i][j], matrix[n - i - 1][j]);
}
}
外层循环: i 从 0 到 n/2-1,只遍历上半部分(不包括中线,中线不需要翻转)
内层循环: j 遍历所有列
交换规则: matrix[i][j] 与 matrix[n-i-1][j] 交换
i < n/2 的原因:
- n=4:i=0,1 遍历,交换 matrix[0]↔matrix[3],matrix[1]↔matrix[2]
- n=5:i=0,1,2 遍历,交换 matrix[0]↔matrix[4],matrix[1]↔matrix[3],matrix[2]↔matrix[2](自身)
5.2 第二次翻转:转置
cpp
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
外层循环: i 从 0 到 n-1,遍历所有行
内层循环: j 从 0 到 i-1,只遍历上三角(不包括主对角线)
交换规则: matrix[i][j] 与 matrix[j][i] 交换
j < i 的原因: 只处理主对角线以上的元素,主对角线自身不需要交换
六、复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n^2) | 两次遍历,每次 O(n^2) |
| 空间复杂度 | O(1) | 只用了常数个变量,原地修改 |
七、面试追问
| 问题 | 回答要点 |
|---|---|
| 为什么要用两次翻转而不是直接旋转? | 直接旋转需要保存一个元素然后用公式移动,代码复杂且容易出错;两次翻转直观易懂 |
| 为什么先上下翻转再转置可以等价于旋转? | 数学推导:旋转 (i,j)→(j,n-1-i),翻转合成效果相同 |
| 为什么不先转置再上下翻转? | 也可以,两种顺序等价,都能得到正确结果 |
| n/2 的意义是什么? | 只遍历上半部分,因为下半部分会和上半部分交换,遍历完整矩阵会换回来 |
| 转置为什么只遍历 j < i 的区域? | 转置时 matrix[i][j] 和 matrix[j][i] 是配对交换,遍历上三角即可避免重复 |
| 能否只用一个循环完成? | 可以用公式直接计算每个元素的目标位置,但代码更复杂 |
| 如果 n 是奇数会有问题吗? | 不会,中间的行(i = n/2)上下翻转时自己和自己交换,不需要特殊处理 |
| 如何验证旋转的正确性? | 旋转前 (i, j) 的元素,旋转后应该在 (j, n-1-i) 的位置 |
八、相关题目
| 题号 | 题目 | 关键点 |
|---|---|---|
| 48 | 旋转图像 | 本题 |
| 54 | 螺旋矩阵 | 矩阵遍历 |
| 59 | 螺旋矩阵 II | 矩阵生成 |
| 73 | 矩阵置零 | 原地标记 |