力扣48旋转图像:原地矩阵旋转的转置翻转法与逐圈循环详解

📅 发布时间:2026/10/5 3:13:34
力扣48旋转图像:原地矩阵旋转的转置翻转法与逐圈循环详解
力扣Hot 100刷到第20题迎面撞上的是这道被无数面试官翻牌的“旋转图像”。我第一次见它时心里想的是就这复制一份矩阵按公式把每个数填到新位置不就行了。但题目后面跟着的两个单词立刻让人冷静下来——原地旋转。它在LeetCode上的编号是48在Hot 100热题榜上排第20位几乎所有大厂算法题库里都留着它的位置属于那种“你不会做面试基本就凉一半”的基础题。这篇文章就把这道题彻底聊透。先讲清楚旋转背后的坐标规律再给出两种主流解法一种是转置加水平翻转上手最快一种是逐圈旋转法更贴近“旋转”的本质。我会把推导过程、代码细节、边界条件、常见坑全部摊开最后再聊聊面试现场怎么讲才能拿满分顺便把逆时针旋转、180度旋转和“旋转家族”的几道同源题目一起串一遍。无论你是刚入门刷题的新手还是准备冲刺一线大厂的老手这题都值得花半小时彻底吃透。1. 题目快速拆解旋转图像到底在考什么1.1 从例子入手先搞清楚输入输出长什么样题目描述很简短给定一个 n × n 的二维矩阵 matrix表示一个图像将图像顺时针旋转 90 度。注意要求原地旋转也就是不能额外开辟一个矩阵来做中转。力扣给出了一个很经典的例子输入matrix [[1,2,3],[4,5,6],[7,8,9]] 输出[[7,4,1],[8,5,2],[9,6,3]]从这个小例子能直观感受到旋转的规律原来第一行的 1、2、3旋转后变成了最后一列而且顺序是 1、2、3从上到下原来第一列的 1、4、7旋转后变成了第一行顺序变成了 7、4、1从右到左。很多第一次接触这题的人会对这种“行列互换顺序反转”的组合感到别扭但其实它背后就是一个非常简单的坐标映射。建议你拿到这个例子后先在草稿纸上画一个 3×3 的方格把每个元素原来的坐标 (i, j) 和旋转后的新坐标写下来多观察几秒。你会发现所有坐标变化都遵循同一个公式这个公式就是解决整个问题的钥匙也是后面两种解法的共同基础。1.2 坐标映射旋转90度本质就是一个公式如果原坐标是 (i, j)顺时针旋转 90 度之后它会落到哪个坐标结论非常干净新的行坐标是原来的列坐标 j新的列坐标是 n - 1 - i也就是倒数第 i 行写成公式就是(i, j) - (j, n - 1 - i)用 3×3 矩阵检验一下matrix[0][0] 的 1 旋转后应该到 (0, 2)也就是右上角matrix[0][2] 的 3 旋转后应该到 (2, 2)也就是右下角。代入公式完全吻合。这个公式能解释一切但面试的时候如果直接把它背出来容易显得像背题。更好的方式是向面试官解释清楚它怎么来的旋转 90 度意味着“原来的行变成了旋转后的列原来的列变成了旋转后的对称行”。你可以想象把一张纸顺时针旋转坐标轴也跟着转原来朝右的方向现在朝下原来朝下的方向现在朝左于是行号变为列号、列号变为倒数行号。一旦掌握了这个映射整个问题就变成了“如何把每个元素移动到它该去的位置并且不借助额外空间”。难点不在理解公式而在于原地替换时一个元素挪走之后它的位置会被另一个元素占用稍不留神就会覆盖掉还没处理的数据。这正是后文两套解法各自要解决的核心矛盾。1.3 为什么题目一定要限定 n × n 方阵很多人刷题时不会多想但这里其实藏着一个很好的面试加分点。题目特意说明是 n × n 的方阵而不是 m × n 的矩形这不是随意的约束而是“原地旋转”能否成立的前提。一个 m × n 矩阵旋转 90 度后形状会变成 n × m。比如一个 2×3 的矩阵旋转后是 3×2它根本没法塞回原来的内存布局里。所以原地旋转只能处理“旋转后形状不变”的方阵。这一点在面试时主动说出来非常加分它能证明你不是在机械背题而是真的想明白了题目设计背后的逻辑。后面在第五章我会再拓展一下非方阵旋转需要什么处理现在先记住方阵这个前提是整道题能原地完成的结构性原因。2. 解法一转置加水平翻转最容易写对的方案2.1 分解动作把“旋转”拆成两个最熟悉的操作先别急着写代码看一个小学数学级别的操作组合。顺时针旋转 90 度可以等价于连续执行两步第一步把矩阵转置也就是沿主对角线做对称交换让 matrix[i][j] 和 matrix[j][i] 互换第二步对每一行做水平翻转也就是把第 j 列元素和第 n - 1 - j 列元素互换我当年第一次看到“转置翻转”这个组合时第一反应是不太相信直到自己在草稿纸上推了一遍坐标才服气。用生活化的类比来说转置相当于把表格沿左上到右下的对角线“翻个面”水平翻转相当于把每一行沿着垂直中线“对折”。这两下叠在一起效果恰好就是整个矩阵被顺时针拧了 90 度像翻牌一样。这个方案最大的价值在于转置和行内反转都是你没有心理负担的操作写起来几乎不可能“逻辑卡壳”。相比之下直接模拟旋转环容易在四个下标之间绕晕。所以面试时我强烈建议先用这个方案把题做对、讲清楚有余力再展示第二种。2.2 坐标推导为什么组合起来恰好等于旋转90度直觉归直觉面试官极大概率会追问一句“为什么这两个操作合起来是正确的”。这时候你不能只回答“大家都这么说”而要把坐标推导摆出来。先说转置它把 (i, j) 变成 (j, i)。再说水平翻转把 (j, i) 变成 (j, n - 1 - i)。两者组合起来整体映射为(i, j) - (j, i) - (j, n - 1 - i)看到了吗最终结果恰好就是我们在第一节推导出的旋转公式。这说明“先转置再水平翻转”与“顺时针旋转90度”在数学上是完全等价的。更进一步如果你把顺序颠倒先水平翻转再转置会得到 (n - 1 - j, i)这恰好是逆时针旋转 90 度的映射。这个对比非常有意思也说明操作顺序很重要不能记混。建议你在纸上把 3×3 的矩阵按这两步走一遍先转置变成 [[1,4,7],[2,5,8],[3,6,9]]再水平翻转每一行变成 [[7,4,1],[8,5,2],[9,6,3]]。结果和题目输出一模一样。走完这一步之后你对这个解法的信任就不是“背来的”而是“推来的”面试时讲出来底气完全不一样。2.3 代码实现与三个易错细节Python 版本非常短class Solution: def rotate(self, matrix: List[List[int]]) - None: n len(matrix) # 第一步转置 for i in range(n): for j in range(i, 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]代码短但陷阱不少。至少三个细节必须注意。第一个细节转置时内层循环必须从 i 开始而不是从 0 开始。如果从 0 开始每对元素会被交换两次第一次把 matrix[i][j] 和 matrix[j][i] 交换后面跑到 (j, i) 这一对时又会交换回来等于白做。只有让 j i 起步才能保证每对元素只处理一次。第二个细节水平翻转时列只需要遍历到 n // 2。如果你傻乎乎地让 j 从 0 走到 n - 1那么前半段交换会生效后半段又交换回原位最终矩阵纹丝不动。n // 2 这个写法在 n 为奇数时自动跳过了正中间的元素因为它本来就不需要动。第三个细节Python 的异位交换写起来很爽matrix[i][j], matrix[j][i] matrix[j][i], matrix[i][j] 会先计算右侧的值再统一赋值不会出现覆盖问题。但如果你写 Java 或者 C别忘了准备临时变量class Solution { public void rotate(int[][] matrix) { int n matrix.length; for (int i 0; i n; i) { for (int j i; 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; } } } }语言特性不同但核心逻辑完全一致。写代码之前先在注释里标注“转置”和“水平翻转”两个阶段能让你的思路在面试时更清晰。3. 解法二逐圈旋转法还原旋转的本质3.1 把矩阵看成层层嵌套的“同心圈”第二种思路也很经典而且更贴近“旋转”这个词的字面意思。想象一个洋葱从外到内一层一层剥开矩阵也可以看作一圈一圈的“环”。旋转时最外面一圈上的元素互相交换位置然后往里一圈再做同样的事直到最中心的元素如果有的话停着不动。每一圈的四条边可以抽象成四个数组上边、右边、下边、左边。顺时针旋转就是让上边的第 i 个元素跑到右边右边的元素跑到下边下边跑到左边左边跑回上边。这一组四个元素需要“循环换位”听起来复杂但只要控制好每圈的宽度代码量其实不比第一种多多少。为什么这个方法能做到原地因为每一次替换都只涉及四个位置它们之间构成了一个完整的闭环不会碰到底层还没处理过的数据。这个“环”的视角从算法本质上看就是坐标置换被分解成若干个独立的循环后面我会再提。3.2 四元素轮换的下标设计从n3到n5逐个验证现在把下标问题彻底说清。用 left 和 right 表示当前圈的左右边界初始时 left 0right n - 1。每一圈里我们会处理 right - left 组元素。注意不是 right - left 个元素而是边长减一个“内点”因为四个角落各属于一条边不能被重复处理两次。对于每一组记上下边界分别为 top leftbottom right第 i 个位置涉及四个点上边matrix[top][left i]右边matrix[top i][right]下边matrix[bottom][right - i]左边matrix[bottom - i][left]把上边的值暂存到临时变量里然后依次做四步搬运左边搬到上边下边搬到左边右边搬到下边最后把暂存的上边值搬到右边。整体代码是class Solution: def rotate(self, matrix: List[List[int]]) - None: n len(matrix) left, right 0, n - 1 while left right: for i in range(right - left): top, bottom left, right tmp matrix[top][left i] matrix[top][left i] matrix[bottom - i][left] matrix[bottom - i][left] matrix[bottom][right - i] matrix[bottom][right - i] matrix[top i][right] matrix[top i][right] tmp left 1 right - 1用 n 3 验证一次最外层 left 0、right 2range(2) 处理 i 0 和 i 1。i 0 处理四个角上 1 到右上角 3 的位置左 7 到上 1 的位置下 9 到左 7 的位置右 3 到下 9 的位置完成一轮。i 1 处理四条边上的第二个元素上 2、右 6、下 8、左 4一轮交换后这四个元素也归位。此时 left 变成 1right 变成 1循环结束正中间元素 5 纹丝不动。整个过程走完矩阵正好旋转 90 度。n 5 时也一样外层处理 4 组元素left 和 right 各内缩一格后内层处理 2 组元素最后 left 2 时循环结束最中心元素不动。所有偶数维度的情况则会把每一层完整处理完没有遗留的“轴心”。3.3 两种解法怎么选面试时如何衔接先说结论面试首选解法一因为它可验证性强、不容易写错而且解释起来用转置和翻转两个常见操作就能让面试官立刻理解。解法二价值体现在“当你被追问还有没有别的方法”时你能展示出对旋转本质的理解。两个方案的时间复杂度和空间复杂度完全相同都是 O(n²) 时间和 O(1) 空间区别只在于编码复杂度和直觉性。如果你硬要问我的个人偏好我只能说解法二写起来是真的“爽”那种四个元素转一圈的感觉很符合直觉但下标一不小心就会出 bug尤其面试时一紧张很容易把 right - i 写成 i。稳妥起见我会在代码里先把 right - left 用 t 存起来再用 left i、top i 这样的组合逐步写入减少跳步。另外还有一个隐藏加分动作如果你先写出解法一再补一句“其实旋转可以看成坐标置换的循环分解按圈做四元素轮换也能原地完成”会让面试官觉得你对线性代数的本质有体感。面试中很少要求两种都写但“知道另一种写法”和“完全不知道”带来的印象分差距是巨大的。4. 边界条件、复杂度与常见坑一次性说清4.1 n0、n1、奇数与偶数边界条件一次过真题的测试用例可不止 3×3暴力验证会让你发现很多“看起来没问题但一跑就挂”的情况。先把边界情况列全n 0空矩阵。len(matrix) 为 0外层循环根本不会进入转置和翻转逻辑直接跳过程序安全结束。n 1只有一个元素。转置循环 i 0、j 0 会和自己交换一次水平翻转 n // 2 0 不执行结果不变完全正确。n 为奇数正中间的元素在旋转中保持不动。解法一里它会在转置时和自己交换水平翻转时也因为 n // 2 的取整被跳过解法二里它会成为最内层 left right 时不被处理的“核心”。n 为偶数没有中心元素所有元素都会被正确地搬运一遍解法二的 while left right 会在最后一层成功处理完所有数据后退出。不管哪种情况上面两段代码都能天然适应不需要额外写 if 特判。这也是我推荐你优先记牢这两版实现的原因之一逻辑统一不容易漏边界。4.2 常见错误清单刷题党踩得最多的四个坑写这道题最容易犯的错误我观察身边同事和网友的普遍反馈主要集中在四个地方。第一个坑是转置内层循环从 0 开始导致同一对元素交换两次矩阵没有变化。这属于对“只处理上三角”意识不足。解决的办法就是写代码时养成习惯凡是做对称交换内层下标从外层 i 开始。第二个坑是水平翻转的列循环写满整行。很多人在这一步大脑突然短路觉得“既然要翻转每列肯定都要走一遍”结果后一半的交换又把前一半的效果抵消了。记住每一行只要交换前 n // 2 列即可。第三个坑是试图用 Python 的 matrix[:] list(zip(*matrix)) 或其他一行流解法。这个操作虽然代码优雅但 list(zip(*matrix)) 会在内部构建一个全新的转置矩阵完全违背题目原地旋转的约束。刷题可以玩技巧面试千万别拿这个糊弄面试官一眼就能看出你多开了 O(n²) 的额外空间。第四个坑是忽略矩阵是“引用类型”。如果你写了一个局部变量直接指向 matrix在函数内修改时没问题但如果有人先用 matrix matrix[::-1] 之类的操作制造了一个新列表原矩阵其实根本没变。所有修改必须作用在传入的 matrix 对象本身而不是重新绑定变量。4.3 时间复杂度与空间复杂度为什么没有更快的解法分析时间复杂度很直接矩阵一共有 n² 个元素无论解法一还是解法二每个元素要么被交换一对要么参与一次四元素轮换访问次数都是常数级别因此总时间复杂度是 O(n²)。空间复杂度方面主要变量只有 n、left、right、top、bottom 还有临时变量 tmp不随 n 成长因此空间复杂度是 O(1)。这已经是理论最优因为要完成旋转至少要把每个元素处理一遍不可能低于 O(n²)。面试时如果面试官问“能不能更快”你可以直接回答不能并解释原因任何解法的下界都由输入规模决定n² 个元素至少要遍历一次。这种“主动给出下界证明”的习惯比背复杂度结论更显功力。5. 扩展逆时针、180度与矩阵系列题目串讲5.1 逆时针旋转90度把操作顺序调个头知道了“转置水平翻转”等于顺时针 90 度之后逆时针 90 度怎么实现就非常自然了。前面推导过如果先做水平翻转再做转置映射会变成 (n - 1 - j, i)正好是逆时针 90 度的公式。你也可以用另一种等价做法先转置再垂直翻转上下翻转。如果要写代码最省事的方法是沿用解法一的框架def rotate_counterclockwise(matrix): n len(matrix) # 先水平翻转 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] # 再转置 for i in range(n): for j in range(i, n): matrix[i][j], matrix[j][i] matrix[j][i], matrix[i][j]还有一种思路我其实更推荐在面试时说逆时针 90 度等于顺时针旋转三次。虽然实际执行时间变长了但你在不引入任何新公式的情况下解决了问题这种“降维打击”式的方法能体现出思维的灵活性。不过真到了写代码直接写三步循环会有点冗余面试时提一嘴即可代码还是用“水平翻转转置”更干净。5.2 旋转180度上下翻转加左右翻转顺序无所谓旋转 180 度的映射公式为 (i, j) - (n - 1 - i, n - 1 - j)也就是行号、列号分别反转。实现上就是水平翻转加垂直翻转而且这两个操作的顺序互不影响因为一个只动行方向一个只动列方向。把它和 90 度旋转对照着记忆特别有效90 度需要“一翻一换”两步180 度则是“两换”一步90 度旋转四次回到原位180 度旋转两次回到原位。面试时遇到旋转类的变种题这些公式可以直接套省下在草稿纸上推来推去的时间。5.3 同源题目串讲矩阵类题型的复习路线这一题做完别急着跳到下一个类型趁着矩阵操作的手感还在把同源题目刷一遍收获最大。我列一个比较顺的路线转置矩阵允许使用额外空间是旋转题的“半成品”用来练转置操作特别合适。螺旋矩阵按圈遍历和逐圈旋转的“圈”是同一种抽象需要处理边界收缩。螺旋矩阵 II反向操作按圈填数练的是同一套“圈层思维”。判断矩阵经轮转后是否一致直接把矩阵旋转 4 次逐一比较相当于给本题做了一个应用扩展。矩阵置零同样是原地操作矩阵但需要用第一行第一列做标记属于另一类经典原地技巧。这几道题串联起来基本覆盖了矩阵操作题的常见套路。做完之后你再回头看旋转图像会觉得它就是整块知识点的起点。6. 面试现场怎么答才能拿满这一题的分6.1 一张合理的答题节奏表如果把这道题放进 20 分钟的面试场景里时间分配很有讲究。我建议的节奏是前 1 分钟读题并和面试官确认输入输出接下来 1 分钟叙述思路优先说“转置水平翻转”中间 10 分钟写代码并逐步解释最后 2 分钟用 3×3 样例验证剩下的时间留给面试官提问。读题阶段一定要开口确认“矩阵是 n×n 方阵”“要求原地不动”“旋转方向是顺时针”这三个确认动作既避免理解偏差也能向面试官展示你的工程素养。思路叙述阶段把“坐标公式 两步操作”讲清楚然后询问面试官是否认可这个方法再开始写代码。很多人喜欢闷头就写其实先交流一句“我打算先转置再左右翻转您看可以吗”面试体验会好很多。6.2 主动说出“为什么必须是方阵”这个加分点前面说的非方阵问题是这道题最被低估的加分点我强烈建议你在思路介绍或代码完成之后主动抛出。你可以这样说本题限定了方阵是因为 m×n 的非方阵旋转后尺寸会变成 n×m无法在原来的空间里完成必须额外分配矩阵。这个说法直接展示了你对问题结构约束的理解很多刷题背答案的人根本想不到这一层。另一个加分层面的点是循环置换。如果你能进一步解释“旋转是一种置换它由若干个独立的 4 元素环组成解法二本质上就是在逐个处理这些环”面试官会意识到你不仅会写代码还对群论或置换分解有了解。不需要深入数学概念点到为止即可。6.3 写完代码后的自测清单最后给你一个我每次面试练习都会过的自测清单总共五条转置的内层循环是否从 i 开始如果写成了 0结果一定错。水平翻转是否只循环到 n // 2如果写成了 n等于白翻。对于输入 [[1,2],[3,4]]手算结果是否等于 [[3,1],[4,2]]对于 3×3 的输入是否能一次性通过题目示例如果面试官让你修改几个字符实现逆时针旋转你是否能立刻说出“换一下水平翻转与转置的顺序”如果这五条你都能不假思索回答出来这道题就算真正吃透了。刷题最怕的就是“看了解法觉得自己会了合上书脑袋空空”把自测清单当成你的检验标准可以避免这种错觉。我在实际刷题时还有一个习惯凡是坐标类或数组类题目拿到手先在草稿纸上画一个 3×3 的小方格把下标标出来再动手。旋转图像这题尤其如此四个下标的轮换关系看一遍代码远不如亲手在纸上写一遍印象深刻。如果你也被这道题卡过花一个晚上把上面两种解法各练三遍再顺手做掉扩展里的几道同源题以后再遇到任何矩阵旋转的变体都能从容应对。