最近在刷 LeetCode Hot100刷到第 16 题正好是 48. 旋转图像。说实话这题乍一看是个“中等难度”但很多第一次做的人包括我都会在方向上绕几分钟到底顺时针是往左还是往右坐标要怎么换更别提“原地旋转”这个限制一上来就断了“开个新数组”的念头。这题其实非常适合用来练二维数组的基础功也常被面试官当作热场题——它不考高深的算法考的是你对索引变化、边界条件、以及原地修改的理解。这篇文章就把我从这道题里挖出来的东西完整写一遍从坐标公式推导、两种主流解法的代码和复杂度到我自己踩过的坑、以及怎么快速验证旋转结果都放进来。不管你是刚开始刷题的小白还是想偷懒直接看结论的选手照着文章走一遍这道题应该就彻底拿下了。1. 题目理解与思路拆解1.1 先搞清楚到底往哪边转题目要求很直白给定一个 n×n 的二维矩阵 matrix把它顺时针旋转 90 度并且必须原地修改。比如输入 matrix [[1,2,3], [4,5,6], [7,8,9]] 输出 matrix [[7,4,1], [8,5,2], [9,6,3]]从行/列的角度去看规律是原来的第一行变成了最后一列原来的第二行变成了中间一列原来的第三行变成了第一列。更严谨地说对于矩阵中的任意一个位置(i, j)顺时针旋转 90 度之后它会移动到(j, n-1-i)。这个公式是整个题目的灵魂。为什么是这个公式把它拆开理解所谓顺时针旋转可以看成先以主对角线为轴做转置再水平翻转左右翻转。转置之后(i, j)变成(j, i)水平翻转之后列坐标变成n-1-i于是合起来就是(j, n-1-i)。也可以用另一种视角把矩阵当成一个正方形的“纸片”旋转后左上角的元素跑到了右上角右上角的元素跑到了右下角右下角的跑到了左下角左下角的跑到了左上角四个位置不断轮换。只要先把这个坐标映射关系写明白后面不管用哪种方法实现心里都有底。有个很容易绕晕的点很多人会把顺时针和逆时针搞混。逆时针 90 度对应的公式是(i, j) - (n-1-j, i)。你看区别只在列和行的变化方向。所以在写代码之前建议先在草稿纸上画一个 3×3 的例子把一个坐标代进去算一遍确定自己没转反。提示刷这种矩阵题最忌讳一上来就硬想代码。先用坐标点验证方向比如(0,0)旋转后应该在(0,n-1)还是(n-1,0)想清楚这一句话后面就稳了。1.2 两种主流思路转置翻转 vs 分组循环理解了坐标公式之后实现方式基本就分两派。第一派是“转置 水平翻转”。前面说过顺时针旋转等价于先转置再水平翻转。具体做法是先沿着主对角线把矩阵转置也就是把matrix[i][j]和matrix[j][i]互换然后对每一行做一次反转。这个方法思路非常清晰代码量很小最重要的是不容易写错。缺点是它需要遍历两轮不过时间复杂度依然是 O(n^2)空间 O(1)完全够用。第二派是“分组循环”其实就是直接在矩阵上模拟四个位置的轮换。你从左上角取一个元素暂存起来然后让左下角的元素移到左上角右下角的移到左下角右上角的移到右下角最后把暂存的左上角元素放到右上角。一个位置一组总共需要处理矩阵四分之一区域的元素。这个方法更贴近旋转的本质但下标计算相对复杂对边界特别敏感。有的人觉得它更“酷”也有面试官喜欢追问这种写法因为它能看出你确实理解了旋转的过程。从实用角度我更推荐面试时先写第一种因为短时间内不容易翻车。第二种可以作为进阶理解或者当面试官问“能不能再优化”的时候讲一讲它的原理。两种解法最终结果完全相同殊途同归。2. 核心实现两种解法详解2.1 解法一转置 水平翻转推荐简单先用 Python 写一版def rotate(matrix: List[List[int]]) - None: n len(matrix) # 1. 按主对角线转置 for i in range(n): for j in range(i 1, n): matrix[i][j], matrix[j][i] matrix[j][i], matrix[i][j] # 2. 每一行反转 for i in range(n): matrix[i].reverse()代码只有两段循环非常好记。转置的时候有个关键细节内层循环j必须从i1开始而不是0。为什么因为对角线上的元素matrix[i][i]不需要和自己交换而如果从 0 开始则会把已经交换过的元素再交换一遍最后矩阵等于没变。举个极端例子如果转置循环里j从 0 到 n-1那么(0,1)和(1,0)会交换两次白费功夫还可能因为 Python 的同步赋值技巧而出现隐藏问题。所以我们总是只遍历右上三角区域保证每对元素只交换一次。水平翻转部分就简单了matrix[i].reverse()或者手动写左右交换都行。如果面试官不让你用库函数就手写for i in range(n): left, right 0, n - 1 while left right: matrix[i][left], matrix[i][right] matrix[i][right], matrix[i][left] left 1 right - 1C 版本也顺手贴一下class Solution { public: void rotate(vectorvectorint matrix) { int n matrix.size(); for (int i 0; i n; i) { for (int j i 1; j n; j) { swap(matrix[i][j], matrix[j][i]); } } for (auto row : matrix) { reverse(row.begin(), row.end()); } } };这版代码的时间复杂度是 O(n^2)因为每个元素最多被访问常数次空间复杂度 O(1)完全没有申请额外矩阵。注意题目要求原地修改所以函数返回值是NonePython 里直接改传入的列表即可。特别提醒不要写matrix new_matrix那样只是把局部变量重定向外部原矩阵根本不会变。这也是“原地”题的一个常见坑。2.2 解法二分组循环直接旋转进阶如果不想先转置再翻转也可以一步到位。先回忆旋转链(i,j)-(j, n-1-i)-(n-1-i, n-1-j)-(n-1-j, i)-(i,j)。四个位置一组轮流覆盖。为了保证不丢失数据只需要用一个临时变量tmp暂存起始位置。关键难点在于循环边界怎么写。最标准的写法是def rotate(matrix: List[List[int]]) - None: n len(matrix) for i in range(n // 2): for j in range(i, n - i - 1): 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你看外层循环i的范围是0到n//2 - 1它控制“层数”。想象一个洋葱矩阵由外到内一层一层剥开最外层是最大的正方形往里每层小一圈。对于 n×n 矩阵一共有n//2层中心如果是奇数矩阵单独的位置不需要旋转。内层循环j的范围是i到n-i-2这是每层内要处理多少个“四元组”。比如 3×3 的最外层i0j 从 0 到 1一共两组一组处理四角一组处理四条边上的中间元素。如果 j 跑到n-i-1那就会把旋转链的起点重复覆盖一次导致结果错乱。这个边界为什么是n-i-1因为每一层正方形的边长是n - 2*i除了最后一个点不需要处理它由第一个点旋转得到所以内层循环要跑边长-1次也就是n - i - 1 - i n - 2i - 1对应 Python 的range(i, n-i-1)正好取i ... n-i-2。C 版本class Solution { public: void rotate(vectorvectorint matrix) { int n matrix.size(); for (int i 0; i n / 2; i) { for (int j i; j n - i - 1; j) { int 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; } } } };写这种解法如果不放心可以在大脑里跑一组 4×4 或者 5×5 的例子从i0,j0开始按链条把每个位置代进去确认没有越界。最容易出错的地方是第二项matrix[n-1-j][i]有人会下意识写成matrix[n-1-i][j]那就变成另一条变换链了。记法从“左边”挪到“顶部”的坐标是(n-1-j, i)。这个位置本质上就是当前列的下方镜像。两种解法都是 O(n^2) 时间、O(1) 空间。解法一更直白解法二更“原生”。我自己在面试时通常会先说解法一然后主动补一句“如果想一步轮换也可以分四组”面试官如果感兴趣我再说第二个。这样既展示理解深度又不给自己制造写错的风险。3. 实操细节与避坑指南3.1 下标计算的忙点在哪里这道题的下标坑多得离谱我总结了几个高频的翻车点。第一转置循环里j起点的坑。前面已经说过j必须从i1开始。但从实际刷题来看很多人不是不知道而是写的时候手一滑写成range(n)。这样做的后果是每对元素被交换两次矩阵原地复原然后你会得到“我明明转了结果没变”的诡异体验。检查方法很简单打印转置后的矩阵如果它和原矩阵一模一样大概率就是交换了两次。第二水平翻转和垂直翻转的混淆。有人为了得到顺时针旋转先做“垂直翻转”上下翻再做转置其实得到的是逆时针旋转。可以把坐标变化推一遍垂直翻转(i,j)-(n-1-i,j)再转置(n-1-i,j)-(j,n-1-i)这不是顺时针还是逆时针算一下发现和顺时针公式不同。如果你真想用“垂直翻转转置”得到顺时针顺序要反过来先转置再垂直翻转。所以请记住组合的“先转置再左右翻”。第三分组循环边界记错。最常见的错是把内层循环写成for j in range(i, n - i)。这样会多处理一个元素旋转链走到最后会把第一个位置再次覆盖导致某个区域重复赋值矩阵就被搅乱了。如果你不确定就先拿 3×3 手推最外层 j 应该只有 0 和 1不应该有 2。多推两次边界就刻在脑子里了。3.2 原地与非原地实现对比如果不要求原地这道题就简单许多直接开一个 n×n 的新数组按照旋转公式把每个元素填进去。def rotate_not_inplace(matrix): n len(matrix) res [[0] * n for _ in range(n)] for i in range(n): for j in range(n): res[j][n - 1 - i] matrix[i][j] return res这个版本时空复杂度分别是 O(n^2) 和 O(n^2)正确性一目了然。那为什么 LeetCode 非要原地因为现实场景中大矩阵可能占据很多内存能省一分是一分。面试官问“原地”其实是在考察你是否意识到数组是引用传参以及是否了解内存受限场景下的处理思路。从非原地到原地的转变可以这么想旋转公式本身不变但坐标(i,j)会被后面的值覆盖所以就需要“临时变量”和“分组轮换”。转置加翻转解法其实就是一种更聪明的原地方案先用两次简单的对称交换完成映射每一步都不会丢数据自然不需要额外的大数组。从这个角度讲解法一不仅好写设计层面也很优雅。注意在 Python 中如果你写了matrix res函数结束后外界看到的matrix引用还是原来的对象根本没被修改。要改成matrix[:] res才有效。这是原地修改题的一个经典陷阱参与过实习面试的朋友应该都见过。4. 常见问题与调试技巧实录4.1 现场踩坑记录我自己刷这道题的时候先试了解法一结果第一次写转置时用了for i in range(n): for j in range(i, n): matrix[i][j], matrix[j][i] matrix[j][i], matrix[i][j]j从i开始对角线元素自交换其实不影响正确性就是白白多执行了 n 次 swap。当时没当回事。后来我又灵机一动想写成for j in range(n): for i in range(j1, n):结果因为交换顺序写错矩阵也没转成。这个经历给我一个教训转置的循环结构最好固定格式不要频繁换循环方向。固定为“外层 i内层 ji1”每次都这样写肌肉记忆最靠谱。解法二我第一次提交也没通过。我犯的是经典错误把外层循环写成for i in range(n // 2 1)导致当 n 是奇数的时候最中心的那个元素被放进旋转链里转了一圈。比如 3×3 矩阵i最大为 1正好把matrix[1][1]给当作分组旋转的一部分。其实 3×3 的中间元素位置是固定的不需要参与任何轮换。好在测试用例会暴露这个问题修改n//2后顺利通过。后来我养成了一个习惯遇到矩阵题先用 3×3 和 4×4 各测一遍再上大矩阵随机测试。4.2 快速验证与辅助工具这里分享一个调试利器写一个打印矩阵的函数和一个“旋转 4 次复原”的断言函数。def show(matrix): for row in matrix: print(row) print() def rotate_and_check(matrix): original [row[:] for row in matrix] rotate(matrix) # 从当前状态再转3次应该回到原始状态 for _ in range(3): rotate(matrix) assert matrix original验证原理很简单旋转是周期性的顺时针 4 次回到原样。所以我每写一个实现就随机生成几个矩阵跑这个断言。如果断言通过基本说明没有下标越界和覆盖顺序错误。配合show(matrix)在每一步打印可以肉眼观察转置后、翻转后的中间形态快速定位“是哪一步转坏了”。除了常规测试我还会手动检查几个特殊点n1 的矩阵只有一个元素不需要做任何操作n2 的矩阵每次两组最容易看出整体形状以及 n5 的奇数矩阵中心元素必须保持不动。边界情况多测几组代码健壮性就上来了。提示LeetCode 的调试器虽然方便但本地用这些辅助函数更顺手。特别是随机生成 10 组 0~10 边长的矩阵反复测试比只依赖 OJ 的用例更让人放心。5. 从 Hot100 说开去5.1 为什么这题被选入 Hot100LeetCode Hot100 收录的题目通常有代表性这题也是。旋转图像考察了二维数组的基础操作、坐标变换、原地修改这几个点几乎在每场算法面试里都有概率出现。而且它够简单容易给面试者树立信心同时它又能演变出许多问法比如逆时针旋转、旋转 180 度、旋转 k 次甚至不旋转只找变换规律。把这题吃透相当于打通了一个小类别的“母题”。在 Hot100 里的位置也很有意思它被夹在很多链表和字符串题中间很多人容易忽略它。但实际刷题时这道题付出的时间回报比很高——你只需要花一下午搞明白两种解法之后遇到矩阵变换类的题会轻松很多。从我在牛客上的观察剑指 Offer 29 这种顺时针打印矩阵的题和它也沾边因为都涉及“一圈一圈处理”思想相通。5.2 相关变式与延伸学会了顺时针旋转 90 度再碰到变体就能快速套用逆时针旋转 90 度。公式是(i,j)-(n-1-j,i)。也可以先用水平翻转上下翻再转置。如果你只记得顺时针解法可以这样转化先做垂直翻转再做一次顺时针旋转自己推一下很快能得出结论。旋转 180 度。等价于每个元素直接对称交换交换(i,j)和(n-1-i,n-1-j)。可以通过两次顺时针 90 度实现也可以写一次双重循环完成两种都正确。旋转 k 次k 为正整数。只需要取模k % 4然后按情况调用一次、两次或三次顺时针旋转。如果 k 是负数可以先取模转换为正整数。这些变体在面试里经常换个外壳出现背住“坐标公式 分组轮换”这两个核心基本就不怕了。另外像“生命游戏”“矩阵置零”这类题也同样考察原地修改矩阵时如何用额外标记记录状态本质都是对二维数组操作的自信心。最后再说一点个人体会。这道题最值得记的不是那几行代码而是“先推公式、再动手写”的习惯。我一开始也是上来就写循环结果来回改边界后来改成先在草稿纸上写出(i,j) - (j, n-1-i)再顺手推一下顺时针 4 个位置的轮换链思路立刻清晰写代码也快了很多。如果你下次做其他矩阵题也卡住不妨试试这个方法可能比你自己瞎试效率高一倍。