刷 LeetCode 的人基本都绕不开第 48 题:旋转图像。这道题在 Hot 100 里挂了挺久,难度标的是 medium,但实际面试里出现频率非常高。核心考点就是矩阵转置、原地修改、以及二维数组下标变换的理解。很多人第一次做的时候,要么写出一个“看起来对但原地要求没做到”的版本,要么被边界条件绕晕。这篇文章我就把矩阵旋转这事彻底讲透,包括为什么“转置+逐行反转”是最优雅的解法、代码怎么写不会翻车、以及常见的几个坑。适合刚开始刷题的人打基础,也适合面试前快速复习的人拿来过一遍思路。
1. 题目到底在说什么:先把问题本身吃透
1.1 输入输出与“原地修改”的硬性要求
题目给的是一个 n x n 的二维矩阵,要求把整个矩阵顺时针旋转 90 度。比如:
输入: 1 2 3 4 5 6 7 8 9 输出: 7 4 1 8 5 2 9 6 3这看起来像图像处理里的旋转操作,本质上是二维数组坐标映射问题。关键约束是“原地修改”,意味着你不能先新建一个同样的矩阵,把旋转后的结果填到新矩阵里,再把它拷贝回原矩阵。空间复杂度得控制在 O(1),也就是说,除了几个临时变量,不能开辟跟输入规模相关的辅助空间。
这一点很重要,因为很多新手第一次写题时会下意识地创建一个 res 数组,最后把 res 赋值给 matrix。这功能上是对的,但题目明确要求原地,面试官想要考察的就是“你是不是真的理解了下标变换,而不是靠额外空间偷懒”。
1.2 最容易想到的解法:分层一圈一圈旋转
在看到“转置+反转”之前,大部分人第一反应是“一层一层转”。从最外层往最里层,每一圈做四个元素的轮换。思路本身没有错,也是 LeetCode 官方题解里的第一种方法。但实现细节相当烦人。
你需要维护四个边界,比如 left、right、top、bottom,然后每层循环从 0 到 n-offset-1,每次交换四个对应位置:
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++、right--、top++、bottom--。这个做法的时间复杂度也是 O(n²),空间 O(1),但它有两个问题:第一,边界条件特别多,写着写着就容易数组越界;第二,面试手撕代码时,一旦紧张,这种“多变量维护”的写法非常容易翻车。
我并不是说分层旋转不能写,它很直观,但刷题场景下我更倾向于“原理简单、代码模板化”的方案,也就是下面要重点讲的转置加反转。
1.3 方阵限制:为什么题目强调 n x n
这题只讨论方阵不是没有原因的。只有当行数和列数相等时,“转置后再反转每一行”才能准确得到顺时针旋转 90 度的结果。如果是 m x n 的矩形矩阵,转置后尺寸变成 n x m,旋转的逻辑就完全不一样了。
所以拿到这题,第一件事不是写代码,而是确认输入类型。看到矩阵长宽相等,就可以放心用转置思路;如果不相等,那题目性质就变了。面试时主动问一句“输入是不是方阵”,反而会显得你对边界情况敏感,这是加分项。
2. 为什么“转置+逐行反转”是这道题的优雅解
2.1 从坐标映射看旋转的本质
任何一个矩阵操作,本质上都是在回答一个问题:原矩阵里位置 (i, j) 的元素,旋转之后应该跑到哪个位置?
对于顺时针旋转 90 度,目标位置是:
(i, j) -> (j, n - 1 - i)也就是说,原来在第 i 行第 j 列的元素,旋转后跑到第 j 行第 n-1-i 列。这个公式是判断所有解法对错的唯一依据。你写出任何代码,都可以拿这个公式来验算。
很多题解上来就讲“转置+左右反转”,但没解释为什么这两步恰好能凑出上面的坐标公式。其实,拆开看就清楚了。
2.2 转置:把行变成列
矩阵转置就是把元素 (i, j) 换成 (j, i),相当于沿着主对角线做一次对称翻转。原本竖着的一列,转置后变成横着的一行。
放在旋转的场景里,转置完成了坐标的“行列互换”:
(i, j) -> (j, i)这一步的效果是让矩阵像被“放倒”了一样,但还没有完成旋转的方向调整。所以转置只是整个变换的一半。
2.3 逐行反转:把每一行倒过来
转置之后,再做一次“左右反转”,也就是把每一行的元素顺序倒过来:
(j, i) -> (j, n - 1 - i)两步连在一起:
(i, j) -> (j, i) -> (j, n - 1 - i)恰好就是顺时针旋转 90 度的目标坐标公式。这就是为什么“转置 + 逐行反转”是标准答案。逻辑链条非常干净,没有任何多余的变量需要维护。
2.4 另一种等价写法:先上下反转再转置
值得注意的是,官方题解里还有另一种做法:先上下反转,再做转置。
验证一下:上下反转 (i, j) -> (n-1-i, j),再做转置 (n-1-i, j) -> (j, n-1-i)。结果同样是顺时针旋转 90 度。所以“转置+左右反转”和“上下反转+转置”是等价的。
这两种写法没有优劣之分,你喜欢哪个就用哪个。但有一点要提醒:面试时千万别在两种思路之间来回横跳。你明明想的是“转置+左右反转”,写到一半突然改成“先上下反转”,那大概率会把方向搞反。选定一条路,从一而终。
2.5 复杂度分析:面试官想听到的答案
这个解法的时间复杂度是 O(n²),因为每个元素恰好访问一次。空间复杂度是 O(1),没有使用任何与 n 相关的辅助存储。面试时把这个复杂度讲清楚,比单纯甩出代码要好得多,因为这表明你理解了这个操作的代价。
3. 手把手实现:代码怎么写,边界怎么控制
3.1 转置的循环范围:为什么只遍历上三角
转置代码的写法看似简单,但特别容易写成下面这种“看起来对,其实矩阵没变”的版本:
for i in range(n): for j in range(n): matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]问题在于,当外层循环 i=1、内层 j=0 时,先交换 matrix[1][0] 和 matrix[0][1];等到外层 i=0、内层 j=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]这样每一对元素只交换一次,对角元素完全不动。这个细节是整个转置步骤里最重要的“坑点”。
3.2 逐行反转:双指针还是内置反转函数
转置完成后,每一行已经变成了原矩阵的一列。这时候再把每一行左右倒过来即可。
双指针是我最推荐的方式,因为逻辑显式、不容易出错:
for i in range(n): l, r = 0, n - 1 while l < r: matrix[i][l], matrix[i][r] = matrix[i][r], matrix[i][l] l += 1 r -= 1Python 里也有更直接的写法:
for row in matrix: row.reverse()不过在很多面试场景下,手写双指针更容易展示你对原地交换的理解。至于 Java 和 C++,内置反转对原生数组不是那么顺手,所以双指针往往是更通用的选择。
3.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 + 1, n): matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] # 再逐行左右反转 for i in range(n): l, r = 0, n - 1 while l < r: matrix[i][l], matrix[i][r] = matrix[i][r], matrix[i][l] l += 1 r -= 1Java 版本也差不多:
class Solution { public void rotate(int[][] matrix) { int n = matrix.length; 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 l = 0, r = n - 1; while (l < r) { int tmp = matrix[i][l]; matrix[i][l] = matrix[i][r]; matrix[i][r] = tmp; l++; r--; } } } }这里有个小细节:LeetCode 要求函数返回 void,意味着你必须在原数组上改,而不是返回一个新数组。如果函数最后写的是return res,在语言层面就已经不符合题目的原地要求了。
3.4 自测用例:奇数和偶数边长都要覆盖
我平时刷这题,会固定用这几个用例自测:
- n=1:
[[1]],旋转后还是[[1]],主要是验证没有越界。 - n=2:
[[1,2],[3,4]],旋转后应该是[[3,1],[4,2]]。这个小矩阵最能测出反转方向对不对。 - n=3:
[[1,2,3],[4,5,6],[7,8,9]],旋转后是[[7,4,1],[8,5,2],[9,6,3]],对应 LeetCode 给的示例。 - n=4:选一个包含 1 到 16 的矩阵,手动算一下四个角和中心区域,用来验证大规模情况。
为什么要特意覆盖奇数和偶数?因为偶数矩阵有“核心四格”,奇数矩阵有正中心的一个元素。边界处理稍稍出错,在 n=2 或 n=3 上就会立刻暴露出来。
4. 常见错误与排查实录
4.1 转置完矩阵看起来没变
这是最常见的报错之一。现象是:你明明写了 swap 逻辑,但矩阵跑完一遍后跟原始输入一模一样。
原因基本都是转置时遍历了全部 n x n 个位置,导致每一对元素被交换了两次。排查方法很简单:在转置后打印一下矩阵,如果发现矩阵完全没变,先看循环边界是不是从j = 0开始。改成j = i + 1之后,问题一般就消失了。
4.2 旋转方向反了,变成了逆时针
如果你得到的矩阵是逆时针旋转 90 度的结果,那多半是第二部把“左右反转”写成了“上下反转”。
比如原始矩阵:
1 2 3 4转置后变成:
1 3 2 4这时候左右反转得到的是:
3 1 4 2这是正确答案(顺时针)。如果你做的是上下反转,得到的是:
2 4 1 3方向就反了。这种错误靠眼睛看不容易发现,但用 n=2 的矩阵手算一次,立刻就能定位。
4.3 结果对了,但被判为“没有原地修改”
这种情况通常发生在你新建了一个res二维数组,最后通过matrix = res来覆盖。看起来好像“改”了原矩阵,但在语言层面,你只是把局部变量重新指向了新对象,原矩阵的内容并没有被逐元素修改。
LeetCode 的判题逻辑会直接检查传入的 matrix 对象内容,所以必须用交换的方式在原来的二维数组上改。记住一个原则:所有写操作都通过matrix[i][j]的形式发生在原数组元素上。
4.4 n=1 或空矩阵的处理
n=1 时,转置循环因为range(1, 1)为空,内置逻辑会自动跳过;反转循环因为 l=0、r=0,也不会执行。所以不用特判。但如果你用的是“分层旋转”写法,遇到 n=1 时容易因为边界维护出错,写完后最好先测一下。
4.5 常见问题速查表
| 错误现象 | 可能原因 | 修复办法 |
|---|---|---|
| 转置后矩阵完全没变 | 遍历了全部位置,交换了两次 | 改为遍历上三角,j 从 i+1 开始 |
| 旋转结果变成逆时针 | 反转方向用了上下反转 | 统一改成左右反转 |
| 答案正确但被判 Wrong Answer | 新建了辅助矩阵,原地要求没满足 | 删除 res,在原矩阵上做元素交换 |
| n=1 或空矩阵报错 | 边界维护出了问题 | 转置+反转写法天然跳过,无需额外特判 |
| 奇偶边长结果不一致 | 没有同时测奇数和偶数用例 | 固定使用 n=1、2、3、4 四组用例回归 |
4.6 一个帮你少踩坑的调试小技巧
如果旋转结果不对,先别急着加日志。拿一支笔,在纸上画一个 2x2 矩阵,手动跑一遍你的坐标变换。比如左上角的元素,顺时针旋转后应该到右上角。如果代码结果里左上角元素跑去了左下角,那基本可以断定是方向反了。这种单元素追踪法,比盯着整个矩阵看高效得多。
5. 举一反三:旋转矩阵的变体与面试延伸
5.1 逆时针旋转 90 度怎么做
如果题目改成逆时针旋转 90 度,只需要把“逐行反转”换成“逐列反转”的思路。用坐标公式推导一下,逆时针 90 度的目标是:
(i, j) -> (n - 1 - j, i)先做转置(i, j) -> (j, i),再做上下反转(j, i) -> (n-1-j, i),结果正好是逆时针。所以记住一个口诀:
- 顺时针 = 转置 + 左右反转
- 逆时针 = 转置 + 上下反转
面试时遇到变体题,直接套这个口诀会非常快。
5.2 旋转 180 度怎么做
旋转 180 度更简单,两步对称变换就行:先上下反转,再左右反转。坐标公式是:
(i, j) -> (n - 1 - i, n - 1 - j)不用转置,直接两个方向的镜像操作。这也是一种常见的扩展考点,写起来比旋转 90 度还简单。
5.3 旋转 k 次 90 度
如果题目说“旋转 k 次”,不要真的循环 k 次。先对 k 取模 4:
- k % 4 == 0:不变
- k % 4 == 1:顺时针 90 度
- k % 4 == 2:旋转 180 度
- k % 4 == 3:逆时针 90 度
然后只执行对应的一次变换。这个优化在时间上非常明显,尤其是 k 很大的时候。面试中如果你能主动想到取模,会是个加分点。
5.4 这类“坐标变换拆解”套路能推广到哪里
“先做一个简单变换,再做另一个简单变换,组合成目标变换”的思路,不只适用于矩阵旋转。很多二维数组题目都能这样拆:
- 矩阵按行、按列反转:直接对称交换。
- 对角线镜像翻转:转置本身就是一个重要操作。
- 矩阵从外圈到内圈的遍历:分层思想可以复用。
- 图像处理里的实际旋转:本质就是像素坐标映射,配合分块缓存可以有效提高性能。
在准备算法面试时,与其死记每一道题的代码,不如多想想“这道题能不能用哪几种基础变换组合出来”。矩阵旋转就是一个最好的训练素材,因为它是二维坐标变换的经典代表。
我自己的经验是:刚开始刷这题时也很喜欢用分层旋转,觉得那样更“直观”,但每次写到下标交换就头皮发麻,不是这里越界就是那里没换对。后来把所有旋转相关题目都统一到“转置 + 反转”这套模板上,代码稳定了很多,面试手撕时也更有底气。
最后再分享一个小技巧:写代码前,先把目标坐标公式写在注释里,比如// (i, j) -> (j, n-1-i),然后根据公式反推需要哪两步变换。这样即使中途思路断了一下,回头看看注释就能续上,不用重新推导整个流程。测试时记得跑一遍 n=1、n=2、n=3 和 n=4,这四个用例基本能覆盖掉所有九十度旋转的边界坑。