面试常见矩阵问题📦

2,627 阅读7分钟

36. 有效的数独

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

 

注意:

  • 一个有效的数独(部分已被填充)不一定是可解的。
  • 只需要根据以上规则,验证已经填入的数字是否有效即可。
  • 空白格用 '.' 表示。

 

示例 1:

输入: board = 
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出: true
class Solution:
    def isValidSudoku(self, board):
        for i in range(9):
            col = [False] * 10
            for j in range(9):
                if board[i][j].isdigit():
                    index = int(board[i][j])
                    if col[index]:
                        return False
                    col[index] = True

        for j in range(9):
            row = [False] * 10
            for i in range(9):
                if board[i][j].isdigit():
                    index = int(board[i][j])
                    if row[index]:
                        return False
                    row[index] = True

        for i in range(0, 9, 3):
            for j in range(0, 9, 3):
                square = [False] * 10
                for x in range(i,i+3):
                    for y in range(j,j+3):
                        if board[x][y].isdigit():
                            index = int(board[x][y])
                            if square[index]:
                                return False
                            square[index] = True
        return True

这是一种通过三个嵌套的循环来判断数独是否有效的方法,时间复杂度为 O(n2)O(n^2),其中n=9。具体来说,对于每一行、每一列以及每个3x3的子矩阵,我们都用一个哈希表来记录数字是否出现过,如果出现重复,就说明数独无效。如果所有的行、列和子矩阵都没有出现重复的数字,就说明数独有效。

这个方法比较直观易懂,但是时间复杂度较高。如果面试官要求优化时间复杂度,我们可以考虑使用其他方法,比如位运算。

class Solution:
    def isValidSudoku(self, board):
        row = [0] * 9
        col = [0] * 9
        box = [0] * 9
        for i in range(9):
            for j in range(9):
                if board[i][j] != '.':
                    num = int(board[i][j]) - 1
                    k = i // 3 * 3 + j // 3  # 计算子矩阵编号
                    if row[i] & (1 << num) or col[j] & (1 << num) or box[k] & (1 << num):
                        return False
                    row[i] |= (1 << num)  # 标记第i行数字num出现过
                    col[j] |= (1 << num)  # 标记第j列数字num出现过
                    box[k] |= (1 << num)  # 标记第k个子矩阵数字num出现过
        return True

其中,变量row[i]表示第i行中数字的出现情况,如果数字j出现过,就将row[i]的第j位标记为1。同理,变量col[j]表示第j列中数字的出现情况,变量box[k]表示第k个子矩阵中数字的出现情况。在遍历整个数独的过程中,如果某个数字已经出现过,就返回False,否则就将对应的行、列、子矩阵的标记更新。如果整个数独遍历完成后没有出现重复的数字,就返回True。


73. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法

 

示例 1:

输入: matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出: [[1,0,1],[0,0,0],[1,0,1]]

示例 2:

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

 

提示:

  • m == matrix.length
  • n == matrix[0].length
  • 1 <= m, n <= 200
  • -231 <= matrix[i][j] <= 231 - 1

 

进阶:

  • 一个直观的解决方案是使用  O(m n) 的额外空间,但这并不是一个好的解决方案。
  • 一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
  • 你能想出一个仅使用常量空间的解决方案吗?
class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        index = []
        for i in range(len(matrix)):
            for j in range(len(matrix[0])):
                if matrix[i][j] == 0:
                    index.append([i, j])

        for i, j in index:
            for x in range(len(matrix)):
                matrix[x][j] = 0
            for y in range(len(matrix[0])):
                matrix[i][y] = 0

这个代码的思路是先遍历整个矩阵,找到值为0的元素的坐标,然后再遍历一遍矩阵,将对应的行和列都置为0。

这个算法的时间复杂度是 O(mn(m+n))O(mn(m+n)),其中 mmnn 分别是矩阵的行数和列数。因为需要遍历整个矩阵两次,而每次遍历需要将一整行和一整列都置为0,所以时间复杂度是 O(mn(m+n))O(mn(m+n))

下面是一个优化后的算法,时间复杂度为 O(mn)O(mn),空间复杂度为 O(m+n)O(m+n)

具体思路是,用两个数组记录哪些行和哪些列需要被置为0。首先遍历一遍矩阵,记录哪些行和哪些列需要被置为0,然后再遍历一遍矩阵,将对应的行和列都置为0。这样只需要遍历矩阵两次,所以时间复杂度是 O(mn)O(mn),空间复杂度是 O(m+n)O(m+n)

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        row = set()
        col = set()
        for i in range(len(matrix)):
            for j in range(len(matrix[0])):
                if matrix[i][j] == 0:
                    row.add(i)
                    col.add(j)
        for i in row:
            for j in range(len(matrix[0])):
                matrix[i][j] = 0
        for j in col:
            for i in range(len(matrix)):
                matrix[i][j] = 0

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

 

提示:

  • n == matrix.length == matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000
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):
            for j in range(i,n):
                matrix[i][j] , matrix[j][i] = matrix[j][i], matrix[i][j]
        # 每行都逆置
        for i in range(n):
            matrix[i].reverse()

这个解法的思路是将矩阵先进行转置操作,然后再将每一行逆置。这样就能够实现将矩阵顺时针旋转90度的效果。

具体来说,转置操作的实现是通过两个循环遍历矩阵,对于每个位置 (i, j),将它和 (j, i) 位置上的数值进行交换。这个操作会将矩阵沿着对角线翻转。

然后再对每一行进行逆置操作,即将每一行的元素顺序颠倒过来。

需要注意的是,这个解法是原地修改矩阵的,不需要使用额外的空间。时间复杂度为 O(n2)O(n^2),空间复杂度为 O(1)O(1)


54. 螺旋矩阵

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

 

示例 1:

输入: matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出: [1,2,3,6,9,8,7,4,5]

示例 2:

输入: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]

 

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100
class Solution:
    def spiralOrder(self, matrix):
        m, n = len(matrix), len(matrix[0])
        res = []
        l, r, t, b = 0, n - 1, 0, m - 1

        while True:
            for i in range(l, r + 1):
                res.append(matrix[t][i])
            t += 1
            if t > b:
                break
            for i in range(t, b + 1):
                res.append(matrix[i][r])
            r -= 1
            if l > r:
                break
            for i in range(r, l - 1, -1):
                res.append(matrix[b][i])
            b -= 1
            if t > b:
                break
            for i in range(b, t - 1, -1):
                res.append(matrix[i][l])
            l += 1
            if l > r:
                break

        return res

采用“模拟”思想,从左到右、从上到下、从右到左、从下到上四个方向循环遍历。每次遍历完一个方向,需要更新边界。当上下边界相遇或左右边界相遇时,说明遍历完成。

在代码实现中,用四个变量分别表示左、右、上、下边界,通过循环控制遍历方向和边界更新。

时间复杂度为 O(mn)O(mn),空间复杂度为 O(1)O(1)


289. 生命游戏

生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

  1. 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
  2. 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
  3. 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
  4. 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;

下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。

 

示例 1:

输入: board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
输出: [[0,0,0],[1,0,1],[0,1,1],[0,1,0]]

示例 2:

输入: board = [[1,1],[1,0]]
输出: [[1,1],[1,1]]

 

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 25
  • board[i][j] 为 0 或 1

 

进阶:

  • 你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。
  • 本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题?

解题思路:

根据题目所述规则来进行实现即可。在更新时为了避免更新后的值影响后续更新,所以需要借助另一个矩阵来存储更新后的值。

具体实现步骤如下:

  • 遍历原始矩阵,对于每个元素:
  • 统计其周围八个邻居中的存活元素个数;
  • 根据规则更新该元素在新矩阵中的状态。
  • 将新矩阵中的值复制到原始矩阵中。

时间复杂度:O(mn)O(mn),其中 mmnn 分别为矩阵的行数和列数。遍历矩阵的所有元素需要 O(mn)O(mn) 的时间。

空间复杂度:O(mn)O(mn),其中 mmnn 分别为矩阵的行数和列数。需要额外使用一个 m×nm\times n 的矩阵来存储更新后的值。

class Solution:
    def gameOfLife(self, board):
        """
        Do not return anything, modify board in-place instead.
        """
        m, n = len(board), len(board[0])
        matrix = [[0] * n for _ in range(m)]


        def alive(i, j):
            count = 0
            for x in range(max(0, i - 1), min(i + 2, m)):
                for y in range(max(0, j - 1), min(j + 2, n)):
                    if x == i and y == j:
                        continue
                    if board[x][y] == 1:
                        count += 1
            return count

        for i in range(m):
            for j in range(n):
                c = alive(i, j)
                if board[i][j] == 1:
                    if c == 3 or c == 2:
                        matrix[i][j] = 1
                else:
                    if c == 3:
                        matrix[i][j] = 1
        for i in range(m):
            for j in range(n):
                board[i][j] = matrix[i][j]

image.png

写完之后回答一下这俩问题:

  • 借助辅助数组
  • 借助辅助函数

这个算法首先定义了一个alive(i, j)函数,用于计算第i行、第j列的格子周围有多少个“活”的格子。接着,算法遍历所有的格子,并根据它周围的“活”格子数量更新这个格子的状态。更新后的状态存储在matrix中。

最后,算法将matrix中的状态拷贝回board,并实现了题目要求的原地修改。

需要注意的是,在创建matrix时,需要使用列表推导式生成每一行的列表,以便在后续修改matrix的值时,不会影响到其他行的值。

可以考虑在原地修改数组,而不使用额外的矩阵。因为题目中说“请使用 原地 执行此次操作,即不要使用复制数组的方式”,所以这种方法更符合题目要求。

可以使用 0 表示死细胞,1 表示活细胞,2 表示原来是死细胞,但是现在是活细胞,-1 表示原来是活细胞,但是现在是死细胞。

具体来说,我们可以遍历整个数组,对于每一个位置,分别计算周围八个位置中活细胞的数量。然后根据题目要求修改原数组:

  • 如果这个位置上原来是死细胞,而且周围有 3 个活细胞,那么这个位置上就会变成活细胞,标记为 2。
  • 如果这个位置上原来是活细胞,而且周围有 2 或 3 个活细胞,那么这个位置上还是活细胞,不需要改变标记。
  • 如果这个位置上原来是活细胞,但是周围活细胞的数量不是 2 或 3,那么这个位置上就会变成死细胞,标记为 -1。
  • 如果这个位置上原来是死细胞,而且周围活细胞的数量不是 3,那么这个位置上还是死细胞,不需要改变标记。

遍历完成后,再遍历一遍数组,把 2 标记为 1,把 -1 标记为 0。

class Solution:
    def gameOfLife(self, board: List[List[int]]) -> None:
        m, n = len(board), len(board[0])
        
        def alive(i, j):
            count = 0
            for x in range(max(0, i - 1), min(i + 2, m)):
                for y in range(max(0, j - 1), min(j + 2, n)):
                    if x == i and y == j:
                        continue
                    if board[x][y] == 1 or board[x][y] == -1:
                        count += 1
            return count
        
        for i in range(m):
            for j in range(n):
                count = alive(i, j)
                if board[i][j] == 1:
                    if count < 2 or count > 3:
                        board[i][j] = -1
                else:
                    if count == 3:
                        board[i][j] = 2
        
        # 将 -1 和 2 转换成 0 和 1
        for i in range(m):
            for j in range(n):
                if board[i][j] == -1:
                    board[i][j] = 0
                elif board[i][j] == 2:
                    board[i][j] = 1