【每日力扣11】旋转图像

一、题目[leetCode-48]

给定一个 n × 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]]

示例 3:

输入:matrix = [[1]]

输出:[[1]]

示例 4:

输入:matrix = [[1,2],[3,4]]

输出:[[3,1],[4,2]]

提示:

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

二、思路

环状替换

结合《Day3 旋转数组》,我们可以使用其中所学环状替换来完成。本题可以将矩阵层层剥开,每层构建一个“环”(用数组体现)。然后对于每一层构建出来的数组,使用环状替换 next = (current + (n-1))% 4(n-1) (最外层的迭代公式)。

最外层数组长度为4(n-1)(边长为n),次外层数组长度为4((n-2)-1)(边长为(n-2)),以此类推,当最内层边长为2(对应n为偶数的情况)或边长为1(对应n为奇数的情况),旋转完成后退出循环。结束。

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        for(int i = n, j = 0; i > 1; i-=2, j++)//将矩阵从最外层层层剥开,i表示每层的边长,j表示每层开始遍历时数组的下标
        {
            //(一)将矩阵外层——[j, j+i-1]×[j, j+1-1]的外表面拷贝到数组“环”
            vector<int> loop((i-1)*4);//对于每一层,先构建环
            int loopIndex = 0;
            for(int k = j; k < j + i; k++)
                loop[loopIndex++] = matrix[j][k];//将该层的上面边部分matrix[j][j,j+i-1]加入loop[]中
            for(int k = j + 1; k < j + i - 1; k++)
                loop[loopIndex++] = matrix[k][i+j-1];//将该层的右面边部分matrix(j,j+i-1)[j+i-1]加入loop[]中
            for(int k = j + i - 1; k >= j; k--)
                loop[loopIndex++] = matrix[j+i-1][k];//将该层的下面边部分matrix[j+i-1](j,j+i-1)加入loop[]中
            for(int k = j + i - 2; k >= j + 1; k--)
                loop[loopIndex++] = matrix[k][j];//将该层的左面边部分matix(j,j+i-1)[j]加入loop[]中
            //至此,矩阵的这一层装入数组loop[]完成。
            //(二)对loop[]进行环状替换
            for(int start = 0; start < i - 1; start++)
            {
                int current = start;
                int prev = loop[start];//环状替换前的初始化
                do
                {
                    int next = (current + i - 1) % ((i-1)*4);
                    swap(loop[next], prev);
                    current = next;
                }
                while(current != start);
            }
            //环状替换完成。
            //(三)将数组loop[]的值依次放回矩阵的对应位置
            loopIndex = 0;
            for(int k = j; k < j + i; k++)
                matrix[j][k] = loop[loopIndex++];//将该层的上面边部分加入loop[]中
            for(int k = j + 1; k < j + i - 1; k++)
                matrix[k][i+j-1] = loop[loopIndex++];//将该层的右面边部分加入loop[]中
            for(int k = j + i - 1; k >= j; k--)
                matrix[j+i-1][k] = loop[loopIndex++];//将该层的下面边部分加入loop[]中
            for(int k = j + i - 2; k >= j + 1; k--)
                matrix[k][j] = loop[loopIndex++];//将该层的左面边部分加入loop[]中
        }
    }
};

PS:一个惨痛教训:数学里习惯用4n、4i、4(i-1)等省略乘号的形式书写,C++编译器里不允许这种写法,必须加上*,如4*i。这个报错查了好久才发现QWQ。

三、官方解法

方法一:使用辅助数组

我们以题目中的示例二

\begin{bmatrix} 5 & 1 & 9 & 11 \\ 2 & 4 & 8 & 10 \\ 13 & 3 & 6 & 7 \\ 15 & 14 & 12 & 16 \end{bmatrix}

作为例子,分析将图像旋转 90 度之后,这些数字出现在什么位置。

对于矩阵中的第一行而言,在旋转后,它出现在倒数第一列的位置:

\begin{bmatrix} 5 &1 &9 &11 \\ & & & \\ & & & \\ & & & \end{bmatrix}\overset{AfterRotation }{\rightarrow}\begin{bmatrix} & & &5 \\ & & & 1\\ & & &9 \\ & & & 11 \end{bmatrix}

并且,第一行的第 x 个元素在旋转后恰好是倒数第一列的第 x 个元素。

对于矩阵中的第二行而言,在旋转后,它出现在倒数第二列的位置:

\begin{bmatrix} & & & \\ 2&4 & 8&10 \\ & & & \\ & & & \end{bmatrix}\overset{AfterRotation }{\rightarrow}\begin{bmatrix} & & 2 & \\ & & 4 & \\ & & 8 & \\ & & 10& \end{bmatrix}

对于矩阵中的第三行和第四行同理。这样我们可以得到规律:

对于矩阵中 第 i 行 第 j 个元素,在旋转后,它出现在 倒数第 i 列 第 j 个位置。

我们将其翻译成代码。由于矩阵中的行列从 0 开始计数,因此对于矩阵中的元素 matrix[row][col],在旋转后,它的新位置为 matrix_{new} [col][n−row−1]。这样以来,我们使用一个与 matrix 大小相同的辅助数组 matrix_{new} ,临时存储旋转后的结果。我们遍历 matrix 中的每一个元素,根据上述规则将该元素存放到 matrix_{new}​中对应的位置。在遍历完成之后,再将 matrix_{new} 中的结果复制到原数组中即可。

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        // C++ 这里的 = 拷贝是值拷贝,会得到一个新的数组
        auto matrix_new = matrix;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                matrix_new[j][n - i - 1] = matrix[i][j];
            }
        }
        // 这里也是值拷贝
        matrix = matrix_new;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/rotate-image/solution/xuan-zhuan-tu-xiang-by-leetcode-solution-vu3m/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

  • 时间复杂度:O(N^2),其中 N 是 matrix_{new} 的边长。
  • 空间复杂度:O(N^2)。我们需要使用一个和 matrix_{new} 大小相同的辅助数组。

方法二:原地旋转

题目中要求我们尝试在不使用额外内存空间的情况下进行矩阵的旋转,也就是说,我们需要「原地旋转」这个矩阵。那么我们如何在方法一的基础上完成原地旋转呢?

我们观察方法一中的关键等式:

matrix_{new}[col][n-1-row] = matrix[row][col]

它阻止了我们进行原地旋转,这是因为如果我们直接将matrix[row][col]放到原矩阵中的目标位置matrix[col][n-1-row]

matrix[col][n-1-row] = matrix[row][col]

原矩阵中的matrix[col][n-1-row]就被覆盖了!这并不是我们想要的结果。因此我们可以考虑用一个临时变量temp暂存matrix[col][n-1-row]的值,这样虽然matrix[col][n-1-row]被覆盖了,我们还是可以通过 temp 获取它原来的值:

\begin{cases} & \text{}temp = matrix[col][n-1-row] \\ & \text{}matrix[col][n-1-row] = matrix[row][col] \end{cases}

那么matrix[col][n-1-row]中的元素经过旋转操作之后会到哪个位置呢?我们还是使用方法一中的关键等式,不过这次,我们需要将

\begin{cases} & \text{ }row=col \\ & \text{ }col=n-1-row \end{cases}

带入关键等式,就可以得到:

matrix[n-1-row][n-1-col] = matrix[col][n-1-row]

同样地,直接赋值会覆盖掉matrix[n-1-row][n-1-col]原来的值,因此我们还是需要使用一个临时变量进行存储,不过这次,我们可以直接使用之前的临时变量temp

\begin{cases} & \text{ }temp = matrix[n-1-row][n-1-col] \\ & \text{ }matrix[n-1-row][n-1-col]=matrix[col][n-1-row]\\ & \text{ }matrix[col][n-1-row]=matrix[row][col] \end{cases}

我们再重复一次之前的操作,matrix[n-1-row][n-1-col]经过旋转操作之后会到哪个位置呢?再将

\begin{cases} & \text{ } row = n-1-row \\ & \text{ } col=n-1-col \end{cases}

带入关键等式,就可以得到:

matrix[n-1-col][row]=matrix[n-1-row][n-1-col]

写进去:

\begin{cases} & \text{ } temp=matrix[n-1-col][row] \\ & \text{ } matrix[n-1-col][row]=matrix[n-1-row][n-1-col] \\ & \text{ } matrix[n-1-row][n-1-col]=matrix[col][n-1-row] \\ & \text{ } matrix[col][n-1-row]=matrix[row][col] \end{cases}

不要灰心,再来一次!matrix[n-1-col][row]经过旋转操作之后回到哪个位置呢?

\begin{cases} & \text{ } row=n-1-col \\ & \text{ } col = row \end{cases}

带入关键等式,就可以得到:

matrix[row][col]=matrix[n-1-col][row]

我们回到了最初的起点matrix[row][col],也就是说:

\begin{cases} & \text{ } matrix[row][col] \\ & \text{ } matrix[col][n-1-row] \\ & \text{ } matrix[n-1-row][n-1-col] \\ & \text{ } matrix[n-1-col][row] \end{cases}

这四项处于一个循环中,并且每一项旋转后的位置就是下一项所在的位置!因此我们可以使用一个临时变量 temp 完成这四项的原地交换

\begin{cases} & \text{ } temp = matrix[row][col] \\ & \text{ } matrix[row][col]=matrix[n-1-col][row]\\ & \text{ } matrix[n-1-col][row]=matrix[n-1-row][n-1-col]\\ & \text{ } matrix[n-1-row][n-1-col] = matrix[col][n-1-row] \\ & \text{ } matrix[col][n-1-row] = temp \end{cases}

当我们知道了如何原地旋转矩阵之后,还有一个重要的问题在于:我们应该枚举哪些位置 (row,col) 进行上述的原地交换操作呢?由于每一次原地交换四个位置,因此:

  •  当 n 为偶数时,我们需要枚举 \frac{n^{2}}{4}=\frac{n}{2}\cdot \frac{n}{2} 个位置,可以将该图形分为四块,以 4×4 的矩阵为例:

保证了不重复、不遗漏;

  • n 为奇数时,由于中心的位置经过旋转后位置不变,我们需要枚举\frac{(n^2-1)}{4}=\frac{n+1}{2}\cdot \frac{n-1}{2}个位置,需要换一种划分的方式,以 5×5 的矩阵为例:

 ​ 

同样保证了不重复、不遗漏,矩阵正中央的点无需旋转。

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        for (int i = 0; i < n / 2; ++i) {
            for (int j = 0; j < (n + 1) / 2; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - j - 1][i];
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
                matrix[j][n - i - 1] = temp;
            }
        }
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/rotate-image/solution/xuan-zhuan-tu-xiang-by-leetcode-solution-vu3m/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

  • 时间复杂度:O(N²),其中 N 是 matrix 的边长。我们需要枚举的子矩阵大小为 ⌊n/2⌋×⌊(n+1)/2⌋)=O(N²)。
  • 空间复杂度:O(1)。为原地旋转。

方法三:用翻转代替旋转

我们还可以另辟蹊径,用翻转操作代替旋转操作。我们还是以题目中的示例二

\begin{bmatrix} 5 & 1 & 9 & 11 \\ 2 & 4 & 8 & 10 \\ 13 & 3 & 6 & 7 \\ 15 & 14 & 12 & 16 \end{bmatrix}

作为例子,先将其通过水平轴翻转得到:

 \begin{bmatrix} 5 & 1 & 9 & 11 \\ 2 & 4 & 8 & 10 \\ 13 & 3 & 6 & 7 \\ 15 & 14 & 12 & 16 \end{bmatrix}\xrightarrow[]{horizontally\, flip}\begin{bmatrix} 15 & 14& 12 &16 \\ 13& 3 & 6&7 \\ 2&4 & 8 &10 \\ 5&1 & 9 & 11 \end{bmatrix}

再根据主对角线翻转得到:

\begin{bmatrix} 15 & 14& 12 &16 \\ 13& 3 & 6&7 \\ 2&4 & 8 &10 \\ 5&1 & 9 & 11 \end{bmatrix}\xrightarrow[]{main\, diagonally\, flip}\begin{bmatrix} 15 & 13& 2&5 \\ 14 & 3 & 4 & 1\\ 12 & 6 & 8 &9 \\ 16&7 &10 & 11 \end{bmatrix}

就得到了答案。这是为什么呢?对于水平轴翻转而言,我们只需要枚举矩阵上半部分的元素,和下半部分的元素进行交换,即

matrix[row][col]\xrightarrow[]{horizontally \,flip}matrix[n-1-row][col]

对于主对角线翻转而言,我们只需要枚举对角线左侧的元素,和右侧的元素进行交换,即

matrix[n-1-row][col]\xrightarrow[]{main\,diagonally\,flip}matrix[col][n-1-row]

和方法一、方法二中的关键等式:

matrix_{new}[col][n-1-row] = matrix[row][col]

 是一致的。

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        // 水平翻转
        for (int i = 0; i < n / 2; ++i) {
            for (int j = 0; j < n; ++j) {
                swap(matrix[i][j], matrix[n - i - 1][j]);
            }
        }
        // 主对角线翻转
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                swap(matrix[i][j], matrix[j][i]);
            }
        }
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/rotate-image/solution/xuan-zhuan-tu-xiang-by-leetcode-solution-vu3m/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

复杂度分析

  • 时间复杂度:O(N²),其中 N 是 matrix 的边长。对于每一次翻转操作,我们都需要枚举矩阵中一半的元素。
  • 空间复杂度:O(1)。为原地翻转得到的原地旋转。

四、学习心得

1.环状替换法

本题与Day3可以放到一起来看。Day3是旋转数组,本题相当于是旋转多个数组(矩阵由外至内的一层层),所以都能用环状替换法,只需O(N)的时间和O(1)的额外空间即可实现一个数组的旋转。

一个思路两道题结合起来总结,还可以大大加深印象!

但是本题的难点在于,对于矩阵的每一层,把矩阵层中的元素for循环放入数组loop[]时数组的下标(确定for循环终止条件时到底是i+j还是i+j-1还是i+j-2……稍有粗心便会数组溢出o(╥﹏╥)o),以及对数组loop[]进行环状替换时数组下标和替换公式。(同理)

2.原地替换/多元素之间的交换swap

①找出每轮变换各元素间的循环关系(官解方法二为每轮4项元素处于一个循环中)

②使用一个临时变量temp来作为循环中元素之间的桥梁。

③先temp保存循环关系中最先被交换的元素,然后依次赋值,最后temp赋给循环中最后交换出的元素。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值