[二维数组] 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不同,本题因为元素的值域限制在固定区间里,所以这个方法是可行的。
思路:特殊值标记法
状态转化的规则总结起来两句话:
- 如果细胞是活的,周围活细胞数量x<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;
}
}