ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

旋转图像为什么难?从坐标变换到原地算法,LeetCode48题彻底拆解

旋转图像为什么难?从坐标变换到原地算法,LeetCode48题彻底拆解 我第一次刷到“旋转图像”这道题第一反应是“这有什么好考的”结果真动手写的时候愣是卡了十几分钟。LeetCode Hot100 第20题48. Rotate Image难度标着 Medium但很多人在面试里就栽在“看着简单、写起来全是边界问题”。说白了题目要求你把一个 n × n 的矩阵顺时针旋转 90 度进阶要求是必须原地修改不能开新矩阵。这个“原地”两个字才是整道题的灵魂。这篇文章我会从坐标变换的数学本质讲起把辅助矩阵解法、转置加翻转、四元素环换这三种思路全部拆开最后再聊代码里的边界陷阱和同类题目的扩展套路。适合正在刷 Hot100 的读者也适合面试前想彻底搞懂矩阵旋转的人。1. 先搞明白旋转90度到底对坐标做了什么1.1 矩阵坐标系里藏着一个反直觉的坑我们平时在数学课上学坐标系x 轴向右y 轴向上。但二维数组的下标完全不是一个逻辑matrix[i][j]里i是行号方向是向下的j是列号方向是向右的。也就是说你的“y 轴”是倒着的。这个区别直接导致一个现象很多人在脑子里模拟“顺时针旋转”的时候会把矩阵想象成一张图片然后用图片旋转的直觉去套结果坐标变换怎么都对不上。原因就是你把数学坐标系和矩阵下标混在一起了。所以做这类题的第一步不是背公式而是把坐标变换的规则严格建立在下标上。1.2 观察旋转前后的位置变化规律看一个 3 × 3 的例子1 2 3 4 5 6 7 8 9顺时针旋转 90 度之后变成7 4 1 8 5 2 9 6 3我们把几个关键位置的坐标变化列出来原位置旋转后位置(0, 0)(0, 2)(0, 1)(1, 2)(1, 0)(0, 1)(2, 0)(0, 0)(1, 1)(1, 1)盯着这个对照表看一会儿能发现两个规律第一原来的行号变成了新矩阵的列号。第 0 行的三个元素1 2 3旋转后变成了最后一列而且是倒着排的1在右上角2在右侧中间3在右下角。第二列号变化和原来的行号方向相反。原位置(i, j)旋转后变成了(j, n - 1 - i)。这个公式非常关键它把“顺时针旋转”翻译成了两个子操作行号等于原来的列号列号等于原来的行号倒数。1.3 用方向变化来理解而不是死记公式如果你觉得记(j, n-1-i)有点抽象可以换个角度理解。想象矩阵里有一个元素它右边有一个箭头。旋转 90 度之后这个箭头会指向下方。原来的行方向也就是“从上往下”的方向旋转后变成了“从右往左”的方向。坐标变化其实是跟着方向走的新的行号由原来的列号决定所以是j。新的列号和原来的行号有关但方向反转了所以是n-1-i。这就是为什么转置和翻转能组合出旋转效果转置本质上是“行和列互换”相当于把(i, j)变成(j, i)左右翻转则实现“列号倒序”把(j, i)变成(j, n-1-i)。两步一拼正好是旋转公式。这个理解方式还有个好处面试的时候你可以现场推导而不是背答案。面试官问“为什么这么做”你能讲清楚每一步的几何意义。2. 辅助矩阵版本五分钟能AC但面试官一定会追问2.1 最直接的坐标映射写法既然有了坐标变换公式最粗暴的解法就是开一个新矩阵把元素一个个放到目标位置上。class Solution: def rotate(self, matrix: List[List[int]]) - None: n len(matrix) new_matrix [[0] * n for _ in range(n)] for i in range(n): for j in range(n): new_matrix[j][n - 1 - i] matrix[i][j] for i in range(n): for j in range(n): matrix[i][j] new_matrix[i][j]这个写法的核心就一行new_matrix[j][n-1-i] matrix[i][j]。就是把公式直接搬过来。时间复杂度和空间复杂度都是 O(n²)。题目本身要求原地所以这个版本只能算是“验证思路”的第一步。2.2 面试官追问的潜台词如果你面试时候写这个版本面试官大概率会追问一句“能不能不用额外空间”这句话其实是在考察两件事第一你知不知道旋转是一个可以在原矩阵上通过交换完成的操作。辅助矩阵版本把每个元素直接“搬”到新位置看起来很自然但原地操作需要的是找出元素之间的轮转关系。第二你有没有意识到这两层循环可以拆解成“两次对称操作”。对称操作天然是交换两个位置的元素不需要额外空间。需要提醒的是辅助矩阵版本最后要把数据拷回原矩阵。这道题的题目要求是原地修改所以matrix[:] new_matrix这种写法可以但matrix new_matrix不行——后者只是让函数内部的局部变量指向了新对象外面的matrix根本没变。这个坑在 Python 里特别常见我在第 4 节还会再展开一次。3. 原地旋转的两个核心技术对称交换与四元素环换3.1 转置加翻转为什么等于旋转这是我认为整道题最值得记住的洞察旋转操作可以分解为两个镜像对称操作的组合。先看效果还是用 3 × 3 的例子。原矩阵1 2 3 4 5 6 7 8 9第一步沿主对角线转置。matrix[i][j]和matrix[j][i]交换得到1 4 7 2 5 8 3 6 9第二步左右翻转。每一行以中轴为基准左右交换7 4 1 8 5 2 9 6 3结果和旋转 90 度完全一致。用坐标公式验证一遍就清楚了转置(i, j) - (j, i)左右翻转(j, i) - (j, n-1-i)最终的(j, n-1-i)就是旋转公式。这个过程也可以用另一种顺序先上下翻转再转置。你自己在纸上推一遍会发现结果一样。但主流写法是转置 左右翻转因为转置的代码更好记。为什么对称操作能原地完成因为“沿对角线转置”和“左右翻转”本质上都是交换一对对称元素交换操作只需要一个临时变量不需要新开矩阵。也就是说你能用它组合出旋转是因为对称交换本身就满足原地条件。3.2 四元素环换法剥洋葱的直觉写法除了转置加翻转还有另一种原地思路把矩阵看成一圈一圈的洋葱从外到内逐层处理。每一层里的四个元素会依次轮转。举个例子最外层(0, 0)这个位置旋转后要接收原来(n-1, 0)位置的值而(n-1, 0)要接收(n-1, n-1)的值以此类推。四个位置组成一个环只需要一个临时变量就能完成轮转。class Solution: def rotate(self, matrix: List[List[int]]) - None: n len(matrix) for i in range(n // 2): for j in range(i, n - 1 - i): tmp 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] matrix[j][n - 1 - i] tmp这里循环范围的设计需要解释一下外层只走到n // 2因为剥到最中间就结束了内层j从i到n-2-i是因为每一层首尾位置会形成闭环最后一个位置不用单独处理。3.3 两种原地方案怎么选方案代码量理解难度出错概率面试表达转置 左右翻转短低低强四元素环换中中中中我自己更推荐转置加翻转。理由有三个第一它把“旋转”这个大问题拆成了两个“交换”小问题每一步都直观不容易写错。第二面试的时候讲“旋转 转置 翻转”面试官能立刻判断你对坐标变换理解到位。第三需要扩展到 180 度、270 度时组合思路可以直接复用。四元素环换的优点是代码只有一段但它需要你把四个位置的下标关系一次写对n-1-i、n-1-j、j、i四个下标搅在一起眼一花就写串了。如果你打算用环换法建议先背熟四个位置的轮转顺序特别是“左边接谁、右边接谁”的关系。4. 完整代码与边界细节这些坑不踩三次记不住4.1 转置 翻转的推荐实现Python 版本class Solution: def rotate(self, matrix: List[List[int]]) - None: n len(matrix) # 沿主对角线转置 for i in range(n): for j in range(i 1, n): matrix[i][j], matrix[j][i] matrix[j][i], matrix[i][j] # 左右翻转 for i in range(n): for j in range(n // 2): matrix[i][j], matrix[i][n - 1 - j] matrix[i][n - 1 - j], matrix[i][j]Java 版本class Solution { public void rotate(int[][] matrix) { int n matrix.length; for (int i 0; i n; i) { for (int j i 1; j n; j) { int tmp matrix[i][j]; matrix[i][j] matrix[j][i]; matrix[j][i] tmp; } } for (int i 0; i n; i) { for (int j 0; j n / 2; j) { int tmp matrix[i][j]; matrix[i][j] matrix[i][n - 1 - j]; matrix[i][n - 1 - j] tmp; } } } }4.2 转置循环里 j 的起点为什么是 i1这是新手最容易踩的坑。很多人写转置的时候内层循环习惯从 0 开始for i in range(n): for j in range(n): matrix[i][j], matrix[j][i] matrix[j][i], matrix[i][j]看起来没问题实际上你做了两遍交换。当i0, j1的时候(0,1)和(1,0)换了一次等i1, j0的时候这两个位置又换了一次等于换回去了。最后矩阵纹丝不动。正确的做法是让j从i 1开始。这样只扫描矩阵的右上三角每个位置恰好处理一次。对角线上的元素matrix[i][i]本来就不需要交换所以从i1开始也顺带跳过了对角线。4.3 n 为奇数或偶数时的边界验证左右翻转时内层循环是j n // 2这个写法不管是奇数还是偶数都成立。偶数n4时每行有 4 个元素只需要交换前两个和后两个j 0, 1正好覆盖。中心是两条中线没有单独的元素。奇数n5时每行 5 个元素中间的元素j2是不用动的。j 5 // 2也就是j 2只处理j0, 1中间元素天然跳过。转置也类似对角线上的元素不参与交换所以奇数或偶数矩阵都能处理。4.4 别急着用一行解原地修改的坑在 Python 的题解区经常能看到类似这种“一行解”matrix[:] zip(*matrix[::-1])跑通没问题结果也对但有几个问题值得你注意。第一它创建了新的列表对象空间复杂度是 O(n²)不符合题目的进阶要求“原地修改”。严格来说如果你在面试里写出这个答案面试官大概率会让你继续优化。第二zip(*matrix[::-1])返回的是迭代器直接赋值给matrix[:]的话需要确保外层是 list。实际测试中不同 Python 版本行为有差异而且用生成器做嵌套列表的转换可读性并不好。第三这个解法的推导过程对新手并不友好。它利用 Python 的切片和解包特性把矩阵旋转变成了“先反转行序再打包成列”。这其实是“上下翻转 转置”的组合和我们的“转置 左右翻转”本质上是一回事但如果你连坐标公式都没吃透看这种技巧只会增加混乱。我更建议在本地练习和面试时都用两层循环的版本。代码确实多几行但逻辑透明出错容易定位也更贴近面试官期望。5. 举一反三旋转90度、180度、270度以及同类矩阵题5.1 四种对称操作与旋转角度的对应关系掌握了转置、左右翻转、上下翻转这三种对称操作之后你可以组合出任意角度的旋转。我把关系整理成一张表旋转角度坐标变换公式推荐组合操作顺时针 90°(i, j) - (j, n-1-i)转置 左右翻转顺时针 180°(i, j) - (n-1-i, n-1-j)上下翻转 左右翻转顺时针 270°(i, j) - (n-1-j, i)左右翻转 转置360°不变不需要操作验证 180 度的组合上下翻转把(i, j)变成(n-1-i, j)左右翻转再变成(n-1-i, n-1-j)正好是中心对称也就是旋转 180 度。验证 270 度的组合左右翻转把(i, j)变成(i, n-1-j)转置把(i, n-1-j)变成(n-1-j, i)结果和逆时针旋转 90 度一致。这个表格不需要背你只需要记住一个原则**任何旋转都可以通过两次对称操作完成。**遇到变体题现场推一遍坐标公式就能确定组合顺序。5.2 从旋转图像到螺旋矩阵同类矩阵题的思维迁移Hot100 里的矩阵操作题不止这一道和它关联最紧密的是 54. 螺旋矩阵 和 59. 螺旋矩阵 II。54 题要求按螺旋顺序遍历一个 m × n 矩阵它考的是“方向控制 边界收缩”。你设置上下左右四个边界走到头了就换方向边界往内收缩一圈。这和我前面说的“剥洋葱”思路很像只是 48 题剥洋葱是为了交换元素54 题剥洋葱是为了遍历路径。59 题反过来要求生成一个螺旋矩阵。思路同样是边界收缩只是从遍历变成填数。这三道题放在一起刷性价比很高。因为它们都涉及矩阵坐标的精细操作刷完以后你对n-1-i、n-1-j这种下标变化会形成肌肉记忆后面遇到图像处理、棋盘类问题会快很多。5.3 面试现场怎么表达更容易通过如果你面试遇到这道题我建议按这个顺序回答第一先说清楚旋转的坐标变换公式(i, j)变成(j, n-1-i)。这能立刻证明你不是背题而是理解操作本质。第二提辅助矩阵解法可以做到 O(n²) 时间和 O(n²) 空间但如果要求原地就需要把它拆成两次对称操作。第三直接抛出转置 左右翻转方案并解释“对称操作为什么适合原地”。这是整道题最重要的加分点。第四面试官如果追问“还有没有别的做法”再提四元素环换法简单说明逐层处理即可。最后提醒一句如果面试官要求你手动跑一个 2 × 2 或 3 × 3 的例子不要慌。拿 2 × 2 验证边界最快比如1 2 3 4转置后1 3 2 4左右翻转后3 1 4 2再和坐标公式逐项对一下结果完全一致。这种小规模验证能帮你快速排查循环边界写错的问题。我个人刷题习惯是把矩阵类题目单独建一个标签每次做完顺手把坐标变换公式记在题解开头。回头再看笔记时不需要重新推导扫一眼公式就知道这道题在考什么。旋转图像这道题之所以值得反复琢磨不是因为它难而是因为它教会你一个很通用的思路**复杂的矩阵变换往往可以拆成几个简单的对称操作来完成。**这个思路能迁移到很多题目上远不止这一道。
返回列表