目录
[1 题目](#1 题目)
[2 代码实现](#2 代码实现)
[C++ 完整代码实现](#C++ 完整代码实现)
[1. 定义变量](#1. 定义变量)
[2. 预处理标记:首行 / 首列是否有 0](#2. 预处理标记:首行 / 首列是否有 0)
[3. 核心标记(复用矩阵空间)](#3. 核心标记(复用矩阵空间))
[4. 批量置零(非首行首列)](#4. 批量置零(非首行首列))
[5. 最后处理首行、首列](#5. 最后处理首行、首列)
[示例 1](#示例 1)
[示例 2](#示例 2)
[3 题目](#3 题目)
[4 代码实现](#4 代码实现)
[5 小结](#5 小结)
[共性 1:都用「边界 / 标记复用原矩阵」,不新开二维数组](#共性 1:都用「边界 / 标记复用原矩阵」,不新开二维数组)
[共性 2:都遵循「先预处理标记 / 定边界 → 再批量遍历处理 → 最后收尾处理边界」固定三步流程](#共性 2:都遵循「先预处理标记 / 定边界 → 再批量遍历处理 → 最后收尾处理边界」固定三步流程)
[共性 3:都不能边遍历边改结果,必须「先标记、后统一修改」](#共性 3:都不能边遍历边改结果,必须「先标记、后统一修改」)
[共性 4:都是分层、按圈、按范围处理矩阵](#共性 4:都是分层、按圈、按范围处理矩阵)
[1)73 矩阵置零 思路记忆](#1)73 矩阵置零 思路记忆)
[2)54 螺旋矩阵 思路记忆](#2)54 螺旋矩阵 思路记忆)
[四、配套极简模板(C++/JS 同逻辑,思路完全对齐)](#四、配套极简模板(C++/JS 同逻辑,思路完全对齐))
[模板 1:矩阵置零 通用骨架(记结构)](#模板 1:矩阵置零 通用骨架(记结构))
[模板 2:螺旋矩阵 通用骨架(记结构)](#模板 2:螺旋矩阵 通用骨架(记结构))
1 题目
给定一个 mxn 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。**
示例 1:

输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:

输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
提示:
m == matrix.lengthn == matrix[0].length1 <= m, n <= 200-231 <= matrix[i][j] <= 231 - 1
进阶:
- 一个直观的解决方案是使用
O(m n)的额外空间,但这并不是一个好的解决方案。 - 一个简单的改进方案是使用
O(m+n)的额外空间,但这仍然不是最好的解决方案。 - 你能想出一个仅使用常量空间的解决方案吗?
2 代码实现
c++
cpp
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size() ;
int n = matrix[0].size() ;
bool row0 = false ;
bool col0 = false ;
for (int i = 0 ; i < m ; i ++){
if (matrix[i][0] == 0 ){
col0 = true ;
break ;
}
}
for (int j = 0 ; j < n ; j ++){
if (matrix[0][j] == 0 ){
row0 = 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 (row0){
for (int j = 0 ; j < n ; j ++){
matrix[0][j] = 0 ;
}
}
if (col0){
for (int i = 0 ; i < m ; i++){
matrix[i][0] = 0 ;
}
}
}
};
js
javascript
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var setZeroes = function(matrix) {
const n = matrix[0].length ;
const m = matrix.length ;
let row0 = false ;
let col0 = false ;
for (let i = 0 ; i < m ; i ++){
if (matrix[i][0] == 0){
col0 = true ;
break ;
}
}
for (let j = 0 ; j < n ; j ++){
if (matrix[0][j] == 0 ){
row0 = true ;
break ;
}
}
for (let i = 1 ; i < m ; i ++){
for (let j = 1 ; j < n ; j++){
if (matrix[i][j] == 0 ){
matrix[i][0] = 0 ;
matrix[0][j] = 0 ;
}
}
}
for (let i = 1 ; i < m ; i ++){
for (let j = 1 ; j < n ; j++){
if(matrix[i][0] == 0 || matrix[0][j] == 0 ){
matrix[i][j] = 0 ;
}
}
}
if (row0){
for (let j = 0 ; j < n ; j++){
matrix[0][j] = 0 ;
}
}
if (col0){
for (let i = 0 ; i < m ; i++){
matrix[i][0] = 0 ;
}
}
};
思考
我的想法是,统计一下0所在的行,列,已经出现过置0的就不用管,不用接下去检查。
题解
原地算法 :不能额外开辟新矩阵,只能使用常数级别的额外空间;功能要求:矩阵中任意元素为 0,将其所在整行、整列全部置为 0。
不使用额外的行 / 列标记数组,直接利用矩阵的第一行、第一列作为标记位,记录哪些行、哪些列需要置 0:
- 先标记:遍历矩阵,用第一行、第一列记录需要置 0 的行和列;
- 再置零:根据第一行、第一列的标记,对矩阵非首行首列的位置置零;
- 最后处理 :单独处理第一行、第一列本身是否需要置零。
为什么要这么做?
- 普通方法需要 O (m+n) 空间存标记,这个方法直接复用原矩阵空间,满足常量空间进阶要求;
- 必须先遍历标记、再统一置零,否则提前置零会覆盖原始数据,导致结果错误。
C++ 完整代码实现
cpp
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
// 获取矩阵的行数和列数
int m = matrix.size();
int n = matrix[0].size();
// 两个标记变量:记录第一行、第一列是否需要置0
bool row0 = false;
bool col0 = false;
// 第一步:检查第一列是否有0,有则标记col0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
col0 = true;
break;
}
}
// 第二步:检查第一行是否有0,有则标记row0
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
row0 = true;
break;
}
}
// 第三步:遍历矩阵(从第1行、第1列开始)
// 用第一行、第一列标记:matrix[i][0] = 0 表示第i行需要置0
// matrix[0][j] = 0 表示第j列需要置0
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;
}
}
}
// 第四步:根据标记,对非首行首列的元素置0
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 当前行标记为0 或 当前列标记为0 → 置0
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 第五步:如果第一行需要置0,将第一行全部置0
if (row0) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
// 第六步:如果第一列需要置0,将第一列全部置0
if (col0) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}
};
1. 定义变量
m:矩阵行数,n:矩阵列数;row0:标记第一行是否需要全部置 0;col0:标记第一列是否需要全部置 0。
2. 预处理标记:首行 / 首列是否有 0
- 遍历第一列,只要有一个 0,
col0 = true; - 遍历第一行,只要有一个 0,
row0 = true;✅ 作用:避免后续标记覆盖,导致无法判断首行首列本身是否需要置零。
3. 核心标记(复用矩阵空间)
从 i=1, j=1 开始遍历矩阵:
- 如果
matrix[i][j] == 0,说明第 i 行、第 j 列都需要置 0; - 我们把
matrix[i][0](第 i 行第一个元素)设为 0,标记第 i 行; - 把
matrix[0][j](第 j 列第一个元素)设为 0,标记第 j 列。
4. 批量置零(非首行首列)
再次遍历 i=1, j=1 开始的元素:
- 只要当前行被标记 (
matrix[i][0]==0)或当前列被标记 (matrix[0][j]==0),直接置 0。
5. 最后处理首行、首列
- 如果
row0 == true,把第一行全部置 0; - 如果
col0 == true,把第一列全部置 0。
复杂度分析
- 时间复杂度:O(m×n)遍历矩阵 4 次,都是线性遍历,时间复杂度为矩阵元素总数。
- 空间复杂度 :O(1)仅使用了 2 个布尔变量,满足常量空间的最高要求。
示例验证
示例 1
输入:[[1,1,1],[1,0,1],[1,1,1]]
- 首行无 0,首列无 0 →
row0=false, col0=false; - 发现
matrix[1][1]=0→ 标记matrix[1][0]=0、matrix[0][1]=0; - 非首行首列根据标记置零;
- 最后首行首列不变;输出:
[[1,0,1],[0,0,0],[1,0,1]]✔️
示例 2
输入:[[0,1,2,0],[3,4,5,2],[1,3,1,5]]
- 首行有 0、首列有 0 →
row0=true, col0=true; - 标记对应行列;
- 最后首行、首列全部置零;输出:
[[0,0,0,0],[0,4,5,0],[0,3,1,0]]✔️
总结
- 最优解法核心:用矩阵第一行、第一列做标记,实现 O (1) 空间;
- 执行顺序:先标记首行首列 → 再标记其他行列 → 统一置零 → 最后处理首行首列;
- 代码无额外空间开销,完全满足题目原地算法要求。
3 题目
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 10-100 <= matrix[i][j] <= 100
4 代码实现
c++
cpp
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res ;
if (matrix.empty() || matrix[0].empty()) return res ;
int top = 0 ;
int bottom = matrix.size() - 1 ;
int left = 0 ;
int right = matrix[0].size() - 1;
while(true) {
for (int i = left ; i <= right ; i++){
res.push_back(matrix[top][i]);
}
top ++ ;
if (top > bottom) break ;
for (int i = top ; i <= bottom ; i ++){
res.push_back(matrix[i][right]);
}
right --;
if (left > right ) break ;
for (int i = right ; i >= left ; i--){
res.push_back(matrix[bottom][i]);
}
bottom -- ;
if (top > bottom) break ;
for (int i = bottom ; i >= top ; i--){
res.push_back(matrix[i][left]);
}
left++;
if (left > right ) break ;
}
return res ;
}
};
js
javascript
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function(matrix) {
let res = [];
if (matrix.length === 0 || matrix[0].length === 0 ) return res;
let top = 0 ;
let bottom = matrix.length - 1 ;
let left = 0 ;
let right = matrix[0].length - 1 ;
while(true){
for (let i = left ; i <= right ; i ++){
res.push(matrix[top][i]);
}
top ++ ;
if (top > bottom ) break ;
for (let i = top ; i <= bottom ; i ++){
res.push(matrix[i][right]);
}
right -- ;
if (left > right) break ;
for ( let i = right ; i >= left ; i --){
res.push(matrix[bottom][i]);
}
bottom -- ;
if (top > bottom ) break ;
for (let i = bottom ; i >= top ; i--){
res.push(matrix[i][left]);
}
left ++ ;
if (left > right) break ;
}
return res ;
};
思考
上下左右边走边减,意思就是走到头以后自己要知道缩减了。
这类题目其实思考起来比较简单,写代码的时候重复操作也很多。
题解
用四个边界限定遍历范围,按顺时针走完一圈就收缩对应边界,直到所有元素遍历完成。
边界收缩法
- 定义四个边界:上 (top)、下 (bottom)、左 (left)、右 (right)
- 顺时针循环遍历 4 个方向:
- 从左 → 右 遍历上边界,遍历完上边界向下收缩(top++)
- 从上 → 下 遍历右边界,遍历完右边界向左收缩(right--)
- 从右 → 左 遍历下边界,遍历完下边界向上收缩(bottom--)
- 从下 → 上 遍历左边界,遍历完左边界向右收缩(left++)
- 每次遍历前都要判断边界是否合法,避免重复 / 越界
cpp
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res; // 存储结果
if (matrix.empty()) return res;
// 定义四个边界
int top = 0; // 上边界
int bottom = matrix.size() - 1; // 下边界
int left = 0; // 左边界
int right = matrix[0].size() - 1; // 右边界
while (true) {
// 1. 左 → 右 遍历上边界
for (int i = left; i <= right; i++) {
res.push_back(matrix[top][i]);
}
if (++top > bottom) break; // 上边界收缩,越界则结束
// 2. 上 → 下 遍历右边界
for (int i = top; i <= bottom; i++) {
res.push_back(matrix[i][right]);
}
if (--right < left) break; // 右边界收缩,越界则结束
// 3. 右 → 左 遍历下边界
for (int i = right; i >= left; i--) {
res.push_back(matrix[bottom][i]);
}
if (--bottom < top) break; // 下边界收缩,越界则结束
// 4. 下 → 上 遍历左边界
for (int i = bottom; i >= top; i--) {
res.push_back(matrix[i][left]);
}
if (++left > right) break; // 左边界收缩,越界则结束
}
return res;
}
};
5 小结
共性 1:都用「边界 / 标记复用原矩阵」,不新开二维数组
- 矩阵置零:复用第一行、第一列当标记数组,不额外开 m+n 数组
- 螺旋矩阵:用上下左右四条边界框住遍历范围,不新开数组、原地按圈遍历
共性 2:都遵循「先预处理标记 / 定边界 → 再批量遍历处理 → 最后收尾处理边界」固定三步流程
通用万能流程(所有矩阵模拟题都适用):
- 预处理:先单独处理「首行 / 首列 / 四条边界」,先记录特殊状态
- 中间批量处理:遍历矩阵内部,做标记 / 按圈遍历收集元素
- 收尾补漏:单独回头处理一开始的首行首列 / 边界剩余部分
共性 3:都不能边遍历边改结果,必须「先标记、后统一修改」
- 矩阵置零:不能发现 0 立刻置整行整列,会覆盖原始 0 信息
- 螺旋矩阵:不能不缩边界一直走,会重复遍历、越界
共性 4:都是分层、按圈、按范围处理矩阵
- 置零:把矩阵分成「首行首列层 + 内部区域层」
- 螺旋:把矩阵看成一圈一圈向内收缩的环形层
二、分别提炼:思路记忆口诀(背这个就够)
1)73 矩阵置零 思路记忆
口诀
先记首行首列有没有 0,
内部遇 0 就打首行列标记,
再按首行列统一置零,
最后单独补首行首列。
思路拆解
- 先单独检查:第一行有没有 0、第一列有没有 0,用两个布尔记下来
- 遍历内部元素 (不从 0 行 0 列开始),遇到 0 就把它的行头、列头标 0
- 再遍历内部:只要行头 / 列头是 0,当前元素直接置 0
- 最后看一开始标记,把第一行、第一列单独置零
2)54 螺旋矩阵 思路记忆
口诀
定好上下左右四边界,
左到右、上到下、右到左、下到上,
走完一边缩一边,
边界交叉立刻停。
思路拆解
- 定义
top bottom left right四条边界 - 固定顺时针四步:左→右 → 上→下 → 右→左 → 下→上
- 每走完一条边,立刻收缩对应边界
- 每次收缩后判断:上下交叉 / 左右交叉 就结束循环
三、统一抽象成「矩阵模拟题通用思维模型」
以后遇到任意二维矩阵模拟题(置零、螺旋、顺时针遍历、对角线、旋转),直接套这个思维:
- 定范围:用边界 / 固定行列划分出「特殊区域 + 普通区域」
- 先特殊:先单独处理边缘、首行首列、边界状态,保留原始信息
- 后普通:遍历内部主体,做标记 / 收集元素
- 再统一:按之前标记批量修改 / 输出
- 最后补漏:回头把最开始的边缘、边界收尾处理
四、配套极简模板(C++/JS 同逻辑,思路完全对齐)
模板 1:矩阵置零 通用骨架(记结构)
1. 求 m n
2. 布尔记首行、首列是否有0
3. 遍历首行首列 赋值布尔
4. 遍历内部 i>=1 j>=1,遇0标行头列头
5. 再遍历内部,按行头列头置0
6. 按布尔 单独置零首行、首列
模板 2:螺旋矩阵 通用骨架(记结构)
1. 求 m n,定上下左右边界
2. while 死循环
3. 左→右走顶行,顶行下移,判越界
4. 上→下走右列,右列左移,判越界
5. 右→左走底行,底行上移,判越界
6. 下→上走左列,左列右移,判越界
7. 越界就 break
五、怎么一次性记住不混淆?
抓最大共同点:
- 都不边遍历边改,都是「先记录 / 定边界 → 再批量处理 → 最后补边缘」
- 都把矩阵拆成:边缘层 + 内部层,先保边缘原始数据,再动内部
- 置零是「用边缘当标记」,螺旋是「用边缘当边界」
只要记住这个三层套路 :定层 → 先边缘预处理 → 内部批量处理 → 最后边缘收尾