LeetCode 热题 100-----18.矩阵置零
一、前置知识
在做这道题之前,我们需要先掌握几个基础概念,哪怕你完全没接触过算法、没学过矩阵,看完这部分也能轻松跟上后续内容。
1. 什么是矩阵?
矩阵就是“二维数组”,简单说就是“表格”——有行、有列,每个格子里放一个数字。
比如题目中的输入matrix = [[1,1,1],[1,0,1],[1,1,1]],就是一个 3 行 3 列的矩阵:
第 0 行(第一行):[1, 1, 1]
第 1 行(第二行):[1, 0, 1]
第 2 行(第三行):[1, 1, 1]
这里要注意:计算机里的“索引”(行号、列号)都是从 0 开始的,不是从 1 开始的,就像我们数数从 0 起步一样,记好这个细节,后面看代码不会乱。
2. 矩阵的遍历(怎么逐个看矩阵里的数字)
要处理矩阵里的每个数字,需要用“双重循环”——外层循环管“行”,内层循环管“列”。
比如:用i表示“当前行号”,用j表示“当前列号”,先固定i(比如先看第 0 行),然后让j从 0 到 列数-1(看完第 0 行的所有列),再让i加 1(看第 1 行),重复这个过程,就能看完矩阵里所有的数字。
举个例子:遍历 3x3 矩阵,顺序是:(0,0) → (0,1) → (0,2) → (1,0) → (1,1) → (1,2) → (2,0) → (2,1) → (2,2),对应表格里的每个格子。
3. 原地算法(题目强制要求)
原地算法的核心:不创建和原矩阵一样大的新矩阵,直接修改题目给的原始矩阵。
简单说:题目给你一个矩阵matrix,你不能再做一个和它一样大的矩阵(比如new_matrix = [[0]*n for _ in range(m)])来存结果,必须直接改matrix里面的数字,这样做的目的是节省内存。
题目里函数返回值是None(Python)或void(C++),就是告诉你“不用返回新矩阵,改原始矩阵就行”。
4. 空间复杂度(衡量算法占用内存的多少)
空间复杂度是算法额外占用的内存大小,数值越小越好,这道题的进阶要求是“常量空间”(占用内存固定,和矩阵大小无关)。我们分三种情况理解,对应后面的三种解法:
O(mn):额外创建一个和原矩阵一样大的矩阵(m 行 n 列),比如暴力解法,占用内存最多;
O(m+n):额外创建两个一维数组(一个存需要置零的行,一个存需要置零的列),占用内存中等;
O(1):只用到几个临时变量(比如一个布尔值、一个整数),不创建任何数组,占用内存最少,是最优解。
补充:m 是矩阵的行数,n 是矩阵的列数(比如 3x3 矩阵,m=3,n=3)。
5. Python 和 C++ 中二维数组的基础操作(必看,否则看不懂代码)
Python 部分
获取矩阵行数:
m = len(matrix)(matrix 是一个列表,里面的每个元素都是一行,所以列表的长度就是行数);获取矩阵列数:
n = len(matrix[0])(matrix[0] 是第一行,第一行的长度就是每一行的列数,矩阵每一行列数都一样);访问矩阵中第 i 行第 j 列的元素:
matrix[i][j];复制矩阵:
temp = [row[:] for row in matrix](row[:] 是复制每一行,避免修改 temp 时影响原 matrix)。
C++ 部分
获取矩阵行数:
int m = matrix.size();(matrix 是 vector<vector<int>> 类型,size() 返回它的元素个数,每个元素是一行);获取矩阵列数:
int n = matrix[0].size();(matrix[0] 是第一行,size() 返回第一行的元素个数,即列数);访问矩阵中第 i 行第 j 列的元素:
matrix[i][j];复制矩阵:
vector<vector<int>> temp = matrix;(vector 赋值会自动复制所有元素,修改 temp 不影响原 matrix);vector 初始化:
vector<bool> row(m, false);(创建一个长度为 m 的 bool 类型数组,所有元素默认是 false)。
6. 关键坑点(避免做错的核心)
绝对不能“边遍历边置零”!比如你遍历到 matrix[i][j] = 0,直接把第 i 行、第 j 列都改成 0,那么后面遍历到这些新改的 0 时,会误以为它们是“原始的 0”,导致不该置零的行和列也被置零,最终结果错误。
比如:矩阵 [[1,1,1],[1,0,1],[1,1,1]],如果直接边遍历边置零,当遇到 (1,1) 是 0,把第 1 行、第 1 列置零后,矩阵变成 [[1,0,1],[0,0,0],[1,0,1]],这时候再遍历 (0,1) 这个新的 0,会把第 0 行、第 1 列再置零(虽然结果对,但逻辑错了,换个矩阵就会出错)。所以必须先“标记”需要置零的行和列,再统一置零。
二、题目解析(再明确一遍题目要求)
给定一个 m x n 的矩阵(二维数组),如果矩阵中任意一个元素是 0,就把这个元素所在的“整行”和“整列”的所有元素都改成 0。要求:必须用原地算法,不能创建新的大矩阵。
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] → 有一个 0 在 (1,1) 位置
输出:[[1,0,1],[0,0,0],[1,0,1]] → 第 1 行全置零,第 1 列全置零
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]] → 0 在 (0,0) 和 (0,3) 位置
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]] → 第 0 行全置零,第 0 列、第 3 列全置零
三、三种解法(从暴力到最优,逐一看懂)
我们按照“暴力解法(入门,好懂)→ 优化解法(面试基础)→ 最优解法(面试进阶)”的顺序讲解,每种解法都包含:核心思路、Python 代码(逐行注释)、C++ 代码(逐行注释)、运行流程(跟着走)、优缺点。
解法一:暴力解法(O(mn) 空间,入门首选)
核心思路
既然不能边遍历边置零,那我们就先“复制一份原始矩阵”,遍历复制的矩阵(temp),遇到 0 就去修改“原始矩阵(matrix)”的对应行和列。这样复制的矩阵里的 0 都是原始的 0,不会被新置的 0 干扰。
步骤:1. 复制原始矩阵 → 2. 遍历复制矩阵,标记并修改原始矩阵 → 3. 完成置零。
Python 代码(逐行注释,能看懂每一句)
from typing import List # 导入List类型,用于指定matrix的类型(可理解为:告诉计算机matrix是二维列表) class Solution: def setZeroes(self, matrix: List[List[int]]) -> None: """ Do not return anything, modify matrix in-place instead. 函数说明(必看): - 这个函数不需要返回任何值(返回None) - 要求直接修改输入的matrix(原地算法) - matrix: 输入的二维矩阵(题目给的原始矩阵) """ # 1. 获取矩阵的行数m和列数n m = len(matrix) # len(matrix)返回matrix的元素个数,每个元素是一行,所以m是行数 n = len(matrix[0]) # matrix[0]是第一行,len(matrix[0])是第一行的元素个数,即列数n # 2. 复制原始矩阵temp,用于后续遍历(避免修改原矩阵时干扰判断) # row[:] 表示复制每一行的所有元素,这样修改temp不会影响matrix temp = [row[:] for row in matrix] # 3. 遍历复制的矩阵temp,找所有的0,然后修改原矩阵matrix # 外层循环:遍历每一行,i是当前行号(从0到m-1) for i in range(m): # 内层循环:遍历当前行的每一列,j是当前列号(从0到n-1) for j in range(n): # 当temp中当前位置(i,j)是0时,说明原矩阵对应位置也是0,需要置零对应行和列 if temp[i][j] == 0: # 3.1 置零原矩阵的第i行(把第i行所有列的元素都改成0) # k是列号,从0到n-1,逐个修改第i行的每个元素 for k in range(n): matrix[i][k] = 0 # 把第i行第k列的元素改成0 # 3.2 置零原矩阵的第j列(把第j列所有行的元素都改成0) # k是行号,从0到m-1,逐个修改第j列的每个元素 for k in range(m): matrix[k][j] = 0 # 把第k行第j列的元素改成0 # 主函数(测试代码,可直接运行,包含示例和自定义输入) if __name__ == "__main__": # 实例化Solution类(可理解为:创建一个“解题工具”) solution = Solution() # 测试示例1 print("=== 测试示例1 ===") matrix1 = [[1,1,1],[1,0,1],[1,1,1]] # 示例1输入 print("输入矩阵:") # 打印输入矩阵(逐行打印,更直观) for row in matrix1: print(row) solution.setZeroes(matrix1) # 调用置零函数,直接修改matrix1 print("输出矩阵:") for row in matrix1: print(row) # 打印修改后的矩阵(应该是[[1,0,1],[0,0,0],[1,0,1]]) # 测试示例2 print("\n=== 测试示例2 ===") matrix2 = [[0,1,2,0],[3,4,5,2],[1,3,1,5]] # 示例2输入 print("输入矩阵:") for row in matrix2: print(row) solution.setZeroes(matrix2) print("输出矩阵:") for row in matrix2: print(row) # 打印修改后的矩阵(应该是[[0,0,0,0],[0,4,5,0],[0,3,1,0]]) # 自定义测试(自己输入一个矩阵,测试算法) print("\n=== 自定义测试 ===") # 自定义一个4x4矩阵,包含多个0 matrix3 = [[1,2,3,4],[5,0,7,8],[9,10,11,12],[13,14,15,0]] print("输入矩阵:") for row in matrix3: print(row) solution.setZeroes(matrix3) print("输出矩阵:") for row in matrix3: print(row) # 预期输出:[[1,0,3,0],[0,0,0,0],[9,0,11,0],[0,0,0,0]]C++ 代码(逐行注释,能看懂每一句)
#include <vector> // 导入vector容器(可理解为:用于创建数组的工具) #include <iostream> // 导入输入输出工具(用于打印矩阵) using namespace std; // 简化代码,不用每次写std:: class Solution { public: // 函数说明: // - void:表示函数不返回任何值(对应Python的None) // - vector<vector<int>& matrix:输入的二维矩阵,&表示“引用”,修改matrix会直接修改原始矩阵(原地算法) void setZeroes(vector<vector<int>>& matrix) { // 1. 获取矩阵的行数m和列数n int m = matrix.size(); // matrix.size()返回矩阵的行数(vector的元素个数,每个元素是一行) int n = matrix[0].size(); // matrix[0].size()返回第一行的元素个数,即列数n // 2. 复制原始矩阵temp,用于后续遍历(避免修改原矩阵时干扰判断) vector<vector<int>> temp = matrix; // 直接赋值,vector会自动复制所有元素 // 3. 遍历复制的矩阵temp,找所有的0,然后修改原矩阵matrix // 外层循环:遍历每一行,i是当前行号(从0到m-1) for (int i = 0; i < m; i++) { // 内层循环:遍历当前行的每一列,j是当前列号(从0到n-1) for (int j = 0; j < n; j++) { // 当temp中当前位置(i,j)是0时,置零原矩阵的第i行和第j列 if (temp[i][j] == 0) { // 3.1 置零原矩阵的第i行 for (int k = 0; k < n; k++) { matrix[i][k] = 0; // 把第i行第k列的元素改成0 } // 3.2 置零原矩阵的第j列 for (int k = 0; k < m; k++) { matrix[k][j] = 0; // 把第k行第j列的元素改成0 } } } } } }; // 主函数(测试代码,可直接运行,包含示例和自定义输入) int main() { // 实例化Solution类(创建解题工具) Solution solution; // 测试示例1 cout << "=== 测试示例1 ===" << endl; vector<vector<int>> matrix1 = {{1,1,1},{1,0,1},{1,1,1}}; // 示例1输入 cout << "输入矩阵:" << endl; // 打印输入矩阵(逐行打印) for (int i = 0; i < matrix1.size(); i++) { for (int j = 0; j < matrix1[0].size(); j++) { cout << matrix1[i][j] << " "; // 打印每个元素,加空格分隔 } cout << endl; // 每打印一行换行 } solution.setZeroes(matrix1); // 调用置零函数,直接修改matrix1 cout << "输出矩阵:" << endl; for (int i = 0; i < matrix1.size(); i++) { for (int j = 0; j < matrix1[0].size(); j++) { cout << matrix1[i][j] << " "; } cout << endl; } // 测试示例2 cout << "\n=== 测试示例2 ===" << endl; vector<vector<int>> matrix2 = {{0,1,2,0},{3,4,5,2},{1,3,1,5}}; // 示例2输入 cout << "输入矩阵:" << endl; for (int i = 0; i < matrix2.size(); i++) { for (int j = 0; j < matrix2[0].size(); j++) { cout << matrix2[i][j] << " "; } cout << endl; } solution.setZeroes(matrix2); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix2.size(); i++) { for (int j = 0; j < matrix2[0].size(); j++) { cout << matrix2[i][j] << " "; } cout << endl; } // 自定义测试(自己输入一个矩阵,测试算法) cout << "\n=== 自定义测试 ===" << endl; vector<vector<int>> matrix3 = {{1,2,3,4},{5,0,7,8},{9,10,11,12},{13,14,15,0}}; cout << "输入矩阵:" << endl; for (int i = 0; i < matrix3.size(); i++) { for (int j = 0; j < matrix3[0].size(); j++) { cout << matrix3[i][j] << " "; } cout << endl; } solution.setZeroes(matrix3); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix3.size(); i++) { for (int j = 0; j < matrix3[0].size(); j++) { cout << matrix3[i][j] << " "; } cout << endl; } return 0; // 主函数结束标志 }运行流程(跟着走,以示例1为例)
示例1输入:matrix = [[1,1,1],[1,0,1],[1,1,1]],m=3,n=3
复制矩阵:temp = [[1,1,1],[1,0,1],[1,1,1]](和原矩阵一样);
遍历temp:
i=0(第0行):j=0→1→2,temp[0][j]都是1,不做操作;
i=1(第1行):j=0,temp[1][0]=1;j=1,temp[1][1]=0 → 触发置零:
置零第1行:k=0→1→2,matrix[1][0]=0、matrix[1][1]=0、matrix[1][2]=0 → 此时matrix变成[[1,1,1],[0,0,0],[1,1,1]];
置零第1列:k=0→1→2,matrix[0][1]=0、matrix[1][1]=0、matrix[2][1]=0 → 此时matrix变成[[1,0,1],[0,0,0],[1,0,1]];
i=2(第2行):j=0→1→2,temp[2][j]都是1,不做操作;
遍历结束,matrix就是最终结果[[1,0,1],[0,0,0],[1,0,1]]。
优缺点
✅ 优点:逻辑最简单,完全不会出错,容易理解,适合入门;
❌ 缺点:空间复杂度O(mn),占用内存极大(比如200x200的矩阵,就要额外创建40000个元素的矩阵),实际开发和面试中绝对不推荐使用。
解法二:优化解法(O(m+n) 空间,面试基础写法)
核心思路
暴力解法的问题是“复制了整个矩阵”,太浪费内存。其实我们不需要复制整个矩阵,只需要“标记哪些行、哪些列需要置零”就行——用两个一维数组,一个存需要置零的行,一个存需要置零的列。
步骤:1. 初始化两个标记数组(row标记行,col标记列)→ 2. 遍历原始矩阵,遇到0就标记对应行和列 → 3. 再次遍历原始矩阵,根据标记数组置零。
举个例子:row = [False, True, False] 表示“第1行需要置零”;col = [False, True, False] 表示“第1列需要置零”,后续遍历矩阵时,只要行或列被标记,就把元素改成0。
Python 代码(逐行注释)
from typing import List class Solution: def setZeroes(self, matrix: List[List[int]]) -> None: """ 函数说明:原地修改矩阵,不返回值 matrix: 输入的二维矩阵 """ # 1. 获取矩阵的行数m和列数n m = len(matrix) # 行数m n = len(matrix[0]) # 列数n # 2. 初始化两个标记数组,默认都是False(表示“不需要置零”) row = [False] * m # row是长度为m的数组,row[i] = True → 第i行需要置零 col = [False] * n # col是长度为n的数组,col[j] = True → 第j列需要置零 # 3. 第一步:遍历矩阵,标记需要置零的行和列 # 外层循环遍历每一行,i是行号 for i in range(m): # 内层循环遍历每一列,j是列号 for j in range(n): # 如果当前元素是0,说明第i行和第j列都需要置零,标记对应的位置 if matrix[i][j] == 0: row[i] = True # 标记第i行需要置零 col[j] = True # 标记第j列需要置零 # 4. 第二步:根据标记数组,统一置零原始矩阵 # 再次遍历矩阵的每一个元素 for i in range(m): for j in range(n): # 如果当前行被标记,或者当前列被标记,就把这个元素改成0 if row[i] or col[j]: matrix[i][j] = 0 # 主函数(测试代码,和解法一一致,包含示例和自定义输入) if __name__ == "__main__": solution = Solution() # 测试示例1 print("=== 测试示例1 ===") matrix1 = [[1,1,1],[1,0,1],[1,1,1]] print("输入矩阵:") for row in matrix1: print(row) solution.setZeroes(matrix1) print("输出矩阵:") for row in matrix1: print(row) # 测试示例2 print("\n=== 测试示例2 ===") matrix2 = [[0,1,2,0],[3,4,5,2],[1,3,1,5]] print("输入矩阵:") for row in matrix2: print(row) solution.setZeroes(matrix2) print("输出矩阵:") for row in matrix2: print(row) # 自定义测试 print("\n=== 自定义测试 ===") matrix3 = [[1,2,3,4],[5,0,7,8],[9,10,11,12],[13,14,15,0]] print("输入矩阵:") for row in matrix3: print(row) solution.setZeroes(matrix3) print("输出矩阵:") for row in matrix3: print(row)C++ 代码(逐行注释)
#include <vector> #include <iostream> using namespace std; class Solution { public: void setZeroes(vector<vector<int>& matrix) { // 1. 获取矩阵的行数m和列数n int m = matrix.size(); int n = matrix[0].size(); // 2. 初始化两个标记数组,默认都是false(不需要置零) vector<bool> row(m, false); // row[i]为true → 第i行需要置零 vector<bool> col(n, false); // col[j]为true → 第j列需要置零 // 3. 第一步:遍历矩阵,标记需要置零的行和列 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (matrix[i][j] == 0) { row[i] = true; // 标记第i行 col[j] = true; // 标记第j列 } } } // 4. 第二步:根据标记数组,统一置零 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { // 行或列被标记,就置零 if (row[i] || col[j]) { matrix[i][j] = 0; } } } } }; // 主函数(测试代码,和解法一一致) int main() { Solution solution; // 测试示例1 cout << "=== 测试示例1 ===" << endl; vector<vector<int>> matrix1 = {{1,1,1},{1,0,1},{1,1,1}}; cout << "输入矩阵:" << endl; for (int i = 0; i < matrix1.size(); i++) { for (int j = 0; j < matrix1[0].size(); j++) { cout << matrix1[i][j] << " "; } cout << endl; } solution.setZeroes(matrix1); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix1.size(); i++) { for (int j = 0; j < matrix1[0].size(); j++) { cout << matrix1[i][j] << " "; } cout << endl; } // 测试示例2 cout << "\n=== 测试示例2 ===" << endl; vector<vector<int>> matrix2 = {{0,1,2,0},{3,4,5,2},{1,3,1,5}}; cout << "输入矩阵:" << endl; for (int i = 0; i < matrix2.size(); i++) { for (int j = 0; j < matrix2[0].size(); j++) { cout << matrix2[i][j] << " "; } cout << endl; } solution.setZeroes(matrix2); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix2.size(); i++) { for (int j = 0; j < matrix2[0].size(); j++) { cout << matrix2[i][j] << " "; } cout << endl; } // 自定义测试 cout << "\n=== 自定义测试 ===" << endl; vector<vector<int>> matrix3 = {{1,2,3,4},{5,0,7,8},{9,10,11,12},{13,14,15,0}}; cout << "输入矩阵:" << endl; for (int i = 0; i < matrix3.size(); i++) { for (int j = 0; j < matrix3[0].size(); j++) { cout << matrix3[i][j] << " "; } cout << endl; } solution.setZeroes(matrix3); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix3.size(); i++) { for (int j = 0; j < matrix3[0].size(); j++) { cout << matrix3[i][j] << " "; } cout << endl; } return 0; }运行流程(以示例2为例)
示例2输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]],m=3,n=4
初始化标记数组:row = [False, False, False],col = [False, False, False, False];
第一步:遍历矩阵,标记行和列: 标记后:row = [True, False, False](第0行需要置零),col = [True, False, False, True](第0列、第3列需要置零);
i=0(第0行):
j=0:matrix[0][0] = 0 → row[0] = True,col[0] = True;
j=1:matrix[0][1] = 1 → 不操作;
j=2:matrix[0][2] = 2 → 不操作;
j=3:matrix[0][3] = 0 → row[0] = True(已标记,不变),col[3] = True;
i=1(第1行):j=0→3,matrix[1][j]都不是0 → 不操作;
i=2(第2行):j=0→3,matrix[2][j]都不是0 → 不操作;
第二步:根据标记置零:
i=0(第0行):row[0] = True → 所有列都置零 → 第0行变成[0,0,0,0];
i=1(第1行):row[1] = False,但col[0] = True、col[3] = True → 第0列和第3列置零 → 第1行变成[0,4,5,0];
i=2(第2行):row[2] = False,但col[0] = True、col[3] = True → 第0列和第3列置零 → 第2行变成[0,3,1,0];
最终结果:[[0,0,0,0],[0,4,5,0],[0,3,1,0]],和示例一致。
优缺点
✅ 优点:空间复杂度优化到O(m+n),占用内存少(比如200x200矩阵,只需要额外400个元素的数组),逻辑简单,面试中最基础、最常考的写法;
❌ 缺点:仍需要额外创建两个数组,没有达到题目“常量空间”的进阶要求。
解法三:最优解法(O(1) 常量空间,面试进阶必背)
核心思路
进阶要求:仅用常量空间(不创建任何数组),那我们就“利用矩阵自身的空间”——用矩阵的第一行和第一列,代替解法二的两个标记数组。
关键逻辑:
用 matrix[i][0](第i行的第一个元素)标记“第i行是否需要置零”(代替解法二的row数组);
用 matrix[0][j](第j列的第一个元素)标记“第j列是否需要置零”(代替解法二的col数组);
特殊问题:matrix[0][0] 既属于第一行,又属于第一列,如果用它标记,会分不清是“第一行需要置零”还是“第一列需要置零”,所以我们用一个临时变量
col0单独标记“第一列是否需要置零”,matrix[0][0] 只标记“第一行是否需要置零”。
步骤(必记):
用临时变量 col0 标记第一列是否需要置零;
遍历矩阵(从第0行第0列开始),遇到0就用第一行、第一列和col0做标记;
从“第二行第二列”开始置零(避免覆盖第一行、第一列的标记);
单独处理第一行(根据matrix[0][0]的标记);
单独处理第一列(根据col0的标记)。
Python 代码(逐行注释,重点讲标记和置零顺序)
from typing import List class Solution: def setZeroes(self, matrix: List[List[int]]) -> None: """ 函数说明:原地修改矩阵,常量空间O(1),最优解 matrix: 输入的二维矩阵 """ # 1. 获取矩阵的行数m和列数n m = len(matrix) # 行数m n = len(matrix[0]) # 列数n # 2. 定义临时变量col0,标记第一列是否需要置零(解决matrix[0][0]的冲突) # 初始值设为1(1表示不需要置零,0表示需要置零,用1/0更方便后续判断) col0 = 1 # 3. 第一步:遍历矩阵,用第一行、第一列和col0做标记(核心步骤) # 遍历所有元素,i从0到m-1,j从0到n-1 for i in range(m): for j in range(n): # 如果当前元素是0,进行标记 if matrix[i][j] == 0: # 3.1 标记第i行:把第i行的第一个元素(matrix[i][0])改成0 matrix[i][0] = 0 # 3.2 标记第j列:分两种情况(避免matrix[0][0]冲突) if j == 0: # j=0表示当前元素在第一列,用col0标记第一列需要置零 col0 = 0 else: # j≠0表示当前元素不在第一列,用第一行的第j个元素(matrix[0][j])标记 matrix[0][j] = 0 # 4. 第二步:置零(从第二行第二列开始,避免覆盖标记) # 为什么从i=1、j=1开始?因为第一行和第一列是标记,先不动它们,否则标记会被覆盖 for i in range(1, m): # i从1开始(跳过第一行) for j in range(1, n): # j从1开始(跳过第一列) # 如果当前行的标记是0(matrix[i][0] == 0),或者当前列的标记是0(matrix[0][j] == 0) if matrix[i][0] == 0 or matrix[0][j] == 0: matrix[i][j] = 0 # 置零当前元素 # 5. 第三步:单独处理第一行(根据matrix[0][0]的标记) # matrix[0][0] == 0 表示第一行需要置零 if matrix[0][0] == 0: for j in range(n): # 遍历第一行的所有列,全部置零 matrix[0][j] = 0 # 6. 第四步:单独处理第一列(根据col0的标记) # col0 == 0 表示第一列需要置零 if col0 == 0: for i in range(m): # 遍历第一列的所有行,全部置零 matrix[i][0] = 0 # 主函数(测试代码,包含示例和自定义输入) if __name__ == "__main__": solution = Solution() # 测试示例1 print("=== 测试示例1 ===") matrix1 = [[1,1,1],[1,0,1],[1,1,1]] # 示例1输入 print("输入矩阵:") for row in matrix1: print(row) solution.setZeroes(matrix1) # 调用最优解函数 print("输出矩阵:") for row in matrix1: print(row) # 预期输出:[[1,0,1],[0,0,0],[1,0,1]] # 测试示例2 print("\n=== 测试示例2 ===") matrix2 = [[0,1,2,0],[3,4,5,2],[1,3,1,5]] # 示例2输入 print("输入矩阵:") for row in matrix2: print(row) solution.setZeroes(matrix2) print("输出矩阵:") for row in matrix2: print(row) # 预期输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]] # 自定义测试 print("\n=== 自定义测试 ===") matrix3 = [[1,2,3,4],[5,0,7,8],[9,10,11,12],[13,14,15,0]] print("输入矩阵:") for row in matrix3: print(row) solution.setZeroes(matrix3) print("输出矩阵:") for row in matrix3: print(row) # 预期输出:[[1,0,3,0],[0,0,0,0],[9,0,11,0],[0,0,0,0]]C++ 代码(逐行注释,和Python逻辑完全对应)
#include <vector> #include <iostream> using namespace std; class Solution { public: void setZeroes(vector<vector<int>>& matrix) { // 1. 获取矩阵的行数m和列数n int m = matrix.size(); // 行数m int n = matrix[0].size(); // 列数n // 2. 临时变量col0,标记第一列是否需要置零(解决matrix[0][0]的冲突) // 初始值1:不需要置零;0:需要置零 int col0 = 1; // 3. 第一步:遍历矩阵,用第一行、第一列和col0做标记(核心步骤) for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (matrix[i][j] == 0) { // 3.1 标记第i行:将第i行第一个元素置为0 matrix[i][0] = 0; // 3.2 标记第j列:分情况处理,避免matrix[0][0]冲突 if (j == 0) { // j=0(第一列),用col0标记,不修改matrix[0][0] col0 = 0; } else { // j≠0,用第一行第j列元素标记第j列 matrix[0][j] = 0; } } } } // 4. 第二步:置零(从第二行第二列开始,避免覆盖标记) // 跳过第一行和第一列,防止标记被破坏 for (int i = 1; i < m; i++) { for (int j = 1; j < n; j++) { // 只要当前行或当前列被标记(对应位置为0),就置零当前元素 if (matrix[i][0] == 0 || matrix[0][j] == 0) { matrix[i][j] = 0; } } } // 5. 第三步:单独处理第一行(根据matrix[0][0]的标记) if (matrix[0][0] == 0) { for (int j = 0; j < n; j++) { matrix[0][j] = 0; // 第一行所有元素置零 } } // 6. 第四步:单独处理第一列(根据col0的标记) if (col0 == 0) { for (int i = 0; i < m; i++) { matrix[i][0] = 0; // 第一列所有元素置零 } } } }; // 主函数(测试代码,和解法一、二一致,便于对比) int main() { Solution solution; // 测试示例1 cout << "=== 测试示例1 ===" << endl; vector<vector<int>> matrix1 = {{1,1,1},{1,0,1},{1,1,1}}; cout << "输入矩阵:" << endl; for (int i = 0; i < matrix1.size(); i++) { for (int j = 0; j < matrix1[0].size(); j++) { cout << matrix1[i][j] << " "; } cout << endl; } solution.setZeroes(matrix1); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix1.size(); i++) { for (int j = 0; j < matrix1[0].size(); j++) { cout << matrix1[i][j] << " "; } cout << endl; } // 测试示例2 cout << "\n=== 测试示例2 ===" << endl; vector<vector<int>> matrix2 = {{0,1,2,0},{3,4,5,2},{1,3,1,5}}; cout << "输入矩阵:" << endl; for (int i = 0; i < matrix2.size(); i++) { for (int j = 0; j < matrix2[0].size(); j++) { cout << matrix2[i][j] << " "; } cout << endl; } solution.setZeroes(matrix2); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix2.size(); i++) { for (int j = 0; j < matrix2[0].size(); j++) { cout << matrix2[i][j] << " "; } cout << endl; } // 自定义测试 cout << "\n=== 自定义测试 ===" << endl; vector<vector<int>> matrix3 = {{1,2,3,4},{5,0,7,8},{9,10,11,12},{13,14,15,0}}; cout << "输入矩阵:" << endl; for (int i = 0; i < matrix3.size(); i++) { for (int j = 0; j < matrix3[0].size(); j++) { cout << matrix3[i][j] << " "; } cout << endl; } solution.setZeroes(matrix3); cout << "输出矩阵:" << endl; for (int i = 0; i < matrix3.size(); i++) { for (int j = 0; j < matrix3[0].size(); j++) { cout << matrix3[i][j] << " "; } cout << endl; } return 0; }运行流程(跟着走,以示例1为例,清晰理解标记和置零顺序)
示例1输入:matrix = [[1,1,1],[1,0,1],[1,1,1]],m=3,n=3,初始col0=1
第一步:遍历矩阵,做标记(核心): i=0(第0行):j=0→1→2,matrix[0][j]都是1,不做任何标记;
i=1(第1行): j=0:matrix[1][0] = 1 → 不操作;
j=1:matrix[1][1] = 0 → 触发标记: 标记第1行:matrix[1][0] = 0(把第1行第一个元素改成0);
标记第1列:j≠0,所以matrix[0][1] = 0(把第一行第1列改成0);
j=2:matrix[1][2] = 1 → 不操作;
i=2(第2行):j=0→1→2,matrix[2][j]都是1,不做任何标记;
第二步:从第二行第二列(i=1,j=1)开始置零: i=1(第1行): j=1:matrix[1][0] = 0(行标记为0)→ matrix[1][1] = 0(已为0,不变);
j=2:matrix[1][0] = 0(行标记为0)→ matrix[1][2] = 0;
i=2(第2行): j=1:matrix[0][1] = 0(列标记为0)→ matrix[2][1] = 0;
j=2:matrix[2][0] = 1(行标记不为0)、matrix[0][2] = 1(列标记不为0)→ 不操作;
第三步:单独处理第一行:matrix[0][0] = 1(未标记)→ 第一行不置零(已符合要求);
第四步:单独处理第一列:col0=1(未标记)→ 第一列不置零(已符合要求);
最终结果:[[1,0,1],[0,0,0],[1,0,1]],和示例一致。
优缺点
✅ 优点:空间复杂度O(1)(仅用1个临时变量col0),是题目要求的最优解;
✅ 面试高频考点,必须掌握(面试官常问“如何用常量空间实现”);
✅ 不浪费额外内存,适合大规模矩阵(比如1000x1000矩阵,比解法一、二节省大量内存);
❌ 缺点:逻辑比前两种解法稍复杂,需要注意“标记顺序”和“单独处理第一行、第一列”,容易遗漏细节(比如忘记处理col0,导致第一列置零错误)。
四、全文总结
这道题的核心是“避免边遍历边置零”,核心思路是“先标记,后置零”,三种解法从易到难,适配不同场景,可按以下顺序学习、掌握:
1. 学习顺序
先学「解法一(暴力解法)」:不用考虑优化,重点理解“为什么不能边遍历边置零”,掌握“复制矩阵→遍历标记→修改原矩阵”的思路,打好基础;
再学「解法二(优化解法)」:重点理解“标记数组”的作用,学会用O(m+n)空间优化,掌握“标记行和列→统一置零”的核心逻辑,这是面试中最基础、最常考的写法;
最后学「解法三(最优解法)」:重点掌握“利用矩阵自身空间做标记”,记住“col0临时变量”的作用和“先置零非首行首列→单独处理首行首列”的顺序,应对面试进阶提问。
2. 三种解法对比
解法 | 空间复杂度 | 核心思路 | 适用场景 |
|---|---|---|---|
解法一(暴力) | O(mn) | 复制矩阵,遍历复制矩阵,修改原矩阵 | 入门、理解题意,实际开发/面试不推荐 |
解法二(优化) | O(m+n) | 用两个标记数组,标记行和列,统一置零 | 日常开发、基础面试,逻辑简单、不易出错 |
解法三(最优) | O(1) | 用矩阵首行首列+col0标记,单独处理首行首列 | 面试进阶、大规模矩阵,节省内存 |
3. 易错点总结(避坑)
坑点1:边遍历边置零 → 解决办法:先标记所有需要置零的行和列,再统一置零;
坑点2:解法三中,忘记用col0标记第一列 → 解决办法:记住matrix[0][0]不能同时标记首行和首列,用col0单独标记首列;
坑点3:解法三中,置零顺序错误(先置零首行首列)→ 解决办法:先置零“第二行第二列及以后”,再单独处理首行和首列;
坑点4:C++中忘记用引用(&)→ 解决办法:函数参数必须写vector<vector<int>>& matrix,否则修改的是副本,原矩阵不变;
坑点5:Python中复制矩阵用temp = matrix(浅复制)→ 解决办法:用temp = [row[:] for row in matrix],实现深复制,避免修改temp影响原矩阵。
