旋转图像:从矩阵转置、镜像到坐标变换的系统理解
一、题目背景
LeetCode 48「旋转图像」要求我们将一个n × n的二维矩阵顺时针旋转 90 度。
题目给定一个矩阵matrix,它表示一张图像,其中每个元素可以理解为图像中的一个像素点。
要求如下:
给定一个n × n的二维矩阵matrix,将图像顺时针旋转 90 度。
并且必须满足一个重要限制:
必须原地旋转矩阵,不能额外创建另一个矩阵。
也就是说,我们不能新建一个new_matrix来保存旋转后的结果,而是要直接修改原来的matrix。
二、题目示例
示例一
原矩阵:
[ [1,2,3], [4,5,6], [7,8,9] ]顺时针旋转 90 度后:
[ [7,4,1], [8,5,2], [9,6,3] ]可以直观理解为:
1 2 3 7 4 1 4 5 6 -> 8 5 2 7 8 9 9 6 3示例二
原矩阵:
[ [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] ]三、矩阵旋转的核心:坐标映射
想真正理解矩阵旋转,不能只记代码,更要理解元素坐标是如何变化的。
假设矩阵中某个元素位于:
matrix[i][j]其中:
i表示行号;j表示列号;行号从上到下递增;
列号从左到右递增。
对于一个n × n的方阵来说,顺时针旋转 90 度后,原来位于(i, j)的元素,会移动到:
(j, n - 1 - i)也就是说:
new_matrix[j][n - 1 - i] = matrix[i][j]这就是顺时针旋转 90 度最本质的坐标公式。
例如对于:
1 2 3 4 5 6 7 8 9元素1原来位于(0,0)。
顺时针旋转 90 度后:
new_matrix[0][2] = matrix[0][0]所以1会移动到第一行最后一列。
这正好符合结果:
7 4 1 8 5 2 9 6 3四、常见矩阵变换公式总结
在理解旋转之前,我们先系统整理几种基础矩阵操作。
1. 矩阵转置
矩阵转置就是把矩阵的行和列交换。
核心公式:
matrix[i][j] ↔ matrix[j][i]例如:
1 2 3 4 5 6 7 8 9转置后变成:
1 4 7 2 5 8 3 6 9也就是主对角线两侧的元素互换。
在代码中,转置方阵时需要注意:
for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { swap(matrix[i][j], matrix[j][i]); } }这里j必须从i + 1开始。
原因是:
主对角线上的元素不用交换;
如果从
0开始,会导致同一对元素被交换两次,最终等于没有交换。
2. 水平镜像
水平镜像也叫左右翻转。
它表示每一行内部的元素进行左右交换。
核心公式:
matrix[i][j] ↔ matrix[i][n - 1 - j]例如:
1 2 3 4 5 6 7 8 9水平镜像后:
3 2 1 6 5 4 9 8 7可以理解为矩阵沿着中间的竖线进行翻转。
代码写法:
for (int i = 0; i < n; i++) { for (int j = 0; j < n / 2; j++) { swap(matrix[i][j], matrix[i][n - 1 - j]); } }3. 垂直镜像
垂直镜像也叫上下翻转。
它表示矩阵的行与行之间进行上下交换。
核心公式:
matrix[i][j] ↔ matrix[m - 1 - i][j]如果是n × n方阵,也可以写成:
matrix[i][j] ↔ matrix[n - 1 - i][j]例如:
1 2 3 4 5 6 7 8 9垂直镜像后:
7 8 9 4 5 6 1 2 3代码写法:
for (int i = 0; i < n / 2; i++) { for (int j = 0; j < n; j++) { swap(matrix[i][j], matrix[n - 1 - i][j]); } }也可以直接交换整行:
for (int i = 0; i < n / 2; i++) { swap(matrix[i], matrix[n - 1 - i]); }交换整行更简洁,效率也很好。
五、顺时针旋转 90 度
顺时针旋转 90 度的坐标变化公式是:
new_matrix[j][n - 1 - i] = matrix[i][j]这表示原来在(i, j)的元素,旋转后会来到(j, n - 1 - i)。
不过题目要求原地旋转,不能直接创建new_matrix。
所以我们需要把这个坐标变化拆成两个可以原地完成的基础操作。
顺时针旋转 90 度有两种常见拆法。
方法一:先转置,再水平镜像
原矩阵:
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这正好就是顺时针旋转 90 度的结果。
所以:
顺时针旋转 90 度 = 转置 + 水平镜像这是 LeetCode 48 最经典、最常用的解法。
方法二:先垂直镜像,再转置
还是原矩阵:
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也得到了顺时针旋转 90 度的结果。
所以:
顺时针旋转 90 度 = 垂直镜像 + 转置需要注意的是,这里的操作顺序不能随便交换。
垂直镜像 + 转置和
转置 + 垂直镜像得到的结果并不一样。
矩阵变换中,操作顺序非常重要。
六、顺时针旋转 180 度
顺时针旋转 180 度时,原来位于(i, j)的元素会移动到:
(n - 1 - i, n - 1 - j)对应公式是:
new_matrix[n - 1 - i][n - 1 - j] = matrix[i][j]它可以拆解为:
顺时针旋转 180 度 = 水平镜像 + 垂直镜像或者:
顺时针旋转 180 度 = 垂直镜像 + 水平镜像这两个顺序在 180 度旋转中效果相同。
例如:
1 2 3 4 5 6 7 8 9先水平镜像:
3 2 1 6 5 4 9 8 7再垂直镜像:
9 8 7 6 5 4 3 2 1这就是旋转 180 度后的结果。
七、逆时针旋转 90 度
逆时针旋转 90 度的坐标变化公式是:
new_matrix[n - 1 - j][i] = matrix[i][j]也就是说,原来的(i, j)会来到(n - 1 - j, i)。
它也可以拆成两个原地操作。
方法一:先转置,再垂直镜像
原矩阵:
1 2 3 4 5 6 7 8 9第一步:转置。
1 4 7 2 5 8 3 6 9第二步:垂直镜像。
3 6 9 2 5 8 1 4 7这就是逆时针旋转 90 度的结果。
方法二:先水平镜像,再转置
原矩阵:
1 2 3 4 5 6 7 8 9第一步:水平镜像。
3 2 1 6 5 4 9 8 7第二步:转置。
3 6 9 2 5 8 1 4 7所以:
逆时针旋转 90 度 = 水平镜像 + 转置八、所有矩阵变换关系汇总
为了方便记忆,可以整理成下面这张表。
| 操作 | 坐标变化 | 可拆解操作 |
|---|---|---|
| 转置 | (i, j) -> (j, i) | 主对角线交换 |
| 水平镜像 | (i, j) -> (i, n - 1 - j) | 每一行左右翻转 |
| 垂直镜像 | (i, j) -> (n - 1 - i, j) | 上下行交换 |
| 顺时针 90 度 | (i, j) -> (j, n - 1 - i) | 转置 + 水平镜像 |
| 顺时针 90 度 | (i, j) -> (j, n - 1 - i) | 垂直镜像 + 转置 |
| 逆时针 90 度 | (i, j) -> (n - 1 - j, i) | 转置 + 垂直镜像 |
| 逆时针 90 度 | (i, j) -> (n - 1 - j, i) | 水平镜像 + 转置 |
| 旋转 180 度 | (i, j) -> (n - 1 - i, n - 1 - j) | 水平镜像 + 垂直镜像 |
这张表非常重要。
它可以帮助我们从“死记硬背代码”升级到“理解矩阵坐标变换”。
九、LeetCode 48 标准 C++ 解法
下面给出最经典的写法:
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Solution { public: void rotate(vector<vector<int>>& matrix) { int n = matrix.size(); // 第一步:矩阵转置 // matrix[i][j] 与 matrix[j][i] 交换 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { swap(matrix[i][j], matrix[j][i]); } } // 第二步:水平镜像 // 每一行左右翻转 for (int i = 0; i < n; i++) { reverse(matrix[i].begin(), matrix[i].end()); } } };这段代码的本质就是:
顺时针旋转 90 度 = 转置 + 水平镜像十、手写水平镜像版本
如果不使用reverse,也可以手写交换过程:
class Solution { public: void rotate(vector<vector<int>>& matrix) { int n = matrix.size(); // 1. 转置 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { swap(matrix[i][j], matrix[j][i]); } } // 2. 水平镜像 for (int i = 0; i < n; i++) { for (int j = 0; j < n / 2; j++) { swap(matrix[i][j], matrix[i][n - 1 - j]); } } } };这种写法更适合初学者理解每一步到底在交换什么。
十一、使用“垂直镜像 + 转置”的写法
根据前面的推导,顺时针旋转 90 度也可以写成:
顺时针旋转 90 度 = 垂直镜像 + 转置代码如下:
class Solution { public: void rotate(vector<vector<int>>& matrix) { int n = matrix.size(); // 第一步:垂直镜像,上下翻转 for (int i = 0; i < n / 2; i++) { swap(matrix[i], matrix[n - 1 - i]); } // 第二步:转置 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { swap(matrix[i][j], matrix[j][i]); } } } };这种写法和 Python 中常见的一行写法非常接近。
十二、Python 一行写法的本质
很多 Python 代码会这样写:
matrix[:] = [list(row) for row in zip(*matrix[::-1])]这行代码看起来很短,但其实信息量很大。
我们拆开来看。
1. matrix[::-1]
matrix[::-1]表示将矩阵的行反转。
也就是垂直镜像。
例如:
matrix = [ [1,2,3], [4,5,6], [7,8,9] ]执行:
matrix[::-1]得到:
[ [7,8,9], [4,5,6], [1,2,3] ]2. zip(*matrix[::-1])
*表示解包。
zip(*matrix[::-1])等价于把每一行展开后传入zip。
例如:
zip([7,8,9], [4,5,6], [1,2,3])得到:
(7,4,1) (8,5,2) (9,6,3)这一步本质上就是转置。
3. list(row)
因为zip得到的是元组,所以需要转换成列表:
[list(row) for row in zip(*matrix[::-1])]结果是:
[ [7,4,1], [8,5,2], [9,6,3] ]4. matrix[:] = ...
最后这一点非常关键。
matrix[:] = ...不是简单地让matrix指向一个新对象,而是修改原列表的内容。
如果写成:
matrix = [list(row) for row in zip(*matrix[::-1])]这只是让局部变量matrix指向了一个新的列表,并不一定能修改调用者传进来的原矩阵。
而 LeetCode 要求原地修改,所以 Python 中应该写:
matrix[:] = [list(row) for row in zip(*matrix[::-1])]它的本质是:
垂直镜像 + 转置也就是顺时针旋转 90 度。
十三、为什么不能直接用 new_matrix?
最容易想到的做法是创建一个新矩阵:
new_matrix[j][n - 1 - i] = matrix[i][j];这确实可以得到正确答案。
但是题目明确要求:
必须原地旋转也就是说,空间复杂度应该是O(1)。
如果新建一个n × n矩阵,那么空间复杂度就是:
O(n^2)不符合题目要求。
因此,我们要用转置、镜像这种可以原地完成的操作。
十四、复杂度分析
对于n × n的矩阵:
时间复杂度
转置需要遍历矩阵上三角区域,大约访问:
n * (n - 1) / 2个元素。
水平镜像需要遍历:
n * (n / 2)个元素。
整体时间复杂度为:
O(n^2)因为矩阵中共有n^2个元素,旋转一张图像至少也要访问大部分元素。
空间复杂度
整个过程中只使用了常数个临时变量进行交换。
所以空间复杂度为:
O(1)这正好满足题目要求的原地旋转。
十五、易错点总结
1. 转置时不能从j = 0开始
错误写法:
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { swap(matrix[i][j], matrix[j][i]); } }这样会导致每一对元素被交换两次。
例如matrix[0][1]和matrix[1][0]:
第一次交换后位置变了,后面又会被交换回来。
正确写法应该是:
for (int j = i + 1; j < n; j++)2. 顺时针和逆时针不要混淆
顺时针 90 度:
(i, j) -> (j, n - 1 - i)逆时针 90 度:
(i, j) -> (n - 1 - j, i)二者非常像,但位置刚好相反。
3. 镜像方向不要搞错
水平镜像是左右翻转:
matrix[i][j] ↔ matrix[i][n - 1 - j]垂直镜像是上下翻转:
matrix[i][j] ↔ matrix[n - 1 - i][j]可以这样记:
水平镜像:行号不变,列号变化;
垂直镜像:列号不变,行号变化。
4. 操作顺序很重要
下面这些是不同结果:
转置 + 水平镜像 = 顺时针 90 度水平镜像 + 转置 = 逆时针 90 度垂直镜像 + 转置 = 顺时针 90 度转置 + 垂直镜像 = 逆时针 90 度矩阵变换一般不满足交换律。
也就是说:
A 操作 + B 操作不一定等于:
B 操作 + A 操作这是矩阵旋转问题中非常重要的思想。
十六、从图像角度理解矩阵旋转
这道题虽然是算法题,但它本质上和图像处理非常接近。
在图像中,每一个像素点都有自己的坐标。
假设一个像素位于:
(row, col)顺时针旋转 90 度之后,它的新位置就是:
(col, n - 1 - row)所以图像旋转并不是简单地改变数据顺序,而是在做一次坐标系统变换。
矩阵中的每一个元素,都按照统一的规则移动到新的位置。
掌握这个坐标映射公式后,不仅可以解决 LeetCode 48,也可以推广到图像处理、数组变换、坐标旋转等问题中。
十七、完整测试代码
下面给出一份可以直接运行的 C++ 测试代码。
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Solution { public: void rotate(vector<vector<int>>& matrix) { int n = matrix.size(); // 1. 转置矩阵 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { swap(matrix[i][j], matrix[j][i]); } } // 2. 水平镜像 for (int i = 0; i < n; i++) { reverse(matrix[i].begin(), matrix[i].end()); } } }; void printMatrix(const vector<vector<int>>& matrix) { for (const auto& row : matrix) { for (int num : row) { cout << num << " "; } cout << endl; } } int main() { Solution solution; vector<vector<int>> matrix1 = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; cout << "原矩阵:" << endl; printMatrix(matrix1); solution.rotate(matrix1); cout << "顺时针旋转 90 度后:" << endl; printMatrix(matrix1); cout << endl; vector<vector<int>> matrix2 = { {5, 1, 9, 11}, {2, 4, 8, 10}, {13, 3, 6, 7}, {15, 14, 12, 16} }; cout << "原矩阵:" << endl; printMatrix(matrix2); solution.rotate(matrix2); cout << "顺时针旋转 90 度后:" << endl; printMatrix(matrix2); return 0; }运行结果:
原矩阵: 1 2 3 4 5 6 7 8 9 顺时针旋转 90 度后: 7 4 1 8 5 2 9 6 3 原矩阵: 5 1 9 11 2 4 8 10 13 3 6 7 15 14 12 16 顺时针旋转 90 度后: 15 13 2 5 14 3 4 1 12 6 8 9 16 7 10 11十八、总结
LeetCode 48「旋转图像」表面上是一道数组模拟题,但它真正考察的是对矩阵坐标变换的理解。
这道题的核心公式是:
new_matrix[j][n - 1 - i] = matrix[i][j]也就是:
(i, j) -> (j, n - 1 - i)为了满足原地旋转的要求,我们不能创建额外矩阵,而是将旋转拆解为两个基础操作:
顺时针旋转 90 度 = 转置 + 水平镜像或者:
顺时针旋转 90 度 = 垂直镜像 + 转置进一步推广:
逆时针旋转 90 度 = 转置 + 垂直镜像或者:
逆时针旋转 90 度 = 水平镜像 + 转置旋转 180 度 = 水平镜像 + 垂直镜像这类题目最重要的不是背代码,而是理解三个问题:
第一,元素原来的坐标是什么。
第二,变换后元素的新坐标是什么。
第三,如何用原地操作模拟这个坐标变化。
只要掌握了坐标映射,矩阵旋转、矩阵翻转、图像变换这类问题都会变得非常清晰。
