旋转图像:从矩阵转置、镜像到坐标变换的系统理解

一、题目背景

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 度 = 水平镜像 + 垂直镜像

这类题目最重要的不是背代码,而是理解三个问题:

第一,元素原来的坐标是什么。

第二,变换后元素的新坐标是什么。

第三,如何用原地操作模拟这个坐标变化。

只要掌握了坐标映射,矩阵旋转、矩阵翻转、图像变换这类问题都会变得非常清晰。

相关推荐
家有两宝,感恩遇见8 小时前
windows安装claude code
windows
fengenrong8 小时前
20260522
算法
一条大祥脚9 小时前
Codeforces Round 1099 (Div. 2) 构造|贪心|图论|还原数组
java·算法·图论
yaoxin5211239 小时前
414. Java 文件操作基础 - 批量压缩与索引:将154首十四行诗高效存储为带目录的二进制文件
java·windows·python
咩咦9 小时前
C++学习笔记23:const 成员函数
c++·学习笔记·类和对象·const·this指针·const成员函数
Sheldon Chao9 小时前
Lecture 7 基于策略梯度的算法
人工智能·算法·机器学习
早睡身体真不戳9 小时前
Cacheable List 接口二次读取报错
windows
始三角龙9 小时前
LeetCode hoot 100 -- 缺失的第一个正整数
算法·leetcode·职场和发展
shandianchengzi9 小时前
【记录】联想|Thinkpad雷电接口连接带鱼屏显示器,画面被异常拉伸怎么解决?
windows·计算机外设·显示器·联想·thinkpad