1. 题目解析与核心思路
1.1 问题描述重述
LeetCode 73题"矩阵置零"要求我们实现一个算法,当给定一个m×n的矩阵时,如果某个元素为0,则将其所在行和列的所有元素都设为0。题目特别强调需要实现原地算法,这意味着我们不能使用额外的m×n空间来复制原始矩阵。
举个例子,给定矩阵:
code复制[
[1,1,1],
[1,0,1],
[1,1,1]
]
处理后应该变为:
code复制[
[1,0,1],
[0,0,0],
[1,0,1]
]
1.2 原地算法的挑战
实现原地操作的核心难点在于:当我们开始修改矩阵时,后续的修改会基于已经改变的值,这可能导致错误判断。比如,如果我们直接遍历矩阵,遇到0就把行列置零,那么后面遍历到的0可能是被新置入的,这样最终整个矩阵都会变成0。
解决这个问题的关键在于找到一种标记方法,能够区分原始0和新置入的0。常见的思路有:
- 使用第一行和第一列作为标记空间
- 使用特殊值标记需要置零的行列
- 分两次遍历,第一次标记,第二次执行置零
1.3 最优解思路分析
经过比较各种方法,使用矩阵的第一行和第一列作为标记空间是最优解,原因如下:
- 空间复杂度O(1),完全符合原地算法要求
- 只需要两个额外变量记录第一行和第一列是否需要置零
- 两次遍历即可完成,时间复杂度O(m×n)
具体步骤:
- 检查第一行和第一列是否有0,用两个布尔变量记录
- 用第一行和第一列记录其他行列是否需要置零
- 根据标记执行置零操作
- 最后处理第一行和第一列
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++实现详解
2.1 代码实现
cpp复制class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
if (m == 0) return;
int n = matrix[0].size();
bool firstRowZero = false;
bool firstColZero = false;
// 检查第一行是否有0
for (int j = 0; j < n; ++j) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// 检查第一列是否有0
for (int i = 0; i < m; ++i) {
if (matrix[i][0] == 0) {
firstColZero = 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 (firstRowZero) {
for (int j = 0; j < n; ++j) {
matrix[0][j] = 0;
}
}
// 处理第一列
if (firstColZero) {
for (int i = 0; i < m; ++i) {
matrix[i][0] = 0;
}
}
}
};
2.2 关键代码解析
- 边界检查:首先检查矩阵是否为空,避免后续操作出错。
- 第一行/列标记:使用两个布尔变量记录第一行和第一列是否需要置零。
- 标记阶段:遍历除第一行和第一列外的所有元素,发现0时,在对应的第一行和第一列位置标记0。
- 置零阶段:再次遍历,根据第一行和第一列的标记执行置零操作。
- 最后处理:根据最初记录的布尔变量处理第一行和第一列。
注意:标记阶段和置零阶段必须分开进行,不能合并,否则会导致标记被提前修改。
2.3 复杂度分析
- 时间复杂度:O(m×n),因为我们遍历了矩阵两次(标记和置零各一次)。
- 空间复杂度:O(1),只使用了常数个额外变量。
3. 常见问题与调试技巧
3.1 典型错误案例
- 直接修改导致连锁反应:
cpp复制// 错误示范
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (matrix[i][j] == 0) {
// 直接置零行列会导致后续判断出错
for (int k = 0; k < n; ++k) matrix[i][k] = 0;
for (int k = 0; k < m; ++k) matrix[k][j] = 0;
}
}
}
- 忘记处理第一行和第一列:
cpp复制// 部分代码
// 只处理了内部标记,忘记处理第一行和第一列本身
if (matrix[0][0] == 0) { // 这样判断不全面
// ...
}
3.2 调试技巧
- 打印中间状态:在标记阶段和置零阶段之间打印矩阵,检查标记是否正确。
- 小矩阵测试:特别测试1×1、2×2等小矩阵,容易暴露边界问题。
- 全零测试:测试矩阵中所有元素都是0的情况。
- 无零测试:测试矩阵中没有0的情况。
3.3 单元测试示例
cpp复制void test() {
Solution sol;
// 测试1:常规情况
vector<vector<int>> matrix1 = {{1,1,1},{1,0,1},{1,1,1}};
sol.setZeroes(matrix1);
printMatrix(matrix1); // 应输出[[1,0,1],[0,0,0],[1,0,1]]
// 测试2:第一行有0
vector<vector<int>> matrix2 = {{0,1,2,0},{3,4,5,2},{1,3,1,5}};
sol.setZeroes(matrix2);
printMatrix(matrix2); // 应输出[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
// 测试3:单元素矩阵
vector<vector<int>> matrix3 = {{0}};
sol.setZeroes(matrix3);
printMatrix(matrix3); // 应输出[[0]]
}
4. 算法优化与变种
4.1 空间优化的其他方法
除了使用第一行和第一列作为标记空间,还有其他几种O(1)空间的方法:
-
特殊值标记法:
- 第一次遍历时,将需要置零的行列元素改为一个特殊值(如INT_MIN)
- 第二次遍历时,将这些特殊值改为0
- 缺点:假设矩阵中不包含该特殊值
-
位图法(适用于小矩阵):
- 使用两个整数,用位表示哪些行/列需要置零
- 例如:rows |= (1 << i) 标记第i行
- 限制:行列数不能超过整数位数
4.2 并行化优化
对于超大矩阵,可以考虑并行化处理:
- 标记阶段可以按行或列并行
- 置零阶段也可以并行执行
- 需要注意数据竞争问题,可以使用原子操作或适当分区
4.3 类似题目拓展
- LeetCode 289. 生命游戏:同样需要原地更新矩阵,可以使用类似的标记技巧。
- LeetCode 54. 螺旋矩阵:矩阵遍历技巧的另一种应用。
- LeetCode 48. 旋转图像:原地操作矩阵的经典题目。
5. 工程实践中的注意事项
5.1 API设计考虑
在实际工程中实现类似功能时,需要考虑:
-
输入验证:
- 检查矩阵是否为null
- 检查是否为矩形矩阵(各行长度一致)
- 处理空矩阵情况
-
异常处理:
- 非法输入抛出异常还是静默处理
- 错误码设计
-
多线程安全:
- 如果可能被多线程访问,需要加锁或使用不可变数据结构
5.2 性能调优
对于性能敏感的场景:
- 循环展开:对于小矩阵,可以手动展开循环
- 缓存友好访问:按行优先顺序访问元素
- SIMD指令:使用向量指令加速置零操作
5.3 测试覆盖率
完善的测试应该包括:
- 常规测试用例
- 边界测试:1×1、1×n、m×1矩阵
- 全零矩阵
- 无零矩阵
- 大规模随机矩阵
- 稀疏矩阵(大部分为零)
- 密集矩阵(零很少)
6. 刷题策略与经验分享
6.1 如何高效刷题
-
分类刷题:将矩阵类题目集中练习,掌握常见模式。
-
五遍刷题法:
- 第一遍:自己想解法,写代码
- 第二遍:看最优解,重新实现
- 第三遍:24小时后复习
- 第四遍:一周后复习
- 第五遍:面试前复习
-
错题本:记录做错的题目,分析错误原因。
6.2 面试技巧
遇到这类题目时:
-
先问清楚要求:
- 是否可以修改原矩阵?
- 矩阵大小范围?
- 需要处理特殊输入吗?
-
从暴力法开始:
- 先给出O(m+n)空间的解法
- 然后优化到O(1)空间
-
解释思路:
- 边写边解释思考过程
- 讨论时间/空间复杂度
6.3 个人心得
在实际刷题过程中,我发现这类矩阵题目有几个关键点:
- 标记技巧:学会利用矩阵本身存储额外信息是解决原地问题的关键。
- 遍历顺序:有时反向遍历或分阶段遍历可以避免干扰。
- 边界处理:第一行/列的处理往往容易出错,需要特别注意。
- 测试驱动:先写测试用例再写代码,可以避免很多边界错误。
对于这道题,我最初尝试了使用额外空间的方法,后来才想到可以利用第一行和第一列作为标记空间。这个思路也可以推广到其他需要原地操作的矩阵题目中。
