LeetCode 48. 旋转图像:转置 + 翻转详解
1. 这道题属于什么算法思想
这题主要归类为:
text
矩阵 / 原地变换 / 坐标映射
它不是动态规划、贪心或回溯题。
题目要求在原矩阵上完成顺时针旋转 90 度,不能创建另一个同样大小的矩阵。
最推荐的原地做法是:
text
1. 沿主对角线转置矩阵。
2. 翻转每一行。
这两步组合起来,刚好得到顺时针旋转 90 度的结果。
2. 这道题到底在问什么
例如:
text
matrix =
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
顺时针旋转 90 度后,应变成:
text
[
[7, 4, 1],
[8, 5, 2],
[9, 6, 3]
]
题目要求:
text
直接修改 matrix。
不能创建另一个 n x n 的结果矩阵。
3. 顺时针旋转的坐标规律
设矩阵大小为:
text
n x n
原位置:
text
(i, j)
顺时针旋转 90 度后的位置是:
text
(j, n - 1 - i)
例如在 3 x 3 矩阵中:
text
matrix[2][0] = 7
旋转后去:
text
matrix[0][0]
代入公式:
text
(2, 0) -> (0, 3 - 1 - 2) -> (0, 0)
再例如:
text
matrix[0][0] = 1
旋转后去:
text
matrix[0][2]
代入公式:
text
(0, 0) -> (0, 3 - 1 - 0) -> (0, 2)
如果允许额外矩阵,可以直接根据这个映射写:
java
result[j][n - 1 - i] = matrix[i][j];
但这样需要 O(n^2) 额外空间,不满足题目原地要求。
4. 为什么不能直接在原矩阵按坐标赋值
假设直接写:
java
matrix[j][n - 1 - i] = matrix[i][j];
会覆盖掉仍然需要使用的旧值。
例如最开始把:
text
matrix[0][0] = 1
写到:
text
matrix[0][2]
原来的:
text
matrix[0][2] = 3
就丢失了,之后无法再把 3 移到它正确的位置。
因此必须通过交换,或者把整个变换拆成不会破坏信息的原地操作。
5. 把旋转拆成转置和每行翻转
第一步:沿主对角线转置
主对角线是:
text
左上 -> 右下
转置的规则是:
text
matrix[i][j] 和 matrix[j][i] 交换。
它的效果是行、列互换。
原矩阵:
text
1 2 3
4 5 6
7 8 9
转置后:
text
1 4 7
2 5 8
3 6 9
第二步:翻转每一行
对转置后的每一行进行左右翻转:
text
1 4 7 -> 7 4 1
2 5 8 -> 8 5 2
3 6 9 -> 9 6 3
最终:
text
7 4 1
8 5 2
9 6 3
这正是原矩阵顺时针旋转 90 度后的结果。
因此:
text
顺时针旋转 90 度 = 转置 + 翻转每一行。
6. 为什么这两步一定等价于顺时针旋转
从坐标角度看。
原元素的位置是:
text
(i, j)
经过转置
转置后位置是:
text
(j, i)
再经过每行左右翻转
行号不变,列下标 i 会变成:
text
n - 1 - i
最终位置是:
text
(j, n - 1 - i)
这和顺时针旋转 90 度的坐标规律完全一致:
text
(i, j) -> (j, n - 1 - i)
所以它不是巧合,而是两步坐标变换组合后正好等于目标变换。
7. 转置时为什么只遍历上三角
转置需要交换:
text
matrix[i][j] <-> matrix[j][i]
例如:
text
matrix[0][1] <-> matrix[1][0]
如果遍历完整矩阵,会发生两次交换:
text
处理 (0, 1):交换 2 和 4。
处理 (1, 0):又把 4 和 2 交换回来。
最终等于没交换。
因此只遍历主对角线的上方区域:
java
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 交换 matrix[i][j] 和 matrix[j][i]。
}
}
j = i + 1 的含义是:
text
j == i:主对角线元素,和自己交换没有意义。
j < i:下三角区域,对应交换已经在上三角完成。
j > i:只处理上三角,每对元素恰好交换一次。
8. Java 代码完整注释
java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 第一步:沿主对角线转置。
// 只遍历上三角,避免同一对元素交换两次。
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 第二步:翻转每一行。
// 转置后再左右翻转每一行,就得到顺时针旋转 90 度的结果。
for (int i = 0; i < n; i++) {
int left = 0;
int right = n - 1;
// 双指针交换当前行两端元素,实现左右翻转。
while (left < right) {
int temp = matrix[i][left];
matrix[i][left] = matrix[i][right];
matrix[i][right] = temp;
left++;
right--;
}
}
}
}
9. 用 4 x 4 矩阵验证
原矩阵:
text
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
先转置:
text
1 5 9 13
2 6 10 14
3 7 11 15
4 8 12 16
再翻转每一行:
text
13 9 5 1
14 10 6 2
15 11 7 3
16 12 8 4
这就是原矩阵顺时针旋转 90 度后的结果。
10. 复杂度分析
转置需要交换主对角线上方的元素,数量约为:
text
n * (n - 1) / 2
翻转每一行也会处理约:
text
n * n / 2
因此时间复杂度是:
text
O(n^2)
这是合理且最优的,因为矩阵本身就有 n^2 个元素,至少需要处理它们。
额外空间复杂度是:
text
O(1)
只使用了 n、temp、left、right 等常数个变量,没有创建新矩阵。
11. 总结
这题属于:
text
矩阵 / 原地变换 / 坐标映射
顺时针旋转 90 度的坐标规律是:
text
(i, j) -> (j, n - 1 - i)
原地实现不直接逐个按坐标赋值,而是把操作拆成:
text
先转置: (i, j) -> (j, i)
再翻转每一行:列 i -> n - 1 - i
合起来刚好得到:
text
(i, j) -> (j, n - 1 - i)
一句话记忆:
text
顺时针旋转 90 度:先沿主对角线转置,再翻转每一行。