刷LeetCode的人几乎绕不开这道题。力扣第48题“旋转图像”英文名Rotate Image给的输入是一个 n x n 的二维矩阵要求把整个矩阵顺时针旋转90度而且必须在原矩阵上直接改不能用额外的矩阵拷贝。听起来就一句话的事真上手写才发现索引一转就晕。这道题在面试里出现频率非常高很多做图像、游戏、前端布局的公司都喜欢拿它当基础题考因为它既涉及到数学规律又考验动手写代码时的边界控制。这篇文章我会从题目理解开始把三种解法完整演一遍再给多语言代码实现最后把我自己刷题时踩过的坑、验证方法和一些延伸思路整理出来。不管你是刚开始刷题的新手还是准备面试想在细节上压别人一头这篇内容都能帮你直接把这类旋转问题吃透。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] ]再看一个 4x4 的例子可能更容易看出规律原矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 旋转后 13 9 5 1 14 10 6 2 15 11 7 3 16 12 8 4观察一下第一行1 2 3 4旋转后跑到了右上角而且顺序倒过来了变成1 2 3 4分别在最后一列从上往下。这就是顺时针旋转90度的直观特点原来的第一行变成了最后一列原来最后一行的变成第一行。1.2 面试官想考察的能力这道题被列为经典不是没有原因的它考察的其实不是某个高深算法而是三个基础能力第一是矩阵下标的坐标变换能力。每个元素旋转前后的位置关系要用公式表达清楚不能靠“感觉”。第二是原地算法的意识题目明确说不能用另一个矩阵这意味着你必须考虑元素覆盖的顺序稍有不慎就会把还没处理的元素盖掉。第三是边界条件的处理n 为奇数、n 为偶数、n 等于1这些情况表现完全不一样。我在实际面试模拟里见过不少候选人对这道题的反馈思路能说清楚画图也能画对一上代码就开始乱。原因很简单画图的时候是“人眼在看”写代码的时候是“索引在跳”这两者之间差着一个熟练的映射关系。1.3 边界条件与隐藏陷阱先记住一个结论旋转90度矩阵本质上是把矩阵的每一圈单独处理从外到内一层一层处理。最外层元素的坐标变化范围是 0 到 n-1第二层是 1 到 n-2依此类推。还有几个容易忽略的点n 0 或者 n 1 时矩阵不需要任何操作代码里要能直接退出。当 n 为奇数时正中间那个元素旋转后还在原地不需要移动也不能让它参与元素交换。题目要求顺时针如果改成逆时针核心逻辑会有区别很多人在面试追问时翻车。理解这些边界之后再看解法就会清楚很多。2. 从暴力到优雅三种解法的完整演进2.1 解法一拷贝矩阵空间换时间最直白的思路我们只需要搞清楚旋转后每个坐标对应原矩阵哪个坐标然后填充到新矩阵里。假设新矩阵第 i 行第 j 列的元素应该来自原矩阵的第 n-1-j 行、第 i 列。这句话刚读起来比较绕我建议你拿 3x3 矩阵手工验证一下。比如新矩阵的(0,0)位置是 7它来自原矩阵(2,0)也就是第 n-1 行第 0 列。用公式new[i][j] old[n-1-j][i]代入 i0, j0得到old[2][0]正确。代码实现基本上就是双重循环加一个结果数组。这种解法时间复杂度和空间复杂度都是 O(n^2)能用但不满足题目“原地”的要求。不过我不建议完全跳过这个解法因为理解坐标公式是后面所有解法的地基。先把公式推明白再考虑省内存。很多人一上来就想原地旋转画了半天圈把自己绕晕就是因为坐标公式都没吃透。2.2 解法二转置加镜像一行核心操作这是我认为最适合面试现场写的解法思路极其简单分成两步第一步把矩阵转置也就是沿着主对角线对称翻转让matrix[i][j]变成matrix[j][i]。第二步把每一行做水平翻转也就是让第 j 列和第 n-1-j 列交换。为什么这样能得到顺时针旋转90度你可以这么理解转置是把行和列互换它本身是一种关于主对角线的对称操作接着再水平翻转相当于把整个矩阵左右镜像两个操作叠加之后恰好等于顺时针旋转了90度。用公式推导一下就更清楚了。转置之后原(i,j)位置的元素挪到了(j,i)位置。再水平翻转(j,i)会被挪到(j,n-1-i)。所以最终位置是(j, n-1-i)而标准顺时针旋转 90 度的坐标映射是元素(i,j)移动到(j, n-1-i)。完全一致。同理如果你想实现逆时针旋转 90 度可以转置之后再做垂直翻转也就是上下翻转也能得到正确结果。这是我很推荐的一种思路数学上很干净代码也不容易写错。2.3 解法三四元素循环交换点对点旋转另一种原地做法是直接让四个元素一组进行旋转交换。比如 3x3 矩阵中角上的四个元素就构成了一个交换循环左上角到右上角右上角到右下角右下角到左下角左下角到左上角。具体来说在矩阵内部按层处理每一层里让元素以四个一组做轮转。以第一层、第 j 列的位置为例四个交换对象分别是matrix[i][j] - matrix[j][n-1-i] matrix[j][n-1-i] - matrix[n-1-i][n-1-j] matrix[n-1-i][n-1-j] - matrix[n-1-j][i] matrix[n-1-j][i] - matrix[i][j]这四个坐标构成一个闭环所以用一个临时变量就可以完成交换。循环的时候i 从 0 到 n/2-1表示层数j 从 i 到 n-2-i表示每一层内的元素位置注意别走到最后一个因为最后一个元素会被第一个元素覆盖完成闭环。这个解法的好处是完完全全的原地空间复杂度 O(1)一看就知道没有用额外矩阵。坏处是代码坐标比较复杂写起来容易把下标写错。面试时如果你能用这个解法说明你对坐标映射的掌握很扎实但如果你不太熟练我更建议先用解法二的思路打开局面。3. 代码实现与细节多语言版本对照3.1 Python 实现Python 写转置加翻转非常简洁几乎可以把它当成“官方解法”来写。转置的时候注意只遍历右上三角也就是for i in range(n)内层for j in range(i1, n)这样交换一次就够了如果遍历整个矩阵会交换两次等于没换。def rotate(matrix): n len(matrix) if n 1: return # 转置 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): left 0 right n - 1 while left right: matrix[i][left], matrix[i][right] matrix[i][right], matrix[i][left] left 1 right - 1Python 里交换两个元素直接用a, b b, a底层实现会避免临时变量问题非常方便。但注意不管用什么语言逻辑核心是一样的先转置再反转。四元素循环交换版本长这样def rotate(matrix): n len(matrix) if n 1: return for i in range(n // 2): for j in range(i, n - 1 - i): temp 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] temp这段代码第一次读容易头晕我给个验证方法找一个 4x4 矩阵取 i0, j0手动走一遍四个坐标的赋值顺序比盯着代码看有效得多。3.2 Java 和 C 版本对照Java 版本我会写成转置加翻转因为这种写法最不容易越界。这里有一个很容易被忽略的细节转置的双重循环里内层循环的起点必须是i 1否则元素会被交换两次矩阵跟没动一样。Java 没有 Python 那种直接解构赋值的语法所以交换需要三步public void rotate(int[][] matrix) { int n matrix.length; if (n 1) return; 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) { int left 0, right n - 1; while (left right) { int tmp matrix[i][left]; matrix[i][left] matrix[i][right]; matrix[i][right] tmp; left; right--; } } }C 版本几乎一样只是把引用和二维数组换过来。实际上到了这一步你会发现算法题的关键并不在语言而在你对坐标变换的理解程度。语言只是把思路翻译出来的工具。3.3 为什么有的代码会越界我见过很多人在写四元素交换时越界最常见的原因是内层循环的 j 范围写成了j n。如果 j 走到最后一列那matrix[n-1-i][n-1-j]里的下标n-1-j就变成负数或者超界了。所以 j 的上界必须是n-1-i。第二常见的原因是搞错层数循环for i in range(n)会导致内层交换重复执行而且坐标对称位置上元素会被反复覆盖。正确写法是for i in range(n // 2)因为中间层要么只有一条线奇数时要么已经处理完了。第三常见原因是忘了处理 n 1。n 1 的时候n//2 0但转置循环里 j 从 i1 开始如果写成j 0就会把自己和自己交换结果虽不影响但会浪费执行时间极端情况下可能干扰调试。4. 高频报错与调试实录4.1 常见错误清单我把这些年看到的高频错误整理成一个速查表你写代码的时候可以对照检查错误类型具体表现原因解决办法元素被覆盖结果矩阵某些位置重复出现同一个数没有用临时变量保存被覆盖的元素四元素交换时先用 temp 存第一个元素多次交换等于没换转置后又转置回来内层遍历了整个矩阵交换了两次内层从 i1 开始只处理右上三角越界异常数组下标负数或超过 n-1内层循环范围过大j 的范围限制在 [i, n-2-i]旋转方向反了得到的是逆时针结果水平翻转和垂直翻转用混了顺时针转置水平翻转逆时针转置垂直翻转奇数中心点动了中间位置的值被换走参与循环交换中心点在 n 为奇数时不需要操作4.2 验证结果的小技巧调试这类矩阵题我建议你写一个辅助函数把矩阵按行打印出来。不要用调试器单步跟踪矩阵元素因为二维数组在调试器里的展示非常混乱肉眼扫一眼打印结果反而更快。另外一个很实用的验证方式构造一个递增矩阵[ [0,1,2,...], [n, n1,...] ]旋转之后检查每一行是否满足规律。比如 3x3 递增矩阵旋转后第一行应该是[6, 3, 0]第二行是[7, 4, 1]第三行是[8, 5, 2]。这个过程可以拿笔推一下也可以用一个自定义的打印函数快速浏览。我自己的习惯是在脑海里把矩阵想象成一个正方形纸片顺时针旋转90度原来的顶边变成右边右边变成底边。这样当你怀疑某段代码方向写反时画一个三行三列的方块用手比划一下旋转方向比死磕公式要直观很多。4.3 时间复杂度和空间复杂度分析不管用哪种解法只要矩阵是 n x n元素总数是 n^2每个元素都要被访问一次所以时间复杂度都是 O(n^2)。区别主要在空间复杂度解法一需要新矩阵额外空间 O(n^2)解法二转置翻转额外空间 O(1)解法三四元素循环交换额外空间 O(1)面试时如果题目要求原地空间复杂度是重点考察项你得分得清两种原地解法的区别。解法二虽然也是原地但它本质上做了两轮遍历解法三一轮遍历就完成了所有交换。从常数因子上看解法三更快但从代码可维护性看解法二更稳。如果你是在面试高压状态下我宁可写解法二也不愿意为了轻微的性能提升把解法三写崩。5. 从LeetCode到真实场景旋转思想能干什么5.1 变体题目与延伸解法LeetCode 48 有很多变体面试官特别喜欢现场改条件。最常见的变体是逆时针旋转 90 度解法很简单先转置再垂直翻转。公式可以自己推一遍不要死记。还有一种变体是把矩阵旋转 180 度这个只做水平翻转再加垂直翻转即可。更进阶的变体是要求旋转一个非正方形矩阵比如 m x n 矩阵整个旋转90度这种情况下的输出矩阵尺寸会变成 n x m而且无法原地完成因为尺寸都变了必须用额外空间。这类变体通常出现在线上笔试的后半段难度会陡然上升。从数据结构的本质来看这些题目都在考“坐标映射”这一件事。你只要掌握了一个元素从旧坐标到新坐标的映射公式无论题目怎么改都能拆成可执行的循环。5.2 图像处理与UI绘制中的旋转真实世界里矩阵旋转最常见的场景就是图像处理。一张数字图像在内存里就是一个二维像素矩阵逆时针或者顺时针旋转90度就是今天这道题的直接应用。很多拍照App的横竖屏切换、图片预览的旋转方向底层都离不开这套坐标变换。前端开发中也有类似场景某个画布组件要做旋转动画或者游戏里的地图块要翻转方向本质上都用到了矩阵变换。只不过真实项目里还会涉及到缩放、平移、裁剪等复合操作这些又会用到更复杂的数学工具比如仿射变换。但基础坐标映射仍然是核心。我曾在某个模拟项目里给一块大屏做地图旋转功能当时就是拿这道题的思路先把90度旋转的坐标换算写好再去处理动画平滑过渡。所以别觉得刷题没用这些规律在需要做像素级操作的领域真的会派上用场。5.3 我的刷题心得LeetCode 48 这类题属于“会者不难难者不会”的典型。你一旦把坐标映射彻底搞清楚以后再遇到旋转、翻转、对称相关题目会非常有底气。我个人的建议是刷这道题时必须亲手画图不要只看题解。你可以在纸上画一个 4x4 的格子把每个元素旋转前后对应的位置标出来。这一步至少花十分钟但非常值。我见过不少同学省略画图环节直接背代码结果题目稍微变形就不会做。另一个建议是写完代码之后主动想一想如何推广到逆时针和180度。这个过程不是浪费时间它能帮你把“转置翻转”的思路内化成自己的思维工具而不是临时抱佛脚背出来的结论。最后说点实在的面试时如果被问到这道题千万不要上来就写最复杂的那版代码。先给面试官说清楚你的两个步骤比如“我先转置再把每一行反转这样就能得到顺时针旋转90度的结果”然后再动手写。把思路前置讲清楚比闷头写对代码更容易拿高分。我见过太多人一上来就闷头写代码是对的但因为没解释推导过程面试官会默认你只是背过答案反手就会追问一个变体。所以把坐标公式吃透、把两种原地解法都准备在手边才是这道题最稳妥的打开方式。