旋转图像:矩阵原地旋转的位移数学推导
旋转图像:矩阵原地旋转的位移数学推导
适读人群:Java 开发者、准备算法面试的工程师 | 阅读时长:约18分钟 | 难度:Medium
开篇故事
做游戏开发的同学小周曾经问我一道算法题,他说他们的手游需要支持图片旋转功能,像素矩阵顺时针旋转 90 度,要求原地完成(不能开辟新矩阵,内存受限的嵌入式环境)。
他当时想到的方法是先转置再翻转,他觉得这个应该是正确的,但自己推导出来之后不太确定。我说:"你想到的方法对,转置+水平翻转等于顺时针旋转 90 度,这是 LeetCode 48 的标准解法。"
他松了口气,然后问:"为什么?怎么证明它等价?"
这个问题问得好。很多人背了"转置+翻转"这个结论,但不知道为什么,导致遇到变体(逆时针旋转、旋转 180 度)就不会推导了。今天我把数学推导完整写出来。
一、题目解析
LeetCode 48. 旋转图像(Rotate Image)
给定一个 n×n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。必须在原地旋转图像,即直接修改输入的二维矩阵,不要使用另一个矩阵来旋转图像。
输入输出示例:
示例 1(3×3):
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]示例 2(4×4):
输入: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]]考察的核心知识点:
- 矩阵旋转的坐标变换公式
- 转置(Transpose)操作
- 原地操作:四个元素一组循环交换 vs 转置+翻转
二、数学推导:旋转的坐标变换
n×n 矩阵,顺时针旋转 90 度后,位置 (i, j) 的元素移到哪里?
原位置 (i, j)(以左上角为原点,行向下,列向右):
- 行坐标 i,列坐标 j
- 相对中心的坐标:(i - (n-1)/2, j - (n-1)/2)
顺时针旋转 90 度的变换(x, y → y, -x):
- 新坐标 = (j - (n-1)/2, -(i - (n-1)/2))
- 换回矩阵坐标:新行 = j,新列 = (n-1) - i
所以:原 (i, j) 的元素 → 新 (j, n-1-i)
验证:原 (0, 0) → 新 (0, n-1),原 (0, n-1) → 新 (n-1, n-1),原 (n-1, n-1) → 新 (n-1, 0),原 (n-1, 0) → 新 (0, 0)。画一个 3×3 矩阵验证,完全正确。
三、解法一:借助额外矩阵(O(n²) 空间)
直接按公式 result[j][n-1-i] = matrix[i][j] 计算新矩阵:
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
int[][] temp = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
temp[j][n - 1 - i] = matrix[i][j];
}
}
// 将结果复制回 matrix
for (int i = 0; i < n; i++) {
System.arraycopy(temp[i], 0, matrix[i], 0, n);
}
}
}时间复杂度:O(n²),空间复杂度:O(n²)。不满足原地要求。
四、解法二:四元素循环交换(原地,O(1) 空间)
每次旋转时,四个位置构成一个循环:(i,j) → (j, n-1-i) → (n-1-i, n-1-j) → (n-1-j, i) → (i,j)。每次操作把这四个位置的元素轮转,只用一个临时变量。
只需遍历矩阵的四分之一(左上角),避免重复操作:
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 只需处理左上角 (n/2 行) × (n - n/2 列) 的元素
for (int i = 0; i < n / 2; i++) {
for (int j = i; j < n - 1 - i; j++) {
// 保存左上角元素
int temp = matrix[i][j];
// 左下角 → 左上角
matrix[i][j] = matrix[n - 1 - j][i];
// 右下角 → 左下角
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
// 右上角 → 右下角
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
// temp(原左上角)→ 右上角
matrix[j][n - 1 - i] = temp;
}
}
}
}时间复杂度:O(n²),空间复杂度:O(1)。
五、解法三:转置+水平翻转(工业级最简解)
数学证明
转置(Transpose):matrix[i][j] 和 matrix[j][i] 互换,即沿主对角线翻转。
转置后位置 (i, j) 的元素来自原来的 (j, i)。
水平翻转(Flip Horizontally):每行左右翻转,matrix[i][j] 换到 matrix[i][n-1-j]。
两步合成:原 (i, j) → 转置后 (j, i) → 水平翻转后 (j, n-1-i)。
这正好是顺时针旋转 90 度的公式 (i, j) → (j, n-1-i)!证明完毕。
各种旋转操作对应:
- 顺时针 90°:转置 + 水平翻转(每行翻转)
- 逆时针 90°:转置 + 垂直翻转(每列翻转,即上下颠倒每行)
- 旋转 180°:水平翻转 + 垂直翻转(或者用两次 90° 旋转)
完整 Java 代码
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 第一步:转置(沿主对角线翻转)
// 只需遍历上三角(j 从 i+1 开始)
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;
}
}
// 第二步:水平翻转(每行左右翻转)
for (int i = 0; i < n; i++) {
int left = 0, right = n - 1;
while (left < right) {
int temp = matrix[i][left];
matrix[i][left] = matrix[i][right];
matrix[i][right] = temp;
left++;
right--;
}
}
}
}复杂度分析
时间复杂度:O(n²)
转置遍历上三角:n(n-1)/2 次操作。水平翻转:n 行,每行 n/2 次操作,共 n²/2 次。总体 O(n²)。
空间复杂度:O(1)
只用了临时变量,原地操作。
我在 LeetCode 提交,运行时间 0ms,击败 100% 的 Java 用户。
六、举一反三
同类型题目
LeetCode 54. 螺旋矩阵
下一篇的主题,矩阵遍历的经典题。
LeetCode 59. 螺旋矩阵 II
生成螺旋矩阵,和 54 是互逆操作。
LeetCode 73. 矩阵置零
如果矩阵某位置是 0,则将其所在行和列都置零。
逆时针旋转 90° 代码:
// 逆时针旋转 = 转置 + 上下翻转(垂直翻转)
// 转置(同上)...
// 垂直翻转:每列上下翻转
for (int j = 0; j < n; j++) {
int top = 0, bottom = n - 1;
while (top < bottom) {
int temp = matrix[top][j];
matrix[top][j] = matrix[bottom][j];
matrix[bottom][j] = temp;
top++; bottom--;
}
}七、踩坑实录
坑一:四元素循环交换的 j 起始值
现象: 某些元素被交换了两次(等于没交换),结果不对。
原因: 外层循环 j 从 0 开始而不是从 i 开始,导致处于对角线上的元素被交换了两次(自己和自己交换了两遍),相当于没变。
解法: j 从 i 开始(每个对角线上的元素只处理一次)。
坑二:转置时遍历了整个矩阵
现象: 转置后矩阵反而和原来一样(被转置了两次等于没转置)。
原因: 转置时 j 从 0 到 n-1,遍历了所有元素,导致 (i,j) 和 (j,i) 被交换了两次(一次在处理 (i,j) 时,一次在处理 (j,i) 时),恢复原样。
解法: 转置时 j 从 i+1 开始(只遍历上三角),每对元素只交换一次。
坑三:误以为"转置+水平翻转"是"逆时针旋转"
现象: 以为两种旋转方向用同一套操作,导致实现了逆时针旋转但题目要求顺时针。
原因: 顺时针旋转 = 转置 + 水平翻转(每行左右翻转);逆时针旋转 = 转置 + 垂直翻转(每列上下翻转)。两者是不同的。
解法: 记住推导过程:顺时针旋转后 (i,j) 去 (j, n-1-i),转置操作使 (i,j) 去 (j,i),再水平翻转 (j,i) 去 (j, n-1-i),两步合成正好是顺时针旋转。
八、总结
旋转图像这道题展示了"操作分解"的思想:复杂的原地变换可以分解为两个简单的原地操作(转置 + 翻转),每步都只用 O(1) 空间。
三条核心要点:
顺时针旋转 90° 的坐标公式:
(i, j) → (j, n-1-i),这是所有解法的数学基础。转置 + 水平翻转 = 顺时针旋转 90°,可以数学证明。记住这个结论,还要会推导逆时针(转置+垂直翻转)和旋转 180°(水平翻转+垂直翻转)。
四元素循环交换的循环范围:外层
i从 0 到 n/2-1,内层j从 i 到 n-2-i,确保不重复处理任何元素。
延伸思考:
这道题的"操作分解"思想在矩阵算法里很通用:复杂的矩阵变换往往可以分解为转置、水平翻转、垂直翻转的组合。理解了这个,处理各种旋转角度(45°, 90°, 180°, 270°)都能从数学角度推导出对应的分解方式。
