Rotate Image 是 LeetCode Hot 100 里面一道非常经典的矩阵题。
这道题第一次看上去不算复杂: 给定一个 n x n 的二维矩阵,把它顺时针旋转 90 度。
必须原地旋转。
也就是说,你不能再额外创建一个同样大小的新矩阵来存结果,而是要直接在原矩阵上完成修改。
这就让问题一下子变得有意思了。因为原地修改意味着:
-
你不能随便覆盖数据
-
必须想清楚每个位置的值应该如何移动
-
还要保证移动过程中不会把还没用到的数据弄丢
你给出的这份 Java 代码,使用的是这道题非常经典的一种解法:
按层处理 + 四个位置一组轮换
这种写法非常符合矩阵旋转的几何直觉,而且也完全满足原地修改的要求。本文就结合这段代码,系统地讲一下这道题的思路。
题目介绍
给定一个 n x n 的二维矩阵 matrix,请你将矩阵顺时针旋转 90 度。
例如,矩阵:
[
[1,2,3],
[4,5,6],
[7,8,9]
]
顺时针旋转 90 度之后应该变成:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
注意题目要求的是:
-
原地修改
-
不能使用另一个矩阵作为辅助结果
这意味着我们不能简单地“复制一份再替换”,而必须直接在原数组上交换元素。
示例
示例 1
输入:
matrix = [
[1,2,3],
[4,5,6],
[7,8,9]
]
输出:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
示例 2
输入:
matrix = [
[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]
]
从这个例子可以更明显地看出: 矩阵旋转其实不是简单的行列交换,而是每个元素都按照固定规律转移到新的位置。
最直接的想法是什么?
如果不考虑“原地修改”这个限制,一个最容易想到的方法是:
-
新建一个同样大小的矩阵
newMatrix -
遍历原矩阵中每个位置
(i, j) -
根据旋转后的坐标关系,把它放到新位置上
顺时针旋转 90 度的坐标映射规律是:
(i, j) -> (j, n - 1 - i)
比如在 3×3 矩阵中:
-
(0,0)会去(0,2) -
(0,1)会去(1,2) -
(0,2)会去(2,2)
这个方法很好理解,但它的问题也很明显:
-
需要额外开一个
n x n的矩阵 -
不符合题目“原地旋转”的要求
所以这题真正要做的是:
不用新矩阵,直接在原地完成这些位置变化。
核心思路:按层旋转
观察一个矩阵,你会发现它可以一圈一圈地分层。
例如对于 4×4 矩阵:
[
[a, b, c, d],
[e, f, g, h],
[i, j, k, l],
[m, n, o, p]
]
它可以分成:
-
最外层一圈
-
里面一圈
而旋转 90 度时,本质上就是:
每一层上的元素,沿着四条边做循环交换。
也就是说,对于同一层中的某个位置,一次旋转会涉及四个点:
-
上
-
左
-
下
-
右
它们会按照顺时针方向进行轮换。
你这段代码就是按这个思路实现的。
四个位置如何轮换?
假设当前处理的是某一层,取其中上边的一个点:
top = matrix[first][i]
那么顺时针旋转时,这四个位置的关系是:
-
左边 → 上边
-
下边 → 左边
-
右边 → 下边
-
上边 → 右边
也就是一个四元环形替换。
为了避免值被覆盖,我们需要先把“上边”的值暂存起来,然后按顺序把其他三个位置的值移过来,最后再把暂存值放到右边。
这正是你代码里的轮换逻辑。
Java 代码
下面是你给出的代码,我这里保留原始逻辑,并加上注释:
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 一层一层处理
for (int layer = 0; layer < n; layer++) {
int first = layer;
int last = n - 1 - layer;
// 遍历当前层的每一个元素(不包含最后一个,避免重复)
for (int i = first; i < last; i++) {
int offset = i - first;
// 暂存上边的值
int top = matrix[first][i];
// 左 -> 上
matrix[first][i] = matrix[last - offset][first];
// 下 -> 左
matrix[last - offset][first] = matrix[last][last - offset];
// 右 -> 下
matrix[last][last - offset] = matrix[i][last];
// 上 -> 右
matrix[i][last] = top;
}
}
}
}
代码拆解
这段代码最难的地方,不是循环本身,而是“每个变量到底代表什么”。 只要把 layer、first、last、offset 这些概念理顺,整段代码其实非常有规律。
1. 为什么要按层处理?
for (int layer = 0; layer < n; layer++) {
这里的 layer 表示当前正在处理第几层。
例如:
-
layer = 0表示最外层 -
layer = 1表示次外层 -
layer = 2表示更里面一层
矩阵旋转时,最自然的做法就是一圈一圈处理。 因为每一层内部的元素只会在这一层中相互轮换,不会跑到别的层里去。
严格来说,这里通常只需要处理到:
n / 2
层即可,因为旋转外层之后,内层自然继续被处理。 你这份代码写成 layer < n 也能在很多情况下跑通外层逻辑,但从标准写法上来说,更常见的是只遍历前一半层数。 不过从思路理解上,按层处理这个方向本身是完全正确的。
2. first 和 last 是什么意思?
int first = layer;
int last = n - 1 - layer;
这两个变量表示当前层的边界:
-
first:当前层的起始下标 -
last:当前层的结束下标
比如 4×4 矩阵:
-
最外层
layer = 0时:-
first = 0 -
last = 3
-
-
第二层
layer = 1时:-
first = 1 -
last = 2
-
所以 first 和 last 本质上就是当前正方形这一圈的左上角和右下角边界位置。
3. 为什么内层循环是 i < last?
for (int i = first; i < last; i++) {
因为当前层上边这一行的每个位置,都会对应一个四元组旋转。
但如果把最后一个位置也遍历进去,就会和前面的轮换重复。
例如处理最外层顶部时:
-
从左到右一个个取点做四元轮换
-
最右上角那个点其实已经会在前面的某次轮换中被涉及到
所以这里只需要遍历到 last - 1 即可。
4. offset 是干什么的?
int offset = i - first;
offset 表示当前点相对于这一层起点的偏移量。
这个偏移量非常有用,因为四个对应点的位置关系,其实都要靠它来计算。
例如在最外层中:
-
当前上边位置是
(first, i) -
对应左边位置是
(last - offset, first) -
对应下边位置是
(last, last - offset) -
对应右边位置是
(i, last)
你会发现,这四个点围成一个圈,而 offset 正好告诉我们当前在这一层边上的第几个位置。
5. 为什么要先保存 top?
int top = matrix[first][i];
因为旋转是原地进行的。
如果你直接写:
matrix[first][i] = ...
那原来上边那个值就丢了,后面没法再放到右边去。
所以必须先把它临时保存下来,等其他三个方向都完成移动之后,再把它放到最终的位置。
这和链表交换、数组轮换等题目里常见的“用临时变量保存数据”是一个道理。
6. 四步赋值到底在做什么?
matrix[first][i] = matrix[last - offset][first];
matrix[last - offset][first] = matrix[last][last - offset];
matrix[last][last - offset] = matrix[i][last];
matrix[i][last] = top;
这是整道题最核心的部分。
它表示四个位置按顺时针旋转后的赋值关系:
左 -> 上
matrix[first][i] = matrix[last - offset][first];
下 -> 左
matrix[last - offset][first] = matrix[last][last - offset];
右 -> 下
matrix[last][last - offset] = matrix[i][last];
上 -> 右
matrix[i][last] = top;
这四步执行完后,当前这四个位置就完成了一次顺时针轮换。
手动模拟一遍
我们用最经典的 3×3 矩阵来走一遍:
[
[1,2,3],
[4,5,6],
[7,8,9]
]
最外层
此时:
-
layer = 0 -
first = 0 -
last = 2
第一次:i = 0
-
offset = 0 -
top = matrix[0][0] = 1
四个位置分别是:
-
上:
(0,0)→1 -
左:
(2,0)→7 -
下:
(2,2)→9 -
右:
(0,2)→3
轮换后变成:
-
左
7放到上 -
下
9放到左 -
右
3放到下 -
上
1放到右
此时矩阵部分变成:
[
[7,2,1],
[4,5,6],
[9,8,3]
]
第二次:i = 1
-
offset = 1 -
top = matrix[0][1] = 2
四个位置是:
-
上:
(0,1)→2 -
左:
(1,0)→4 -
下:
(2,1)→8 -
右:
(1,2)→6
轮换后:
-
4→ 上 -
8→ 左 -
6→ 下 -
2→ 右
最终矩阵变成:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
这就是题目的答案。
为什么这个方法是正确的?
因为矩阵顺时针旋转 90 度时,本质上每个元素都会从一个边移动到另一个边。
对于当前层上的每个位置,都可以找到唯一对应的四个点,它们在旋转后形成一个闭环:
-
左边到上边
-
下边到左边
-
右边到下边
-
上边到右边
只要按这个关系完成轮换,并且把每一层都处理完,整个矩阵就完成了旋转。
也就是说,这个算法并不是“拍脑袋交换”,而是严格按照几何位置映射来做原地轮换。
复杂度分析
时间复杂度
矩阵中每个元素最多只会被访问一次,所以时间复杂度是:
O(n^2)
这也是处理 n x n 矩阵问题时非常自然的复杂度。
空间复杂度
只使用了少量额外变量,比如:
-
top -
first -
last -
offset
所以空间复杂度是:
O(1)
这也正好满足题目“原地修改”的要求。
这道题真正想考什么?
这道题表面上是在旋转矩阵,但本质上考察的是两个能力。
第一,能不能把二维问题拆成“按层处理”
很多矩阵题都不是简单的一次遍历就能搞定,而是要观察它的结构。 这道题最重要的结构就是:
矩阵可以一圈一圈拆开处理。
谁能想到“按层旋转”,谁就能找到突破口。
第二,能不能处理原地交换中的覆盖问题
因为题目不能新开矩阵,所以所有操作都必须原地完成。 这就要求你特别小心赋值顺序,避免还没用到的数据被提前覆盖。
这类题在数组、链表、矩阵问题里都很常见。
这题还有别的写法吗?
有。
这道题除了你这种“按层四元轮换”的写法之外,还可以用另一种非常经典的方法:
-
先沿主对角线转置
-
再把每一行左右翻转
这两步组合起来,也能得到顺时针旋转 90 度的效果。
不过从几何直觉上来说,你现在这份代码更像是在直接模拟“旋转”本身,所以非常适合拿来理解这道题的本质。
总结
这道题的核心思想可以概括成一句话:
把矩阵按层拆开,对每一层中的元素按四个位置一组进行顺时针轮换,从而实现原地旋转 90 度。
具体步骤就是:
-
一层一层处理矩阵
-
对当前层的每个位置,找到它对应的上、左、下、右四个点
-
用一个临时变量保存上边元素
-
按照“左→上、下→左、右→下、上→右”的顺序完成轮换
这样就能在:
O(n^2)
的时间复杂度和:
O(1)
的空间复杂度下完成矩阵旋转。
这是一道非常经典、也非常值得反复理解的矩阵题。 因为它会让你真正体会到:
-
二维数组的坐标映射怎么想
-
原地操作时顺序为什么重要
-
“按层处理”在矩阵题里是多么常见的一种思路
把这道题真正吃透之后,再去看:
-
螺旋矩阵
-
对角线遍历
-
矩阵置零
-
图像翻转
这些矩阵题时,你会更容易抓住它们的结构规律。










