数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵

1 二维数组遍历

题库列表

  • 48. 旋转图像

  • 54. 螺旋矩阵

  • 59. 螺旋矩阵Ⅱ

1.1 旋转图像

1.1.1 题目描述

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第1张图片

        题目链接:https://leetcode.cn/problems/rotate-image/

1.1.2 辅助矩阵

        如下图所示,矩阵顺时针旋转 9 0 ∘ 90^{\circ} 90 后,可找到以下规律:

  • 「第 i i i 行」元素旋转到「第 n − 1 − i n−1−i n1i 列」元素;
  • 「第 j j j 列」元素旋转到「第 j j j 行」元素;

        因此,对于矩阵任意第 i i i 行、第 j j j 列元素 m a t r i x [ i ] [ j ] matrix[i][j] matrix[i][j],矩阵旋转 9 0 ∘ 90^{\circ} 90 后「元素位置旋转公式」为:

m a t r i x [ i ] [ j ] → m a t r i x [ j ] [ n − 1 − i ] 原索引位置 → 旋转后索引位置 \begin{aligned} matrix[i][j] & \rightarrow matrix[j][n - 1 - i] \\ 原索引位置 & \rightarrow 旋转后索引位置 \end{aligned} matrix[i][j]原索引位置matrix[j][n1i]旋转后索引位置

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第2张图片

        根据以上「元素旋转公式」,考虑遍历矩阵,将各元素依次写入到旋转后的索引位置。但仍存在问题:在写入一个元素 m a t r i x [ i ] [ j ] → m a t r i x [ j ] [ n − 1 − i ] matrix[i][j] \rightarrow matrix[j][n - 1 - i] matrix[i][j]matrix[j][n1i] 后,原矩阵元素 m a t r i x [ j ] [ n − 1 − i ] matrix[j][n - 1 - i] matrix[j][n1i] 就会被覆盖(即丢失),而此丢失的元素就无法被写入到旋转后的索引位置了。

        为解决此问题,考虑借助一个「辅助矩阵」暂存原矩阵,通过遍历辅助矩阵所有元素,将各元素填入「原矩阵」旋转后的新索引位置即可。

class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        n = len(matrix)
        # 深拷贝 matrix -> tmp
        tmp = copy.deepcopy(matrix)
        # 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素
        for i in range(n):
            for j in range(n):
                matrix[j][n - 1 - i] = tmp[i][j]

        遍历矩阵所有元素的时间复杂度为 O ( N 2 ) O(N^2) O(N2);由于借助了一个辅助矩阵,空间复杂度为 O ( N 2 ) O(N^2) O(N2)

1.1.3 原地修改

        考虑不借助辅助矩阵,通过在原矩阵中直接「原地修改」,实现空间复杂度 O(1)O(1)O(1) 的解法。

        以位于矩阵四个角点的元素为例,设矩阵左上角元素 A A A、右上角元素 B B B、右下角元素 C C C、左下角元素 D D D。矩阵旋转 9 0 ∘ 90^{\circ} 90 后,相当于依次先后执行 D → A D \rightarrow A DA C → D C \rightarrow D CD B → C B \rightarrow C BC A → B A \rightarrow B AB 修改元素,即如下「首尾相接」的元素旋转操作:
A ← D ← C ← B ← A A \leftarrow D \leftarrow C \leftarrow B \leftarrow A ADCBA

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第3张图片

        如上图所示,由于第 1 1 1 D → A D \rightarrow A DA 已经将 A A A 覆盖(导致 A A A 丢失),此丢失导致最后第 4 4 4 A → B A \rightarrow B AB 无法赋值。为解决此问题,考虑借助一个「辅助变量 tmp」预先存储 A A A,此时的旋转操作变为:

暂存 t m p = A A ← D ← C ← B ← t m p 暂存 tmp = A \\ A \leftarrow D \leftarrow C \leftarrow B \leftarrow tmp 暂存tmp=AADCBtmp

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第4张图片

        如上图所示,一轮可以完成矩阵 4 个元素的旋转。因而,只要分别以矩阵左上角 1 4 \frac{1}{4} 41 的各元素为起始点执行以上旋转操作,即可完整实现矩阵旋转。

        具体来看,当矩阵大小 n n n 为偶数时,取前 n 2 \frac{n}{2} 2n 行、前 n 2 \frac{n}{2} 2n 列的元素为起始点;当矩阵大小 n n n 为奇数时,取前 n 2 \frac{n}{2} 2n 行、前 n + 1 2 \frac{n + 1}{2} 2n+1 列的元素为起始点。

        令 m a t r i x [ i ] [ j ] = A matrix[i][j]=A matrix[i][j]=A,根据文章开头的元素旋转公式,可推导得适用于任意起始点的元素旋转操作:

暂存 t m p = m a t r i x [ i ] [ j ] m a t r i x [ i ] [ j ] ← m a t r i x [ n − 1 − j ] [ i ] ← m a t r i x [ n − 1 − i ] [ n − 1 − j ] ← m a t r i x [ j ] [ n − 1 − i ] ← t m p 暂存 tmp = matrix[i][j] \\ matrix[i][j] \leftarrow matrix[n - 1 - j][i] \leftarrow matrix[n - 1 - i][n - 1 - j] \leftarrow matrix[j][n - 1 - i] \leftarrow tmp 暂存tmp=matrix[i][j]matrix[i][j]matrix[n1j][i]matrix[n1i][n1j]matrix[j][n1i]tmp

        如下图所示,为示例矩阵的算法执行流程。

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第5张图片
class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        # 设矩阵行列数为 n
        n = len(matrix)
        # 起始点范围为 0 <= i < n // 2 , 0 <= j < (n + 1) // 2
        # 其中 '//' 为整数除法
        for i in range(n // 2):
            for j in range((n + 1) // 2):
                # 暂存 A 至 tmp
                tmp = matrix[i][j]
                # 元素旋转操作 A <- D <- C <- B <- tmp
                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] = tmp

复杂度分析

  • 时间复杂度 O ( N 2 ) O(N^2) O(N2): 其中 N N N 为输入矩阵的行(列)数。需要将矩阵中每个元素旋转到新的位置,即对矩阵所有元素操作一次,使用 O ( N 2 ) O(N^2) O(N2) 时间。
  • 空间复杂度 O ( 1 ) O(1) O(1): 临时变量 t m p tmp tmp 使用常数大小的额外空间。值得注意,当循环中进入下轮迭代,上轮迭代初始化的 t m p tmp tmp 占用的内存就会被自动释放,因此无累计使用空间。

1.1.4 对角线反转,左右翻转

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第6张图片 数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第7张图片
数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第8张图片 数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第9张图片
class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        n = len(matrix)
        for i in range(n):
            # 注意这里j的范围 如果j的范围也是0到n-1那么会出现交换后又交换回来 等于没有交换
            for j in range(i):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
        
        for line in matrix:
            line.reverse()      # 左右翻转

1.1.5 上下反转,对角线反转

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第10张图片
class Solution:
    def rotate(self, matrix):
        matrix[:] = matrix[::-1]        # 上下反转
        for i in range(len(matrix)):
            for j in range(i+1, len(matrix)):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

1.2 螺旋矩阵

1.2.1 题目描述

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第11张图片

        题目链接:https://leetcode.cn/problems/spiral-matrix/

1.2.2 思路分析

1. 按照「形状」进行模拟
        解题的核心思路是按照右、下、左、上的顺序遍历数组,并使用四个变量圈定未遍历元素的边界:

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第12张图片

        随着螺旋遍历,相应的边界会收缩,直到螺旋遍历完整个数组:

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第13张图片
def spiralOrder(matrix: List[List[int]]) -> List[int]:
    upper_bound = 0
    lower_bound = len(matrix) - 1
    left_bound = 0
    right_bound = len(matrix[0]) - 1
    res = []
    # res.length == m * n 则遍历完整个数组
    while len(res) < m * n:
        if upper_bound <= lower_bound:
            # 在顶部从左向右遍历
            for j in range(left_bound, right_bound + 1):
                res.append(matrix[upper_bound][j])
            # 上边界下移
            upper_bound += 1
        
        if left_bound <= right_bound:
            # 在右侧从上向下遍历
            for i in range(upper_bound, lower_bound + 1):
                res.append(matrix[i][right_bound])
            # 右边界左移
            right_bound -= 1
        
        if upper_bound <= lower_bound:
            # 在底部从右向左遍历
            for j in range(right_bound, left_bound - 1, -1):
                res.append(matrix[lower_bound][j])
            # 下边界上移
            lower_bound -= 1
        
        if left_bound <= right_bound:
            # 在左侧从下向上遍历
            for i in range(lower_bound, upper_bound - 1, -1):
                res.append(matrix[i][left_bound])
            # 左边界右移
            left_bound += 1
    
    return res

2. 按照「方向」进行模拟

(1) 起始位置
        螺旋矩阵的遍历起点是矩阵的左上角,也就是 (0, 0) 位置。

(2) 移动方向
        起始位置的下一个移动方向是向右。在遍历的过程中,移动方向是固定的:
右 → ,下 ↓ ,左 ← ,上 ↑ 右→,下↓,左←,上↑ ,下,左,上

        移动方向是按照上面的顺序循环进行的。每次当移动到了边界,才会更改方向。但边界并不是固定的,请看下面分析。

(3) 边界
        本题的边界是最大的难点,因为是随着遍历的过程而变化的。螺旋遍历的时候,已经遍历的数字不能再次遍历,所以边界会越来越小。

        规则是:如果当前行(列)遍历结束之后,就需要把这一行(列)的边界向内移动一格。

        以下面的图为例,up, down, left, right 分别表示四个方向的边界,初始时分别指向矩阵的四个边界。如果我们把第一行遍历结束(遍历到了右边界),此时需要修改新的移动方向为向下、并且把上边界 up 下移一格,即从 旧 up 位置移动到 新 up 位置。

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第14张图片

        当绕了一圈后,从下向上走到 新 up 边界的时候,此时需要修改新的移动方向为向右、并且把左边界 left 下移一格,即从 旧 left 位置移动到 新 left 位置。

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第15张图片

        由此可见,根据维护的四个方向的边界,就知道什么时候更改移动方向了。

(4) 结束条件
        螺旋遍历的结束条件是所有的位置都被遍历到。

代码实现:

  • up, down, left, right 分别表示四个方向的边界。
  • x, y 表示当前位置。
  • dirs 分别表示移动方向是 右、下、左、上 。
  • cur_d 表示当前的移动方向的下标,dirs[cur_d] 就是下一个方向需要怎么修改 x, y。
  • cur_d == 0 and y == right 表示当前的移动方向是向右,并且到达了右边界,此时将移动方向更改为向下,并且上边界 up 向下移动一格。
  • 结束条件是结果数组 res 的元素个数能与 matrix 中的元素个数。
class Solution(object):
    def spiralOrder(self, matrix):
        if not matrix or not matrix[0]: return []
        M, N = len(matrix), len(matrix[0])
        left, right, up, down = 0, N - 1, 0, M - 1
        res = []
        x, y = 0, 0
        dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        cur_d = 0
        while len(res) != M * N:
            res.append(matrix[x][y])
            if cur_d == 0 and y == right:
                cur_d += 1
                up += 1
            elif cur_d == 1 and x == down:
                cur_d += 1
                right -= 1
            elif cur_d == 2 and y == left:
                cur_d += 1
                down -= 1
            elif cur_d == 3 and x == up:
                cur_d += 1
                left += 1
            cur_d %= 4
            x += dirs[cur_d][0]
            y += dirs[cur_d][1]
        return res

1.3 螺旋矩阵Ⅱ

1.3.1 题目描述

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第16张图片

        题目链接:https://leetcode.cn/problems/spiral-matrix-ii/

1.3.2 思路分析

1. 按照「形状」进行填充

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第17张图片
def generateMatrix(n: int) -> List[List[int]]:
    matrix = [[0 for _ in range(n)] for _ in range(n)]
    upper_bound, lower_bound = 0, n - 1
    left_bound, right_bound = 0, n - 1
    # 需要填入矩阵的数字
    num = 1
    
    while num <= n * n:
        if upper_bound <= lower_bound:
            # 在顶部从左向右遍历
            for j in range(left_bound, right_bound+1):
                matrix[upper_bound][j] = num
                num += 1
            # 上边界下移
            upper_bound += 1
        
        if left_bound <= right_bound:
            # 在右侧从上向下遍历
            for i in range(upper_bound, lower_bound+1):
                matrix[i][right_bound] = num
                num += 1
            # 右边界左移
            right_bound -= 1
        
        if upper_bound <= lower_bound:
            # 在底部从右向左遍历
            for j in range(right_bound, left_bound-1, -1):
                matrix[lower_bound][j] = num
                num += 1
            # 下边界上移
            lower_bound -= 1
        
        if left_bound <= right_bound:
            # 在左侧从下向上遍历
            for i in range(lower_bound, upper_bound-1, -1):
                matrix[i][left_bound] = num
                num += 1
            # 左边界右移
            left_bound += 1
    
    return matrix

2. 按照「方向」进行填充

(1) 四个变量标记边界

class Solution(object):
    def generateMatrix(self, n):
        if n == 0: return []
        res = [[0] * n for i in range(n)]
        left, right, up, down = 0, n - 1, 0, n - 1
        x, y = 0, 0
        dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        cur_d = 0
        count = 0
        while count != n * n:
            res[x][y] = count + 1
            count += 1
            if cur_d == 0 and y == right:
                cur_d += 1
                up += 1
            elif cur_d == 1 and x == down:
                cur_d += 1
                right -= 1
            elif cur_d == 2 and y == left:
                cur_d += 1
                down -= 1
            elif cur_d == 3 and x == up:
                cur_d += 1
                left += 1
            cur_d %= 4
            x += dirs[cur_d][0]
            y += dirs[cur_d][1]
        return res

(2) 使用非 0 数字标记边界

        我们在遍历的过程中,需要依次放入 1 − N 2 1-N^2 1N2 数字,如果我们把结果数组的所有位置初始化为 0,那么非 0 的位置就代表我们已经遍历过了,相当于边界。

        当遍历到数组的原始边界或者撞到了非 0 的数字,表示当前方向已经遍历到了边界,需要更改移动方向。这个做法的优点是省去了维护 4 个变量表示的边界。

数组(五)-- LC[48]&[54]&[59] 旋转矩阵与螺旋矩阵_第18张图片

        初始移动方向是向右,如果遇到了数组边界或者遇到了非 0 的数字,那么就要转动方向。转向的方法是 cur_d = (cur_d + 1) % 4,cur_d 表示了当前的方向是 directions 中的哪个,顺序依次是 右、下、左、上。

class Solution(object):
    def generateMatrix(self, n):
        directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        res = [[0] * n for i in range(n)]
        x, y = 0, 0
        count = 0
        cur_d = 0
        while count != n * n:
            res[x][y] = count + 1
            count += 1
            dx, dy = directions[cur_d][0], directions[cur_d][1]
            newx, newy = x + dx, y + dy
            if newx < 0 or newx >= n or newy < 0 or newy >= n or res[newx][newy] != 0:
                cur_d = (cur_d + 1) % 4
                dx, dy = directions[cur_d][0], directions[cur_d][1]
            x, y = x + dx, y + dy
        return res

参考

  • 旋转图像(辅助矩阵 / 原地修改,清晰图解):https://leetcode.cn/problems/rotate-image/solutions/1228078/48-xuan-zhuan-tu-xiang-fu-zhu-ju-zhen-yu-jobi/
  • 矩阵遍历问题的四部曲:https://leetcode.cn/problems/spiral-matrix-ii/solutions/659234/ju-zhen-bian-li-wen-ti-de-si-bu-qu-by-fu-sr5c/

你可能感兴趣的:(LC,PAT,矩阵,旋转矩阵,螺旋矩阵,二维数组遍历)