矩阵
有效的数独
请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
我的代码:
Java
class Solution {
public boolean isValidSudoku(char[][] board) {
// 就是采用朴素的遍历即可
// 但是可以通过构建多维数组的方式
// 使得遍历依次board即可
// rows[i][j] 记录第i行,数字j有没有出现过
boolean[][] rows = new boolean[9][9];
// cols[i][j] 记录第i列,数字j有没有出现过
boolean[][] cols = new boolean[9][9];
// subMatrix[i][j][k] 记录第i行第j个子矩阵中,数字k有没有出现过
boolean[][][] subMatrix = new boolean[3][3][9];
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
// 当前字符
char c = board[i][j];
if(c == '.'){
// 空白格直接跳过即可
continue;
}
// number才是当前的数字
int number = c - '1';
// 检查行
if(rows[i][number] == true){
return false;
}
rows[i][number] = true;
// 检查列
if(cols[j][number] == true){
return false;
}
cols[j][number] = true;
// 检查子矩阵
int subMatrixRow = i / 3;
int subMatrixCol = j / 3;
if(subMatrix[subMatrixRow][subMatrixCol][number] == true){
return false;
}
subMatrix[subMatrixRow][subMatrixCol][number] = true;
}
}
return true;
}
}
螺旋矩阵
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
看答案之后我的解答:
Java
class Solution {
private final int[][] direction = {{0,1},{1,0},{0,-1},{-1,0}};
public List<Integer> spiralOrder(int[][] matrix) {
// 借鉴0x3f的思路,进行朴素的实现
// 但是其中可学习的地方在于,使用方向数组使得代码一目了然,容易理解
List<Integer> result = new ArrayList<>();
// m行,n列
int m = matrix.length;
if(m == 0){
return result;
}
int n = matrix[0].length;
// 初始化遍历行列位置
int i = 0;
int j = 0;
// di为方向,最开始为右
int di = 0;
for(int k = 0; k < m * n; k++){
result.add(matrix[i][j]);
// 标记为遍历过了
matrix[i][j] = Integer.MAX_VALUE;
// 查看下一步位置
int nextI = i + direction[di][0];
int nextJ = j + direction[di][1];
// 如果越界或者访问到已经访问的元素,则换方向
if(nextI < 0 || nextI >= m || nextJ < 0 || nextJ >= n || matrix[nextI][nextJ] == Integer.MAX_VALUE){
// 换方向
di = (di+1)%4;
}
i += direction[di][0];
j += direction[di][1];
}
return result;
}
}
旋转图像
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
我的思路:
Java
class Solution {
public void rotate(int[][] matrix) {
// 这个其实是有规律的
// 对于n x n的矩阵,原来martix[0][0] 移到martix[0][n-1]的位置
// 即第1行第1列的变化为(0, n-1)
// 可知第1行第2列的变化为(1,n-2)
// 第1行第3列的变化为(2,n-3) 以此类推
// 所以我们知道了某一行的第一列变化值,就知道这一行后续的变化值
// 那么第2行的第1列如何呢
// 可以找到规律,第2行的第1列的变化为(-1,n-2)
// 第3行的第1列的变化为(-2,n-3) 以此类推
// 这样,我们就知道了整个变化的规律
// 一般的,从0开始的下标
// 第i行第0列的变化值为(-i,n-1-i);
// 从而第i行第j列的变化值为(-i+j,n-1-i-j);
// 从而第i行第j列应该到(j,n-1-i)
// 如此,我们就不断的变化,直到变化了n x n次即可。
int n = matrix.length;
if(n < 2){
return;
}
int[] change = {0, n-1};
int i = 0;
int j = 0;
for(int k = 0; k < n * n; k++){
int nextI = j;
int nextJ = n-1-i;
// 将要赋予的位置 的 原来的值
int nextValue = matrix[nextI][nextJ];
// 将当前的值 覆盖 将要旋转到的位置
matrix[nextI][nextJ] = preValue;
// 这里不知道怎么写了
}
}
}
推导出的公式是正确的
但在实现上,遇到了经典难题:"连锁覆盖"。
如果单纯地用循环去覆盖,原本的值会被冲掉,导致后续旋转出错。虽然可以用两个暂存值,但逻辑会变得非常复杂,且容易陷入死循环。
实际上在一个 n x n 矩阵中,顺时针旋转 90 度是由 4 个元素组成一组"环"互相交换的。
- (i,j)(i, j)(i,j) 移到 (j,n−1−i)(j, n-1-i)(j,n−1−i)
- (j,n−1−i)(j, n-1-i)(j,n−1−i) 移到 (n−1−i,n−1−j)(n-1-i, n-1-j)(n−1−i,n−1−j)
- (n−1−i,n−1−j)(n-1-i, n-1-j)(n−1−i,n−1−j) 移到 (n−1−j,i)(n-1-j, i)(n−1−j,i)
- (n−1−j,i)(n-1-j, i)(n−1−j,i) 移到 (i,j)(i, j)(i,j)
而实际上,我们直到是(i,j)−>(j,n−1−i)(i,j) -> (j, n-1-i)(i,j)−>(j,n−1−i);
实际上这等同于(i,j)−>(j,i)−>(j,n−1−i)(i,j) -> (j,i) -> (j, n-1-i)(i,j)−>(j,i)−>(j,n−1−i)
即,第一步为求转置,第二步为按行翻转
Java
class Solution {
public void rotate(int[][] matrix) {
// 转置 + 行翻转
int n = matrix.length;
if(n < 2){
return;
}
// 转置
for(int i = 0; i < n; i++){
// 千万注意,转置的时候j要从i开始,不如会换回来
for(int j = i; j < n; j++){
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 行翻转
for(int i = 0; i < n; i++){
for(int j = 0; j < n / 2; j++){
int temp = matrix[i][j];
matrix[i][j] = matrix[i][n-1-j];
matrix[i][n-1-j] = temp;
}
}
}
}
矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
我的思路:
Java
class Solution {
public void setZeroes(int[][] matrix) {
// 遇到0就将其行与列都设置为0
// 问题在于,在设置01的过程中又遇到0怎么办
// 这是被其他原有0设置成的0吗?还是这就是一个原有0?
// 否则,"0病毒"就会蔓延到整个矩阵
// 最简单的做法就是在创建一个m*n的矩阵
// 初步优化的算法就是创建一个m+n的矩阵,来记录各行各列有没有0
int m = matrix.length;
if(m < 1){
return ;
}
int n = matrix[0].length;
// 记录原矩阵每行是否有0
boolean[] rows = new boolean[m];
boolean[] cols = new boolean[n];
// 遍历查看是否有0
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(matrix[i][j] == 0){
rows[i] = true;
cols[j] = true;
}
}
}
// 遍历数组,如果行或列有原0,则将其设置为0
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(rows[i] || cols[j]){
matrix[i][j] = 0;
}
}
}
}
}
原地算法思路:
Java
class Solution {
public void setZeroes(int[][] matrix) {
// 还有一个空间优化算法就是利用第一行与第一列
// 来实现上个代码的rows和cols两个数组的功能
// 但是存在一个问题就是原来第一行与第一列是否存在真0不知道
// 就使用两个个标志位,如果存在,则需要将第一行与第一列置为0
int m = matrix.length;
if(m < 1){
return;
}
int n = matrix[0].length;
// 第一行是否原来就有0
boolean firstRowZero = false;
// 第一列是否原来就有0
boolean firstColZero = false;
// 遍历球求得第一行第一列的含0信息
for(int i = 0; i < m; i++){
if(matrix[i][0] == 0){
firstColZero = true;
break;
}
}
for(int j = 0; j < n; j++){
if(matrix[0][j] == 0){
firstRowZero = true;
break;
}
}
// 利用第一行第一列来进行存储,需要注意的是,不能对第一行第一列进行遍历
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(matrix[i][j] == 0){
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 修改整个矩阵,仍要跳过第一行第一列
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(matrix[i][0] == 0 || matrix[0][j] == 0){
matrix[i][j] = 0;
}
}
}
// 第一行第一列
if(firstColZero){
for(int i = 0; i < m; i++){
matrix[i][0] = 0;
}
}
if(firstRowZero){
for(int j = 0; j < n; j++){
matrix[0][j] = 0;
}
}
}
}
生命游戏
生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是 同时 发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。
给定当前 board 的状态,更新 board 到下一个状态。
注意 你不需要返回任何东西。
我的代码:
Java
class Solution {
public void gameOfLife(int[][] board) {
// 一个简单的思路就是复制一份,然后通过复制的进行指导更新
// 其实要复制的主要原因就是怕连环更新
// 那反正都是int数组,我们就使用0/1之外的值来记录就好了
// 比如一个细胞复活了,我们就定义为2,这样之后的细胞就不会认为他现在就是活的了
// 同理,定义-1为之前活着现在死了
// 最后将其转换回0/1即可
int m = board.length;
if(m < 1){
return ;
}
int n = board[0].length;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
// 相邻格子存活数目
int liveCount = 0;
// 注意这样把自己也算进去了
for(int x = i - 1; x <= i + 1; x++){
// 边界判断,控制行不超出
if(x < 0){
continue;
}
if(x >= m){
break;
}
for(int y = j - 1; y <= j+1; y++){
// 边界判断,使得列不超出
if(y < 0){
continue;
}
if(y >= n){
break;
}
if(board[x][y] == 1){
liveCount++;
}
}
}
// 减去自己
liveCount -= board[i][j];
// 判断自己生死
if(board[i][j] == 0){
// 当前是死细胞,判断能否复活
if(liveCount == 3){
// 复活
board[i][j] = 2;
}
}else{
// 当前是活细胞,判断死活
if(liveCount < 2 || liveCount > 3){
// 死
board[i][j] = -1;
} // 否则,继续活,即不变
}
}
}
// 修改中间态
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(board[i][j] == -1){
board[i][j] = 0;
}else if(board[i][j] == 2){
board[i][j] = 1;
}
}
}
}
}
由于 -1 在逻辑上代表"过去是活的",它应该被计入周围细胞的 liveCount 中。 但我的代码跳过了 -1,导致后面的细胞看到的"活邻居"比实际少。
Java
class Solution {
public void gameOfLife(int[][] board) {
// 一个简单的思路就是复制一份,然后通过复制的进行指导更新
// 其实要复制的主要原因就是怕连环更新
// 那反正都是int数组,我们就使用0/1之外的值来记录就好了
// 比如一个细胞复活了,我们就定义为2,这样之后的细胞就不会认为他现在就是活的了
// 同理,定义-1为之前活着现在死了
// 最后将其转换回0/1即可
int m = board.length;
if(m < 1){
return ;
}
int n = board[0].length;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
// 相邻格子存活数目
int liveCount = 0;
// 注意这样把自己也算进去了
for(int x = i - 1; x <= i + 1; x++){
// 边界判断,控制行不超出
if(x < 0){
continue;
}
if(x >= m){
break;
}
for(int y = j - 1; y <= j+1; y++){
// 边界判断,使得列不超出
if(y < 0){
continue;
}
if(y >= n){
break;
}
if(board[x][y] == 1 || board[x][y] == -1){ // 重要修正
// 原来为-1,表明当前是活的,只是下一步要死而已
// 所以应该计入
liveCount++;
}
}
}
// 减去自己
liveCount -= board[i][j];
// 判断自己生死
if(board[i][j] == 0){
// 当前是死细胞,判断能否复活
if(liveCount == 3){
// 复活
board[i][j] = 2;
}
}else{
// 当前是活细胞,判断死活
if(liveCount < 2 || liveCount > 3){
// 死
board[i][j] = -1;
} // 否则,继续活,即不变
}
}
}
// 修改中间态
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(board[i][j] == -1){
board[i][j] = 0;
}else if(board[i][j] == 2){
board[i][j] = 1;
}
}
}
}
}