力扣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 四元素轮换的下标设计:从n=3到n=5逐个验证
现在把下标问题彻底说清。用 left 和 right 表示当前圈的左右边界,初始时 left = 0,right = n - 1。每一圈里,我们会处理 right - left 组元素。注意不是 right - left 个元素,而是边长减一个“内点”,因为四个角落各属于一条边,不能被重复处理两次。
对于每一组,记上下边界分别为 top = left,bottom = 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 = 2,range(2) 处理 i = 0 和 i = 1。i = 0 处理四个角:上 1 到右上角 3 的位置,左 7 到上 1 的位置,下 9 到左 7 的位置,右 3 到下 9 的位置,完成一轮。i = 1 处理四条边上的第二个元素:上 2、右 6、下 8、左 4,一轮交换后这四个元素也归位。此时 left 变成 1,right 变成 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 n=0、n=1、奇数与偶数,边界条件一次过
真题的测试用例可不止 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 的小方格,把下标标出来再动手。旋转图像这题尤其如此,四个下标的轮换关系,看一遍代码远不如亲手在纸上写一遍印象深刻。如果你也被这道题卡过,花一个晚上把上面两种解法各练三遍,再顺手做掉扩展里的几道同源题,以后再遇到任何矩阵旋转的变体,都能从容应对。