力扣hot100之48:旋转图像
力扣hot100之48:旋转图像

LeetCode 48:旋转图像(Rotate Image)题解

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;
            }
        }
    }
}

代码拆解

这段代码最难的地方,不是循环本身,而是“每个变量到底代表什么”。 只要把 layerfirstlastoffset 这些概念理顺,整段代码其实非常有规律。


1. 为什么要按层处理?

for (int layer = 0; layer < n; layer++) {

这里的 layer 表示当前正在处理第几层。

例如:

  • layer = 0 表示最外层

  • layer = 1 表示次外层

  • layer = 2 表示更里面一层

矩阵旋转时,最自然的做法就是一圈一圈处理。 因为每一层内部的元素只会在这一层中相互轮换,不会跑到别的层里去。

严格来说,这里通常只需要处理到:

n / 2

层即可,因为旋转外层之后,内层自然继续被处理。 你这份代码写成 layer < n 也能在很多情况下跑通外层逻辑,但从标准写法上来说,更常见的是只遍历前一半层数。 不过从思路理解上,按层处理这个方向本身是完全正确的。


2. firstlast 是什么意思?

int first = layer;
int last = n - 1 - layer;

这两个变量表示当前层的边界:

  • first:当前层的起始下标

  • last:当前层的结束下标

比如 4×4 矩阵:

  • 最外层 layer = 0 时:

    • first = 0

    • last = 3

  • 第二层 layer = 1 时:

    • first = 1

    • last = 2

所以 firstlast 本质上就是当前正方形这一圈的左上角和右下角边界位置。


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)

这也正好满足题目“原地修改”的要求。


这道题真正想考什么?

这道题表面上是在旋转矩阵,但本质上考察的是两个能力。

第一,能不能把二维问题拆成“按层处理”

很多矩阵题都不是简单的一次遍历就能搞定,而是要观察它的结构。 这道题最重要的结构就是:

矩阵可以一圈一圈拆开处理。

谁能想到“按层旋转”,谁就能找到突破口。

第二,能不能处理原地交换中的覆盖问题

因为题目不能新开矩阵,所以所有操作都必须原地完成。 这就要求你特别小心赋值顺序,避免还没用到的数据被提前覆盖。

这类题在数组、链表、矩阵问题里都很常见。


这题还有别的写法吗?

有。

这道题除了你这种“按层四元轮换”的写法之外,还可以用另一种非常经典的方法:

  1. 先沿主对角线转置

  2. 再把每一行左右翻转

这两步组合起来,也能得到顺时针旋转 90 度的效果。

不过从几何直觉上来说,你现在这份代码更像是在直接模拟“旋转”本身,所以非常适合拿来理解这道题的本质。


总结

这道题的核心思想可以概括成一句话:

把矩阵按层拆开,对每一层中的元素按四个位置一组进行顺时针轮换,从而实现原地旋转 90 度。

具体步骤就是:

  1. 一层一层处理矩阵

  2. 对当前层的每个位置,找到它对应的上、左、下、右四个点

  3. 用一个临时变量保存上边元素

  4. 按照“左→上、下→左、右→下、上→右”的顺序完成轮换

这样就能在:

O(n^2)

的时间复杂度和:

O(1)

的空间复杂度下完成矩阵旋转。

这是一道非常经典、也非常值得反复理解的矩阵题。 因为它会让你真正体会到:

  • 二维数组的坐标映射怎么想

  • 原地操作时顺序为什么重要

  • “按层处理”在矩阵题里是多么常见的一种思路

把这道题真正吃透之后,再去看:

  • 螺旋矩阵

  • 对角线遍历

  • 矩阵置零

  • 图像翻转

这些矩阵题时,你会更容易抓住它们的结构规律。

因为“旋转图像”本身就是矩阵原地操作题里非常经典的一道代表题。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇