旋转图像

给定一个 n x n 的二维矩阵 matrix 表示一个图像。

请你将图像顺时针旋转 90 度。

你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。
请不要使用另一个矩阵来旋转图像。

示例 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]]

提示:

  • n == matrix.length == matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

解法一(转置 + 反转每一行):

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();

        for (int i = 0; i < n; i++)
        {
            for (int j = i + 1; j < n; j++)
            {
                swap(matrix[i][j], matrix[j][i]);
            }
        }

        for (int i = 0; i < n; i++)
        {
            reverse(matrix[i].begin(), matrix[i].end());
        }
    }
};

解法二(分层四点交换):

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();

        for (int i = 0; i < n / 2; i++)
        {
            for (int j = i; j < n - 1 - i; j++)
            {
                int 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;
            }
        }
    }
};

核心思想

这题要求原地把一个 n x n 矩阵顺时针旋转 90 度。

如果不考虑原地限制,最直观的做法是新建一个矩阵 ans

对于原矩阵中的位置:

(i, j)

顺时针旋转 90 度之后,它会去到新矩阵中的位置:

(j, n - 1 - i)

也就是:

ans[j][n - 1 - i] = matrix[i][j];

但是题目明确要求不能使用另一个矩阵,所以我们需要在原矩阵内部完成这个坐标变换。

这题最常用、最清晰的原地做法是:

先沿主对角线转置,再反转每一行。

这两个操作都可以在原矩阵上完成,并且合起来刚好等价于顺时针旋转 90 度。

坐标变化公式

先看顺时针旋转 90 度本身的坐标变化。

原矩阵中第 i 行第 j 列的元素:

matrix[i][j]

旋转后应该出现在:

matrix[j][n - 1 - i]

原因是:

  • 原来的行号 i 会变成新的列号,并且方向反过来,所以是 n - 1 - i
  • 原来的列号 j 会变成新的行号,所以是 j

因此目标映射是:

(i, j) -> (j, n - 1 - i)

只要我们的原地操作能让每个元素都满足这个映射,就完成了顺时针旋转。

为什么转置后反转每一行是正确的

解法一分两步。

1. 沿主对角线转置

主对角线转置就是交换:

matrix[i][j]matrix[j][i]

转置之后,元素的坐标变化是:

(i, j) -> (j, i)

例如:

1 2 3        1 4 7
4 5 6   ->   2 5 8
7 8 9        3 6 9

2. 反转每一行

反转每一行时,行号不变,列号左右翻转。

对于一个 n 列的矩阵,列号 i 会变成:

n - 1 - i

所以坐标变化是:

(row, col) -> (row, n - 1 - col)

转置之后,一个元素已经从:

(i, j)

来到了:

(j, i)

再反转它所在的这一行,坐标继续变成:

(j, n - 1 - i)

这正好就是顺时针旋转 90 度的目标位置。

因此:

(i, j) -> (j, i) -> (j, n - 1 - i)

等价于:

(i, j) -> (j, n - 1 - i)

所以“转置 + 反转每一行”可以完成顺时针旋转。

为什么转置要从 j = i + 1 开始

转置时只需要交换主对角线两侧的元素。

主对角线上的元素满足:

i == j

它们转置后仍然在原位置,不需要交换。

如果从 j = 0 开始遍历整个矩阵,会出现一个问题:

第一次交换:

swap(matrix[i][j], matrix[j][i]);

后面遍历到 (j, i) 时,又会交换回来。

所以我们只遍历主对角线右上方的元素:

for (int j = i + 1; j < n; j++)

这样每一对需要交换的元素只会被处理一次。

分层四点交换的思路

解法二不通过转置,而是直接按照旋转后的四个位置进行交换。

对于某一层中的一个位置:

(i, j)

它对应的四个位置分别是:

  • 上:(i, j)
  • 右:(j, n - 1 - i)
  • 下:(n - 1 - i, n - 1 - j)
  • 左:(n - 1 - j, i)

顺时针旋转后:

  • 左边的值会去到上边
  • 上边的值会去到右边
  • 右边的值会去到下边
  • 下边的值会去到左边

所以代码中执行的是:

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

这里的 temp 保存上边位置的原值,避免在交换过程中被覆盖。

每次操作会同时完成四个位置的旋转。
外层循环处理一层一层的边框,内层循环处理当前层中的每一组四点。

正确性证明

我们证明:解法一可以将矩阵原地顺时针旋转 90 度。

结论 1:转置后,每个元素从 (i, j) 移动到 (j, i)

算法第一步沿主对角线交换:

swap(matrix[i][j], matrix[j][i]);

并且只交换 j > i 的位置。

对于任意一对非对角线位置 (i, j)(j, i),算法恰好交换一次。

因此,原来位于 (i, j) 的元素,转置后会位于 (j, i)

对于主对角线元素,i == j,转置前后位置相同,也符合:

(i, i) -> (i, i)

所以结论 1 成立。

结论 2:反转每一行后,每个元素的列号会变成 n - 1 - col

算法第二步对每一行执行:

reverse(matrix[i].begin(), matrix[i].end());

反转一行不会改变元素的行号。

在长度为 n 的一行中,原列号为 col 的元素,反转后会出现在列号:

n - 1 - col

的位置。

所以反转每一行的坐标变化是:

(row, col) -> (row, n - 1 - col)

结论 2 成立。

结论 3:两步合起来等价于顺时针旋转 90

对于原矩阵中的任意元素 (i, j)

第一步转置后,它的位置变成:

(j, i)

第二步反转每一行后,它的位置变成:

(j, n - 1 - i)

这正好是顺时针旋转 90 度的目标坐标。

也就是说,对任意元素都有:

(i, j) -> (j, n - 1 - i)

因此所有元素都会被放到旋转后的正确位置。

结论 4:算法满足原地要求

转置操作只使用若干次 swap

反转每一行时,标准库 reverse 也是在当前行内部交换元素。

整个过程中没有创建另一个 n x n 矩阵,只使用了常数级额外变量。

所以算法满足题目的原地旋转要求。

得出结论

由结论 1 可知,第一步可以正确完成主对角线转置。

由结论 2 可知,第二步可以正确完成每一行的左右反转。

由结论 3 可知,这两个操作合起来正好等价于顺时针旋转 90 度。

由结论 4 可知,算法满足原地操作要求。

因此算法正确。

举例理解

以:

matrix = [[1,2,3],
          [4,5,6],
          [7,8,9]]

为例。

第一步,沿主对角线转置:

1 2 3        1 4 7
4 5 6   ->   2 5 8
7 8 9        3 6 9

第二步,反转每一行:

1 4 7        7 4 1
2 5 8   ->   8 5 2
3 6 9        9 6 3

最终结果就是:

[[7,4,1],
 [8,5,2],
 [9,6,3]]

再看坐标变化。

原来左上角元素 1 的位置是:

(0, 0)

旋转后位置应该是:

(0, 2)

根据公式:

(i, j) -> (j, n - 1 - i)

代入 i = 0j = 0n = 3

(0, 0) -> (0, 2)

正好对应最终矩阵中的右上角。

复杂度分析

解法一

转置矩阵时,需要处理大约一半的元素。

反转每一行时,每个元素也会被处理一次。

所以总时间复杂度是:

O(n^2)

整个过程只使用常数个额外变量,没有使用额外矩阵。

所以空间复杂度是:

O(1)

解法二

分层四点交换时,每个元素也只会被移动一次。

所以时间复杂度是:

O(n^2)

只使用 temp 等常数个变量。

所以空间复杂度是:

O(1)

两种解法都满足原地旋转要求。

如果只追求代码清晰,推荐使用解法一。