一、题目背景
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 度 = 水平镜像 + 垂直镜像
这类题目最重要的不是背代码,而是理解三个问题:
第一,元素原来的坐标是什么。
第二,变换后元素的新坐标是什么。
第三,如何用原地操作模拟这个坐标变化。
只要掌握了坐标映射,矩阵旋转、矩阵翻转、图像变换这类问题都会变得非常清晰。