【LeetCode 热题 100】(六)矩阵
73. 矩阵置零
class Solution {public void setZeroes(int[][] matrix) {int row_length = matrix.length;int col_length = matrix[0].length;boolean[] row = new boolean[row_length];boolean[] col = new boolean[col_length];for (int i = 0; i < row_length; i++) {for (int j = 0; j < col_length; j++) {if(matrix[i][j] == 0){row[i] = true;col[j] = true;}}}for (int i = 0; i < row_length; i++) {for (int j = 0; j < col_length; j++) {if(row[i]== true || col[j] == true){matrix[i][j] = 0;}}}}
}
解题思路描述
这段代码实现了 “矩阵置零” 问题。给定一个 m x n 矩阵,如果某个元素为 0,则将其所在行和列的所有元素都设为 0。代码需要满足原地修改的要求(不返回新矩阵)。
核心思想:标记法
使用两个标记数组分别记录哪些行和列需要置零:
- 行标记数组
row[]
:记录包含 0 的行 - 列标记数组
col[]
:记录包含 0 的列
通过两次遍历完成操作:第一次扫描标记,第二次根据标记置零。
步骤详解
-
初始化标记数组:
row[length]
:布尔数组,长度 = 矩阵行数col[length]
:布尔数组,长度 = 矩阵列数- 初始值均为
false
,表示暂无需要置零的行列
-
第一次遍历:标记需要置零的行列(时间复杂度 O(m*n))
- 遍历每个元素
matrix[i][j]
- 当元素值为 0 时:
- 标记行:
row[i] = true
- 标记列:
col[j] = true
- 标记行:
- 关键点:此时不修改矩阵值,避免影响后续标记
- 遍历每个元素
-
第二次遍历:执行置零操作(时间复杂度 O(m*n))
- 再次遍历每个元素
matrix[i][j]
- 若当前行被标记 (
row[i] == true
) 或当前列被标记 (col[j] == true
) - 将元素置零:
matrix[i][j] = 0
- 再次遍历每个元素
为什么有效?
- 原地修改:直接操作输入矩阵,不返回新矩阵
- 避免覆盖问题:先完成全部标记再修改,确保不会遗漏或误标记
- 时间复杂度:O(m*n) 两次完整遍历矩阵
- 空间复杂度:O(m+n) 使用两个额外标记数组
示例演算
以 3x3 矩阵为例:
初始矩阵:
[1, 2, 3]
[4, 0, 6]
[7, 8, 9]步骤1:标记行列在(1,1)发现0 → 标记 row[1]=true, col[1]=true步骤2:根据标记置零row[1]=true → 将第1行(索引1)全置零:[4,0,6]→[0,0,0]col[1]=true → 将第1列(索引1)全置零:[2]→0, [0]→0 (已置零), [8]→0最终结果:
[1, 0, 3]
[0, 0, 0]
[7, 0, 9]
优化方向
虽然当前代码清晰易懂,但空间复杂度可优化至 O(1):
- 用矩阵第一行和第一列代替标记数组
- 额外两个变量标记第一行/列是否需要置零
但当前版本更易理解,适合展示核心思路。
54. 螺旋矩阵
class Solution {public List<Integer> spiralOrder(int[][] matrix) {List<Integer> order = new ArrayList<Integer>();if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {return order;}int rows = matrix.length;int cols = matrix[0].length;// 四个角int left = 0, right = cols - 1, top = 0, bottom = rows - 1;while (left <= right && top <= bottom){// 1.从左向右for (int column = left; column <= right; column++) {order.add(matrix[top][column]);}// 2.从上往下for (int row = top+1; row <= bottom; row++){order.add(matrix[row][right]);}if(left < right && top< bottom){// 3.从右往左for (int column = right-1; column > left; column--) {order.add(matrix[bottom][column]);}// 4.从下往上for (int row = bottom; row > top; row--){order.add(matrix[row][left]);}}left++;right--;top++;bottom--;}return order;}
}
解题思路描述
这段代码实现了 “螺旋矩阵” 问题。给定一个 m x n 的矩阵,要求按照顺时针螺旋顺序返回所有元素。代码的核心思想是模拟螺旋遍历过程,通过控制边界完成遍历。
核心思想:边界收缩法
使用四个变量标记当前遍历的边界:
left
:左边界列索引right
:右边界列索引top
:上边界行索引bottom
:下边界行索引
每完成一圈螺旋遍历,就向内收缩边界,直到遍历完所有元素。
步骤详解
-
初始化边界:
left = 0
(最左列)right = cols - 1
(最右列)top = 0
(最上行)bottom = rows - 1
(最下行)
-
螺旋遍历(循环条件:
left <= right && top <= bottom
):- 从左到右(上边界):
- 遍历
top
行,从left
到right
- 示例:3x3 矩阵中,遍历第0行的 [0][0], [0][1], [0][2]
- 遍历
- 从上到下(右边界):
- 遍历
right
列,从top+1
到bottom
- 示例:3x3 矩阵中,遍历第2列的 [1][2], [2][2]
- 遍历
- 从右到左(下边界,需满足内圈条件):
- 仅当
left < right && top < bottom
时执行(防止单行/单列重复遍历) - 遍历
bottom
行,从right-1
到left+1
- 示例:3x3 矩阵中,遍历第2行的 [2][1]
- 仅当
- 从下到上(左边界,需满足内圈条件):
- 遍历
left
列,从bottom
到top+1
- 示例:3x3 矩阵中,遍历第0列的 [2][0], [1][0]
- 遍历
- 从左到右(上边界):
-
边界收缩:
- 每完成一圈:
left++
,right--
,top++
,bottom--
- 示例:3x3 矩阵第一圈后,边界变为
left=1, right=1, top=1, bottom=1
- 每完成一圈:
-
内圈处理:
- 当边界收缩后仍满足
left <= right && top <= bottom
时继续遍历 - 示例:3x3 矩阵内圈只剩 [1][1] 元素
- 当边界收缩后仍满足
关键点解析
-
内圈条件判断:
- 从右到左和从下到上的遍历需要满足
left < right && top < bottom
- 避免单行/单列时的重复遍历(如 3x1 矩阵)
- 从右到左和从下到上的遍历需要满足
-
边界处理顺序:
- 固定顺序:左→右、上→下、右→左、下→上
- 每次只处理当前边界的最外层
-
终止条件:
- 当左边界越过右边界或上边界越过下边界时结束
示例演算(3x3 矩阵)
初始矩阵:
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]遍历过程:
1. 左→右: [0][0]=1, [0][1]=2, [0][2]=3
2. 上→下: [1][2]=6, [2][2]=9
3. 右→左(内圈): [2][1]=8
4. 下→上(内圈): [2][0]=7, [1][0]=4
5. 边界收缩:left=1, right=1, top=1, bottom=1
6. 左→右(内圈): [1][1]=5结果:[1,2,3,6,9,8,7,4,5]
复杂度分析
- 时间复杂度:O(m×n)
每个元素恰好被访问一次 - 空间复杂度:O(1)(不计输出列表)
仅使用固定数量的边界变量
特殊矩阵处理
- 单行矩阵:仅执行左→右遍历
- 单列矩阵:执行左→右和上→下遍历
- 空矩阵:开始时的空值检查直接返回空列表
这个解法通过精确控制边界移动和遍历方向,高效地实现了螺旋顺序遍历,是处理此类问题的经典方法。
48. 旋转图像
class Solution {public void rotate(int[][] matrix) {int n = matrix.length;int[][] matrix_new = new int[n][n];for (int i = 0; i < n; ++i) {for (int j = 0; j < n; ++j) {matrix_new[j][n-1-i] = matrix[i][j];}}for (int i = 0; i < n; ++i) {for (int j = 0; j < n; ++j) {matrix[i][j] = matrix_new[i][j];}}}
}
解题思路描述:旋转图像(顺时针90度)
方法1:使用额外空间(O(n²)空间复杂度)
核心思路:创建一个新矩阵,按照旋转规律映射元素位置,最后复制回原矩阵。
解题步骤:
-
创建新矩阵:
int n = matrix.length; int[][] matrix_new = new int[n][n]; // 创建与输入矩阵相同大小的新矩阵
-
元素位置映射:
- 旋转规律:原矩阵中
[i][j]
位置的元素,在旋转后应位于[j][n-1-i]
- 映射实现:
for (int i = 0; i < n; ++i) {for (int j = 0; j < n; ++j) {matrix_new[j][n-1-i] = matrix[i][j];} }
- 旋转规律:原矩阵中
-
复制回原矩阵:
for (int i = 0; i < n; ++i) {for (int j = 0; j < n; ++j) {matrix[i][j] = matrix_new[i][j];} }
关键点解析:
- 空间复杂度:O(n²)(需要额外n×n空间存储新矩阵)
- 正确性保证:不修改原矩阵数据,避免覆盖问题
- 时间复杂度:O(n²)(两次完整遍历)
示例演算(2×2矩阵):
原矩阵: 旋转后:
[1,2] [3,1]
[3,4] →→→→ [4,2]映射过程:
1→[0][0] → 新位置[0][1]
2→[0][1] → 新位置[1][1]
3→[1][0] → 新位置[0][0]
4→[1][1] → 新位置[1][0]
方法2:原地旋转(O(1)空间复杂度)
核心思路:通过两次翻转操作实现原地旋转,无需额外空间。
解题步骤:
-
主对角线翻转(矩阵转置):
- 交换元素位置:
matrix[i][j]
↔matrix[j][i]
- 关键:只需遍历上三角区域,避免重复交换
- 交换元素位置:
-
水平翻转(逐行逆序):
- 交换元素位置:
matrix[i][j]
↔matrix[i][n-1-j]
- 关键:每行只需遍历前半部分
- 交换元素位置:
完整代码实现:
public void rotate(int[][] matrix) {int n = matrix.length;// 1. 主对角线翻转(转置)for (int i = 0; i < n; i++) {for (int j = i; j < n; j++) { // 注意:j从i开始int temp = matrix[i][j];matrix[i][j] = matrix[j][i];matrix[j][i] = temp;}}// 2. 水平翻转(逐行逆序)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;}}
}
关键点解析:
-
旋转的数学本质:
顺时针90° = 转置 + 水平翻转 逆时针90° = 转置 + 垂直翻转
-
遍历范围优化:
- 转置时:只遍历上三角区(j从i开始),避免重复交换
- 翻转时:每行遍历前半部分(j < n/2)
-
空间复杂度:O(1)(仅使用常数临时变量)
示例演算(3×3矩阵):
原矩阵:
[1,2,3]
[4,5,6]
[7,8,9]步骤1:转置(主对角线翻转):
[1,4,7]
[2,5,8]
[3,6,9]步骤2:水平翻转(逐行逆序):
[7,4,1] ← 第一行:1和7交换,4不动
[8,5,2] ← 第二行:2和8交换
[9,6,3] ← 第三行:3和9交换(实际操作时按行翻转)最终结果(顺时针旋转90°):
[7,4,1]
[8,5,2]
[9,6,3]
两种方法对比:
特性 | 方法1(额外空间) | 方法2(原地旋转) |
---|---|---|
空间复杂度 | O(n²) | O(1) |
可读性 | 直观易懂 | 需要理解旋转数学原理 |
适用场景 | 内存空间充足时 | 内存受限环境 |
修改方式 | 创建新矩阵 | 直接修改输入矩阵 |
时间复杂度 | O(n²)(两次遍历) | O(n²)(两次遍历) |
方法2通过巧妙的两次翻转操作,在不使用额外空间的情况下高效实现了矩阵旋转,是解决此类问题的经典原地算法。