当前位置: 首页 > news >正文

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

一、题目背景

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

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

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

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

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

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

http://www.jsqmd.com/news/867933/

相关文章:

  • QuantDinger 本地部署实战:5 分钟跑通 AI 量化系统,值不值?
  • 收藏!2026年AI风口来袭,普通人也能抓住高薪机会,附7步学AI路线图
  • 熵与编码:工业数据压缩的数学奥秘
  • 深入理解关系数据库三范式
  • 气动黄油机核心技术解析:泵的选择与厂家评估方法论
  • 东莞AI培训排名情况分析与技术问题排查实践
  • 口碑好的经销商管理系统哪家
  • NotebookLM样本量计算实战手册(含Python自动计算脚本+置信度校验表)
  • Keil MDK中实现原始以太网数据接收与协议处理
  • 微信小程序年度费用全拆解:SaaS、开源与定制开发的3年成本实测对比
  • 指针(一)
  • 推荐1款提升办公效率神器,文件(夹)批量重命名工具
  • Servlet 表单数据处理指南
  • 独立开发者如何利用Taotoken一站式解决模型选型与接入难题
  • 超低功耗语音识别加速器:SNN与硬件协同设计
  • 从技术实现角度聊聊全屋定制:一套柜子的品质由哪些底层因素决定
  • 2026年近期青少年自行车厂家综合实力评估与联系指南 - 2026年企业推荐榜
  • 《PHP 测验》
  • 大模型提示词压缩技术全景:五大类方法解析与应用指南
  • 20251910 2025-2026-2 《网络攻防实践》第8次作业
  • 大模型推理平台优选推荐榜单——白菜大模型推理平台深度评测与选型指南
  • 2026 年 GPT-5.5 技术架构与模型分层定价:mini 与 nano 版本的取舍逻辑
  • Cortex-M7 AXI接口设计与性能优化指南
  • MMU初始化与预测执行:避免系统崩溃的关键细节
  • 受众洞察 vs 传统市场调研:2026 年决策者指南
  • 沙伯基础创新塑料:高性能工程材料解决方案解析
  • OpenAI 与 Anthropic 财务大比拼:一家亏损求上市,一家盈利逆袭在望!
  • 剪映草稿批量导出工具使用分享,剪映导出还在一条一条点?教你用批处理告别重复操作
  • AXI协议中地址与数据顺序问题解析
  • 实测!朱自清散文AI率超60%?2026年AIGC检测技术局限与降痕方案全解析