[二维数组] 73. 矩阵置零(HashSet记录坐标 → 第0行第0列代替Set) 289. 生命游戏(特殊值标记法)

73. 矩阵置零

题目链接:https://leetcode-cn.com/problems/set-matrix-zeroes/


分类:

  • 二维数组:
    • 开辟同样大小的二维数组记录0元素 → 开辟两个set存放0元素的x,y坐标(空间O(MN) → 空间O(M+N));
    • 特殊值标记法:把需要置0的位置填充一个值域之外的数字(空间O(1),但较费时);
    • 借用第0行第0列代替两个set,注意第0行第0列重叠部分的处理(空间O(1));

在这里插入图片描述

题目分析

遇到每一个0元素,就遍历它所在的行和列,将行、列上的所有元素都置零。

考点:只有初始值为0的元素才需要对它所在的行列置0,后面才置0的元素不能再对其所在的行列置0,所以问题在于:如何区分初始即为0的元素和后面置0的元素

类似题目:289. 生命游戏

思路1:两个Set存放初始0元素的横纵坐标

这题的考点就在于区分出初始值为0的元素和后来置0的元素,初始即为0的元素需要对它所在的行列置0,后面才置0的元素就不需要做这样的操作。

我们可以开辟两个set,先遍历一次二维数组,记录初始0元素的横纵坐标。然后再重新遍历一次二维数组,遇到0元素就先判断该元素的坐标是否存在于set中:

  • 如果存在,说明该0元素是初始0元素,将其所在行、列元素都置0
  • 如果不存在,说明该0元素是后来置0的元素,直接跳过。

实现代码:

class Solution {
    public void setZeroes(int[][] matrix) {
        Set<Integer> xSet = new HashSet<>();
        Set<Integer> ySet = new HashSet<>();
        int rows = matrix.length, cols = matrix[0].length;
        //遍历矩阵,记录初始0元素的坐标
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                if(matrix[i][j] == 0){
                    xSet.add(i);//set内部自动去重
                    ySet.add(j);
                }
            }
        }
        //重新遍历矩阵,将初始0元素所在的行、列置0
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                if(matrix[i][j] == 0){
                    //横纵坐标同时存在于set中才说明遇到初始0元素
                    if(xSet.contains(i) && ySet.contains(j)){
                        //将它所在的列置0
                        for(int k = 0; k < rows; k++) matrix[k][j] = 0;
                        //将它所在的行置0
                        for(int k = 0; k < cols; k++) matrix[i][k] = 0;
                    }
                }
            }
        }
    }
}
  • 时间复杂度:最差情况下,初始时矩阵所有元素都是0元素,遍历每个0元素需要O(MN),对每个0元素还都需要遍历它所在的行和列的所有元素,需要O(M+N),所以整体时间复杂度为O(M*N*(M+N));
  • 空间复杂度:O(M+N),一个存放横坐标,一个存放纵坐标,最差情况下每一行,每一列都有初始0元素,两个set各需要开辟M和N的空间。

思路2:特殊值标记法(推荐,空间O(1),时间O(M*N*(M+N)))

区分初始0元素和后来置0的元素其实不需要辅助空间,可以直接在遍历过程中修改原二维数组:

遍历一次矩阵,寻找所有初始0元素,对于每个初始0元素,都将它所在的行、列上的所有位置都填充一个值域范围之外的特殊值,用于标记“该位置需要置0”,同时与初始0相区分。

当所有初始0元素都处理完后,再重新遍历一次矩阵,将元素值 = 特殊标记值 的位置全部置0。

实现代码:

class Solution {
    int SPECIAL = Integer.MIN_VALUE;//特殊填充值
    public void setZeroes(int[][] matrix) {
        int rows = matrix.length, cols = matrix[0].length;
        //将所有需要置0的位置填充特殊值
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                if(matrix[i][j] == 0){
                    //将它所在的行,列全部置为特殊值(需要跳过其他初始0元素)
                    for(int k = 0; k < rows; k++){
                        if(k != i && matrix[k][j] != 0) matrix[k][j] = SPECIAL;
                    }
                    for(int k = 0; k < cols; k++){
                        if(k != j && matrix[i][k] != 0) matrix[i][k] = SPECIAL;
                    }
                }
            }
        }
        //将填充特殊值的位全部置为0
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                if(matrix[i][j] == SPECIAL){
                    matrix[i][j] = 0;
                }
            }
        }    
    }
}
  • 存在的问题:特殊值的取值必须确保在值域范围之外,否则可能出现元素初始值 = 特殊值而被误认为需要置0的情况。

    本题的矩阵元素是int型,但取值没有限制,所以实际上不存在这样严格有效的特殊值,这个思路存在缺陷。(实现代码里选择的是Integer.MIN_VALUE,但用例里出现了元素值为该值的情况,所以无法通过)

  • 时间复杂度:和思路1一样,整体时间复杂度为O(M*N*(M+N)),存在对同一个位置重复置0的情况,思路3有对这一方面的优化。

  • 空间复杂度:直接在原矩阵上操作,所以空间复杂度为O(1)。

思路3:借用第0行和第0列代替两个set(推荐,空间O(1), 时间O(MN))

遍历matrix,如果matrix[i][j]=0:

  • 将该元素所在的行的第0列matrix[i][0]置0,表示matrix[i][0]所在的第i行要全部置0;
  • 将该元素所在的列的第0行matrix[0][j]置0,表示matrix[0][j]所在的第j列要全部置0;

相当于借助第0行和第0列标记出(i,j)是0,例如:
在这里插入图片描述

但是,第0列和第0行有重叠的部分matrix[0][0],如果matrix[0][0]置为0,到底是表示第0行要全部置0,还是第0列要全部置0?

这里我们拿matrix[0][0]标记第0行的情况,再额外创建一个变量col标记第0列是否存在0元素。

所以在遍历第0列时,如果遇到初始0元素,就将col置1,其他不需要处理。

在处理完一遍matrix后,再分别遍历一次matrix的第0行和第0列,分别将第0行上0元素对应的列全部置0(除了第0列,因为第0列由col变量记录),将第0列上0元素所在的行全部置0(包括第0行)。

最后再检验col,如果col=1表示第0列也有0元素,将第0列置0。

实现代码:

class Solution {
    public void setZeroes(int[][] matrix) {
        int rows = matrix.length, cols = matrix[0].length;
        int col = 0;//记录第0列是否需要置0
        //遍历矩阵的0元素,将其对应的matrix[i][0],matrix[0][j]置0
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                if(matrix[i][j] == 0){
                    //如果0元素位于第0列,则将col置1,不需要其他操作
                    if(j == 0) col = 1;
                    else{
                        matrix[0][j] = 0;
                        matrix[i][0] = 0;
                    }
                }
            }
        }  
        //检查矩阵的第0行
        for(int j = 1; j < cols; j++){//跳过第0列(第0列的情况记录在col变量上)
            //如果第0行存在0元素,就将整列都置0
            if(matrix[0][j] == 0){
                for(int i = 0; i < rows; i++) matrix[i][j] = 0;
            }
        }
        //检查矩阵的第0列
        for(int i = 0; i < rows; i++){//第0行要记得处理
            //如果第0列存在0元素,就将整行都置0
            if(matrix[i][0] == 0){
                for(int j = 0; j < cols; j++) matrix[i][j] = 0;
            }
        }
        //检查col变量:如果col=1,就将第0列全部置0
        if(col == 1){
            for(int i = 0; i < rows; i++) matrix[i][0] = 0;
        }
    }
}
  • 时间复杂度:和思路2相比,每找到一个初始0元素,并没有立即把所在行、列所有元素都处理,而是记录在第0行、第0列处,所以遍历矩阵寻找初始0元素的过程只用了O(M*N),最后检查第0行将对应列置0,检查第0列将对应行置0,检查col将第0列置0,都不会超过O(M*N),所以整体时间复杂度为O(M*N)
  • 空间复杂度:O(1)

289. 生命游戏

题目链接:https://leetcode-cn.com/problems/game-of-life/


分类:

  • 二维数组(特殊值标记法:0表示死细胞,1表示活细胞,-1表示原本是活,新状态是死的细胞,2表示原本是死,新状态是活的细胞)

在这里插入图片描述

题目分析

这题和73题类似,但其实更简单,只是状态变化的规则比较复杂,但对于空间的优化措施更易想到,因为矩阵里的元素只有0,1两种,所以可以用其他数值来表示其他状态,和原始状态相区分。

  • 和73题的思路2不同,本题因为元素的值域限制在固定区间里,所以这个方法是可行的。

思路:特殊值标记法

状态转化的规则总结起来两句话:

  1. 如果细胞是活的,周围活细胞数量x<2或>3,则死亡;
  2. 如果细胞是死的,周围活细胞==3,则复活。
  3. 其他情况保持原状。

我们需要将原状态和新状态相区分,避免新旧状态之间互相影响,所以需要设置一个特殊值来表示新状态:

  • 2来表示原本是死的,新状态是活的。
  • -1来表示原本是活的,新状态是死的。

基于这样的设置,对于每个元素,先判断它的旧状态,再使用对应的规则进行状态转化:

  • 如果元素绝对值=1,说明旧状态是活的;
  • 如果元素绝对值!= 1,说明旧状态是死的;

接着调用liveNum()函数检查它周围的8个元素,统计活细胞的个数,因为旧状态和新状态都存放在board上,而状态转化时只参考周围元素的旧状态,所以在统计活细胞个数时,对于矩阵上处于旧状态的元素可以直接处理,而对于处于新状态的元素,则判断:如果元素的绝对值 == 1,说明旧状态是活的;如果元素绝对值 != 1,说明旧状态是死的。

根据上述规则处理完整个矩阵后,再对矩阵做一次遍历,将所有大于0的元素都置为1,所以小于0的元素都置为0,保存每个细胞的最终状态。

实现代码:

class Solution {
    public void gameOfLife(int[][] board) {
        for(int i = 0; i < board.length; i++){
            for(int j = 0; j < board[i].length; j++){
                //细胞的旧状态是活的
                if(board[i][j] == 1){
                    //统计周围8个元素的活细胞个数
                    int count = liveNum(board, i, j);
                    if(count < 2 || count > 3) board[i][j] = -1;//细胞从活->死,置-1 
                }
                else if(board[i][j] == 0){
                    int count = liveNum(board, i, j);
                    if(count == 3) board[i][j] = 2;//细胞从死->活,置2
                }
            }
        }
        //将新状态统一为0,1
        for(int i = 0; i < board.length; i++){
            for(int j = 0; j < board[i].length; j++){
                if(board[i][j] > 0) board[i][j] = 1;
                else board[i][j] = 0;
            }
        }
    }
    //统计周围8个元素的活细胞个数
    public int liveNum(int[][] board, int row, int col){
        int count = 0;
        for(int i = -1; i <= 1; i++){
            //行下标的越界判断
            if(row + i < 0 || row + i >= board.length) continue;
            for(int j = -1; j <= 1; j++){
                //列下标的越界判断
                if(col + j < 0 || col + j >= board[0].length) continue;
                //跳过细胞本身
                if(i == 0 && j == 0) continue;
                if(Math.abs(board[row + i][col + j]) == 1) count++;
            }
        }
        return count;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值