刷题刷到第48题,很多人的第一反应是“哦,就是那个旋转图像的题”。确实,LeetCode 48 旋转图像几乎出现在每一份算法面试清单里,但真正在面试白板上写的人,十个里有三四个会在坐标映射上卡住。这道题不涉及动态规划,不涉及复杂数据结构,矩阵操作和下标推导就是全部,属于那种“看起来简单、写起来绕、考完还想骂”的典型题目。这篇文章把我自己从背答案到真正上手的路径复刻出来,把暴力复制、原地分圈、转置翻转三条路线讲清楚,也会说一下测试时踩过的坑,希望能让你一次吃透。
1. 题目到底在考什么:一道“旋转”题背后的核心考点
1.1 题目原文与示例回顾
题面非常短:给定一个 n × n 的二维矩阵 matrix 表示一个图像,把这个图像顺时针旋转 90 度,并且要求原地旋转,也就是直接修改输入的二维矩阵,不要使用另一个矩阵来承接。
示例:
输入: 1 2 3 4 5 6 7 8 9 输出: 7 4 1 8 5 2 9 6 3再看一个 4 × 4 的输入:
5 1 9 11 2 4 8 10 13 3 6 7 15 14 12 16旋转后变成:
15 13 2 5 14 3 4 1 12 6 8 9 16 7 10 11观察坐标变化前,我建议你先自己动手把这两个例子走一遍。你会发现规律其实很朴素:旋转前的第 0 行变成了旋转后的最后一列,旋转前的第 1 行变成了倒数第二列,但列内元素的顺序是反的。这只是表象,真正稳定的说法是坐标映射:原来在 (row, col) 位置的元素,顺时针旋转 90 度后会跑到 (col, n - 1 - row) 位置。这个公式是全题的命根子,无论你后面用哪种写法,最后都要回到这个映射上。
1.2 出题人真正想考察的四个点
第一,抽象建模能力。一个图片旋转问题,能不能快速把它翻译成二维数组下标变换。这不是记忆力问题,而是你有没有处理过矩阵坐标变换的思维框架。
第二,原地修改的意识。题目明确要求不要用额外矩阵,这就是在逼你在空间复杂度上做文章。很多人在第一步就崩了:直接新建一个二维数组往回填,这叫“结果对了,但你根本没按题目要求做”。面试官会追问,你要当场拿出原地方案。
第三,边界处理能力。n 是奇数还是偶数,处理逻辑会略有不同;n=1 的时候程序不能崩溃;每次循环的起止边界写错,矩阵就永远旋转不成功,而且看起来很诡异。
第四,工程直觉。代码能不能让别人一眼看懂,为什么内层循环跑 len(matrix[0]) 而不是 len(matrix),这些都是实战中真正会遇到的细节。
所以别看这道题只有“原地旋转”四个字,它实际上已经把矩阵题的几个核心难点都覆盖了。理解了这四点,你后面看任何矩阵旋转、矩阵翻转、螺旋遍历的题都会顺畅很多。
2. 暴力解法不是玩具:先会复制,再谈优化
2.1 最简单也最容易出错的实现
很多人一上来就追求原地,结果越写越乱。我的建议是先写一版“开新数组”的解法,哪怕最后不提交,也一定要在草稿纸上推一遍。它最大的价值是让你迈过“坐标映射”这道坎。
新建一个同样大小的 result 矩阵,遍历原矩阵的每个位置 (i, j),把元素填到目标位置。依据前面说的映射关系:
def rotate_bruteforce(matrix): n = len(matrix) result = [[0] * n for _ in range(n)] for i in range(n): for j in range(n): result[j][n - 1 - i] = matrix[i][j] return result跑一下示例:
[1, 2, 3] [7, 4, 1] [4, 5, 6] -> [8, 5, 2] [7, 8, 9] [9, 6, 3]这个版本的关键就这一行:result[j][n - 1 - i] = matrix[i][j]。只要你能读明白它,后面所有优化都是在同一个公式上打转。
2.2 暴力解法的时间和空间代价
时间复杂度显然是 O(n^2),因为必须访问每一个元素;空间复杂度也是 O(n^2),因为开了一个同样大小的矩阵。这个版本的问题不是逻辑,而是空间。
这里有两个容易被忽略的坑。第一个是 Python 的二维数组初始化写法。[[0] * n] * n看起来没错,但实际每一行是同一个列表对象的引用,修改一行会串到所有行。正确写法是列表推导式[[0] * n for _ in range(n)]。第二个坑是很多人在自己的代码里直接用matrix接结果,比如matrix = result,这确实让外部变量指向了新矩阵,可原来传入的矩阵内容并没有被改动,严格来说不满足“原地修改”的要求。
暴力版本虽然在空间上不合格,但它帮我们锁定了一个重要结论:坐标映射公式本身是对的,只是需要找一个不需要新数组的方式来执行。有这条线索,再去看原地旋转就不会觉得抽象了。
3. 原地旋转:分圈处理才是正解
3.1 旋转过程的手工推导
原地旋转的经典思路是把矩阵看成一圈一圈的洋葱,从外到内一层一层转。最外层是边界元素,往里一层规模减 2,直到最中心。
外层旋转的本质是什么?每一圈上,四个边的对应元素做轮换。拿 4 × 4 矩阵第一圈的左上角那组举例:
5 1 9 11 2 4 8 10 13 3 6 7 15 14 12 16取第一圈的四个角:左上 5、右上 11、右下 16、左下 15。顺时针旋转 90 度之后,5 应该去右上角,11 应该去右下角,16 应该去左下角,15 应该去左上角。也就是说这 4 个位置的元素要同时顺时针轮转。
不只是角,每条边中间的元素也是同样的轮转模式。所以问题可以拆成:对当前这一圈,从顶行的第一个元素开始,依次取出四个对应位置,做一次轮换;然后再往后挪一个位置,再轮换;直到这一圈的起点和终点重叠。外层处理完,缩进一层继续。
3.2 四指针循环的标准写法
按这个思路,最经典的标准写法如下:
def rotate(matrix): n = len(matrix) # 从外层到内层,只需要转 n // 2 圈 for i in range(n // 2): # 当前这一圈的有效长度 for j in range(i, n - i - 1): # 四个对应位置 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 # 上 -> 右这一段初看会有点晕,但只需要盯住一个概念:四步逆时针取值。这里我用的是把“上”“左”“下”“右”四个位置逆时针倒着取出来,最后再赋给目标的技巧。也就是先存顶行元素,然后把左边元素搬到顶行,下面元素搬到左边,右边元素搬到下面,最后把存的顶行元素放到右边。这样一次就能让一组元素完成顺时针腾挪。
最外层的i表示当前圈层的起始行和起始列,范围是range(n // 2)。为什么不是n?因为转圈是一层一层往内缩的,最里面那层如果只剩一个元素,不需要动;如果是两层,n//2=1,只处理外层,内层自然在下一轮 j 的范围里消失。里面的j从i起步,到n - i - 1结束,注意是左闭右开,这保证了当前圈中每条边只处理“去掉角落那组”之后剩余的部分,但又恰好能把每条边的所有元素都覆盖到。
我用 3 × 3 矩阵走一遍:
i = 0, j = 0 temp = matrix[0][0] = 1 matrix[0][0] = matrix[2][0] = 7 matrix[2][0] = matrix[2][2] = 9 matrix[2][2] = matrix[0][2] = 3 matrix[0][2] = temp = 1 结果第一组角完成: 7 2 1 4 5 6 9 8 3接着:
i = 0, j = 1 temp = matrix[0][1] = 2 matrix[0][1] = matrix[1][0] = 4 matrix[1][0] = matrix[2][1] = 8 matrix[2][1] = matrix[1][2] = 6 matrix[1][2] = temp = 2最终得到:
7 4 1 8 5 2 9 6 3正好是题目给的预期输出。这个推导过程很重要,建议自己拿笔走一遍 3×3 和 4×4,走顺了再写代码。
3.3 下标公式的记忆套路
每次写这题都要现推四个下标,确实容易慌。我自己的记忆方法是这样:先记住左上角位置是(i, j),右上角是(j, n-1-i),右下角是(n-1-i, n-1-j),左下角是(n-1-j, i)。
这个规律其实就是四个顶点绕着中心逆时针走。你只要在纸上画一个方框,写上 i、j,然后逆时针把四个角标出来,再根据坐标轴方向写出对应坐标,一分钟就能推出来。比起死记硬背,画画更快,也更稳。
原地分圈写法的时间复杂度 O(n^2),空间复杂度 O(1),这是这道题最主要的合格解。
4. 另一种妙解:先转置再水平翻转
4.1 为什么先转置再翻转能旋转90度
除了分圈轮换,还有一种技巧性更强但也很好用的思路:把旋转操作拆成两个主对角线镜像操作。
第一步,沿着主对角线做转置,也就是matrix[i][j]和matrix[j][i]交换。转置之后,原本的第 0 行变成了第 0 列,第 1 行变成了第 1 列,所有元素都沿主对角线对调。
第二步,对每一行做水平翻转,也就是把matrix[i][j]和matrix[i][n - 1 - j]交换。
为什么这两步合起来就能达到顺时针旋转 90 度?可以先这样直观理解:转置让行变列,相当于把图像沿左上到右下的对角线翻折;水平翻转再让所有列的顺序反向。两个镜像变换叠加起来的效果,正好是一个顺时针 90 度旋转。这在几何上非常自然:两次沿不同轴的反射等于一次旋转。
用 3×3 验证:
原始: 1 2 3 4 5 6 7 8 9 转置: 1 4 7 2 5 8 3 6 9 每行水平翻转: 7 4 1 8 5 2 9 6 3和旋转变换完全一致。
4.2 代码实现及注意事项
这种思路的代码结构比圈层轮换更直观,两道简单的循环就搞定:
def rotate(matrix): 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): left, right = 0, n - 1 while left < right: matrix[i][left], matrix[i][right] = matrix[i][right], matrix[i][left] left += 1 right -= 1几个关键点:
- 转置时内层循环从
i + 1开始,不是从0开始。因为对角线上的元素交换后不变,而且如果你从 0 开始,每个元素会交换两次,等于没换。 - 翻转操作可以用双指针,也可以用切片,但切片在 Python 里会产生新列表,不够“原地”,面试时最好写双指针版本。
- 这个方法的复杂度同样是 O(n^2) 时间和 O(1) 空间。
4.3 两种原地方案的对比与选型
| 维度 | 分圈轮换 | 转置 + 水平翻转 |
|---|---|---|
| 理解难度 | 坐标映射较复杂,容易下标写错 | 拆分后每一步语义清晰,几乎不会记错 |
| 代码量 | 逻辑集中,边界处理需要仔细 | 两段独立逻辑,维护性好 |
| 面试表现 | 能体现对矩阵结构的深度理解 | 能体现数学化简能力 |
| 出错点 | 四个下标容易写混 | 转置范围、翻转方向容易遗漏 |
如果是面试现场,我一般建议先想到哪种就写哪种,但写完一定用 2×2 或 3×3 的用例手算一遍。对初学者来说,"转置+翻转"的出错率远低于"分圈轮换",因为它把一个大问题拆成了两个能独立验证的小问题。工程上我更倾向于这种写法,可读性更好,review 的人不用为了你的四个下标推导半天。
5. 实战中的延伸:非方阵、逆时针、批量小技巧
5.1 非方阵怎么办
LeetCode 48 明确要求 n×n 方阵,但真实项目里图片和数据集很少有恰好方阵的情况。处理 m×n 的矩阵想顺时针旋转 90 度,结果会变成 n×m,所以不可能在原来的结构上原地完成,必须新建一个新尺寸的矩阵。
通用写法:
def rotate_rectangle(matrix): rows = len(matrix) cols = len(matrix[0]) result = [[0] * rows for _ in range(cols)] for i in range(rows): for j in range(cols): result[j][rows - 1 - i] = matrix[i][j] return result注意这里目标矩阵行数是原列数,列数是原行数,坐标里用的是rows - 1而不是n - 1。遇到非方阵,先和面试官确认能不能开新数组,通常都会允许,因为原地物理上不可能。
5.2 逆时针旋转的对应思路
逆时针 90 度同样可以拆操作。如果是转置派,逆时针等于先转置,再做垂直翻转(也就是每一列上下翻转)。如果你更习惯直接写坐标,逆时针的映射是(row, col) -> (n - 1 - col, row),把它改成分圈轮换也完全可行,四步操作的方向换成顺时针取数即可。
我偏好一个更通用的封装:写一个函数,通过参数控制旋转方向。
def rotate_general(matrix, clockwise=True): 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] if clockwise: # 水平翻转 for i in range(n): matrix[i].reverse() else: # 垂直翻转 for i in range(n // 2): matrix[i], matrix[n - 1 - i] = matrix[n - 1 - i], matrix[i]这样一次把旋转方向都覆盖了。顺手写出来,面试官会觉得你对这类变换有系统理解。
5.3 扩展:矩阵旋转在真实项目中的映射
这道题虽然出在算法题库,但实际应用场景非常密集。二维图像像素矩阵的旋转是图片编辑工具的基础操作;游戏里的地图块、棋盘类数据在展示前做旋转对齐;表格导出报表时把行转列再横向输出,本质就是转置加翻转;甚至某些数据增强工具在训练图像模型时,也会把整批图片做 90 度旋转来扩增样本。
我自己的体会是,真正工作时很少直接手写旋转逻辑,大都有现成库,但遇到自定义数据格式、流式处理、或者移动端内存受限的场景,你就必须明白底层坐标是怎么换的。记忆公式没用,理解推导过程才能在各种变体里快速改代码。
6. 常见问题与排查技巧实录
6.1 高频Bug:下标越界和漏转
下标越界通常出现在把 n 写成len(matrix[0])。方阵时两者没区别,但如果你按非方阵的思路去写,或者测试用例是 1×n 的矩阵,问题就暴露了。建议统一用len(matrix)作边长。
漏转的典型症状是矩阵旋转之后和原样一模一样。最常见的原因是转置的内层范围写错,比如写成了range(n),导致每对元素交换两次等于没交换。另一种情况是分圈写法里j的范围误写成range(n),导致每条边里的元素被重复轮换。重复轮换和只轮换一半在视觉上都是“矩阵几乎没变”,尤其 2×2 的用例中很难看出来,所以一定拿 3×3 甚至 4×4 的用例验证。
还有一个隐蔽的坑:Python 里对行做matrix[i] = reversed(matrix[i]),这其实生成了一个新的列表并赋值给当前行,虽然是局部操作,但严格来说不是所有语言都能接受这种写法。面试时尽量用双指针交换,避免节外生枝。
6.2 面试场景下的答题节奏
我的建议是:
- 先和面试官确认:“矩阵一定是方阵吗?可以额外开辟空间吗?”问清楚需求,避免做完才发现理解偏差。
- 先给暴力解法,哪怕知道要优化。它能帮助你顺利引出坐标映射公式,也给后续优化做铺垫。
- 写原地方案前,在白板上画一个小方阵,把四个顶点的坐标圈出来,再口头推一遍下标关系。别急着写代码,写错再改更浪费时间。
- 代码写完,用 n=1、n=2、n=3 三个用例分别自测。n=1 验证不死循环,n=2 验证最小有效旋转,n=3 验证奇数边长中心元素是否受影响。
- 最后自己主动提复杂度:“时间 O(n^2),空间 O(1)”,不用等面试官问。
6.3 避坑清单
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 结果与输入一模一样 | 转置范围错,交换两次 | 内层从 i+1 开始 |
| 矩阵只转了一半 | 内层 j 范围多算或少算 | j 取 range(i, n-i-1) |
| 使用额外数组返回 | 没读懂“原地”要求 | 改为交换操作 |
| 3×3 中间元素被改变 | 内层循环边界不正确 | 边界用 n-i-1 排除中心 |
| 每行翻转时漏了中间列 | while left < right 条件写错 | 用双指针,左闭右开即可 |
| 把 len(matrix[0]) 当边长 | 方阵习惯,但矩形会崩 | 统一用行数 len(matrix) |
说实话这题的 bug 类型翻来覆去就这几种,每一个我都踩过。第一次写分圈轮换时,我把n - 1 - i写成了n - i,结果用 3×3 用例测试时数组下标越界,改成n - 1 - i才对。从那以后我每次都会先在草稿纸上用具体数字推算一遍再动键盘。
我个人在实际操作中的体会是,这题没必要背任何一套解法,更不需要囤多种语言的模板。真正管用的是把旋转拆成“坐标映射”这一个核心,然后自己推一遍四个角的位置变化。推过之后,分圈写法自然就出来了,转置翻转只是锦上添花。后续遇到 90 度旋转的变体题、螺旋矩阵、对角线遍历,你会发现全是在同一套坐标关系上做文章。能把一道经典题吃透到这个程度,比盲目刷十道类似的题要值得多。