手撕hot100之矩阵!看完这篇就AC~

目录

[1 题目](#1 题目)

[2 代码实现](#2 代码实现)

c++

js

思考

题解

为什么要这么做?

[C++ 完整代码实现](#C++ 完整代码实现)

[1. 定义变量](#1. 定义变量)

[2. 预处理标记:首行 / 首列是否有 0](#2. 预处理标记:首行 / 首列是否有 0)

[3. 核心标记(复用矩阵空间)](#3. 核心标记(复用矩阵空间))

[4. 批量置零(非首行首列)](#4. 批量置零(非首行首列))

[5. 最后处理首行、首列](#5. 最后处理首行、首列)

复杂度分析

示例验证

[示例 1](#示例 1)

[示例 2](#示例 2)

总结

[3 题目](#3 题目)

[4 代码实现](#4 代码实现)

c++

js

思考

题解

边界收缩法

[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 题目

73. 矩阵置零

给定一个 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.length
  • n == matrix[0].length
  • 1 <= 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:

  1. 先标记:遍历矩阵,用第一行、第一列记录需要置 0 的行和列;
  2. 再置零:根据第一行、第一列的标记,对矩阵非首行首列的位置置零;
  3. 最后处理单独处理第一行、第一列本身是否需要置零。

为什么要这么做?

  • 普通方法需要 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]]

  1. 首行无 0,首列无 0 → row0=false, col0=false
  2. 发现 matrix[1][1]=0 → 标记 matrix[1][0]=0matrix[0][1]=0
  3. 非首行首列根据标记置零;
  4. 最后首行首列不变;输出:[[1,0,1],[0,0,0],[1,0,1]] ✔️

示例 2

输入:[[0,1,2,0],[3,4,5,2],[1,3,1,5]]

  1. 首行有 0、首列有 0 → row0=true, col0=true
  2. 标记对应行列;
  3. 最后首行、首列全部置零;输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]] ✔️

总结

  1. 最优解法核心:用矩阵第一行、第一列做标记,实现 O (1) 空间;
  2. 执行顺序:先标记首行首列 → 再标记其他行列 → 统一置零 → 最后处理首行首列
  3. 代码无额外空间开销,完全满足题目原地算法要求。

3 题目

54. 螺旋矩阵

给你一个 mn 列的矩阵 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.length
  • n == matrix[i].length
  • 1 <= 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 ;
};

思考

上下左右边走边减,意思就是走到头以后自己要知道缩减了。

这类题目其实思考起来比较简单,写代码的时候重复操作也很多。

题解

用四个边界限定遍历范围,按顺时针走完一圈就收缩对应边界,直到所有元素遍历完成

边界收缩法

  1. 定义四个边界:上 (top)、下 (bottom)、左 (left)、右 (right)
  2. 顺时针循环遍历 4 个方向:
    • 从左 → 右 遍历上边界,遍历完上边界向下收缩(top++)
    • 从上 → 下 遍历右边界,遍历完右边界向左收缩(right--)
    • 从右 → 左 遍历下边界,遍历完下边界向上收缩(bottom--)
    • 从下 → 上 遍历左边界,遍历完左边界向右收缩(left++)
  3. 每次遍历前都要判断边界是否合法,避免重复 / 越界
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:都遵循「先预处理标记 / 定边界 → 再批量遍历处理 → 最后收尾处理边界」固定三步流程

通用万能流程(所有矩阵模拟题都适用):

  1. 预处理:先单独处理「首行 / 首列 / 四条边界」,先记录特殊状态
  2. 中间批量处理:遍历矩阵内部,做标记 / 按圈遍历收集元素
  3. 收尾补漏:单独回头处理一开始的首行首列 / 边界剩余部分

共性 3:都不能边遍历边改结果,必须「先标记、后统一修改」

  • 矩阵置零:不能发现 0 立刻置整行整列,会覆盖原始 0 信息
  • 螺旋矩阵:不能不缩边界一直走,会重复遍历、越界

共性 4:都是分层、按圈、按范围处理矩阵

  • 置零:把矩阵分成「首行首列层 + 内部区域层」
  • 螺旋:把矩阵看成一圈一圈向内收缩的环形层

二、分别提炼:思路记忆口诀(背这个就够)

1)73 矩阵置零 思路记忆

口诀

先记首行首列有没有 0,

内部遇 0 就打首行列标记,

再按首行列统一置零,

最后单独补首行首列。

思路拆解

  1. 先单独检查:第一行有没有 0、第一列有没有 0,用两个布尔记下来
  2. 遍历内部元素 (不从 0 行 0 列开始),遇到 0 就把它的行头、列头标 0
  3. 再遍历内部:只要行头 / 列头是 0,当前元素直接置 0
  4. 最后看一开始标记,把第一行、第一列单独置零

2)54 螺旋矩阵 思路记忆

口诀

定好上下左右四边界,

左到右、上到下、右到左、下到上,

走完一边缩一边,

边界交叉立刻停。

思路拆解

  1. 定义 top bottom left right 四条边界
  2. 固定顺时针四步:左→右 → 上→下 → 右→左 → 下→上
  3. 每走完一条边,立刻收缩对应边界
  4. 每次收缩后判断:上下交叉 / 左右交叉 就结束循环

三、统一抽象成「矩阵模拟题通用思维模型」

以后遇到任意二维矩阵模拟题(置零、螺旋、顺时针遍历、对角线、旋转),直接套这个思维:

  1. 定范围:用边界 / 固定行列划分出「特殊区域 + 普通区域」
  2. 先特殊:先单独处理边缘、首行首列、边界状态,保留原始信息
  3. 后普通:遍历内部主体,做标记 / 收集元素
  4. 再统一:按之前标记批量修改 / 输出
  5. 最后补漏:回头把最开始的边缘、边界收尾处理

四、配套极简模板(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

五、怎么一次性记住不混淆?

抓最大共同点:

  • 都不边遍历边改,都是「先记录 / 定边界 → 再批量处理 → 最后补边缘」
  • 都把矩阵拆成:边缘层 + 内部层,先保边缘原始数据,再动内部
  • 置零是「用边缘当标记」,螺旋是「用边缘当边界」

只要记住这个三层套路定层 → 先边缘预处理 → 内部批量处理 → 最后边缘收尾

相关推荐
时光足迹1 小时前
电子书阅读器之笔记高亮(跨段处理)
前端·javascript·react.js
笨笨饿1 小时前
#79_NOP()嵌入式C语言中内联汇编宏的抽象封装模式研究
linux·c语言·网络·驱动开发·算法·硬件工程·个人开发
如君愿1 小时前
考研复习 Day 30 | 习题--计算机网络 第五章(运输层 上)、数据结构 图(上)
数据结构·计算机网络·课后习题
Hello-Mr.Wang2 小时前
【保姆级教程】MasterGo MCP + Cursor 一键实现 UI 设计稿还原
前端·javascript·vue.js·ai编程
weixin_421725262 小时前
C语言中volatile关键字怎么用C语言volatile在多线程中的作用
c语言·数据结构·运算符优先级·变量命名·volatile关键字
宁雨桥2 小时前
前端修行日记之JS 原型与 AI基础常识
前端·javascript·原型模式
风萧萧19992 小时前
问答样例如何在RAG问答中使用?
算法
七夜zippoe2 小时前
DolphinDB分区策略:HASH分区与COMPO分区
算法·哈希算法·hash·dolphindb·compo
水云桐程序员2 小时前
前端教程官方文档|HTML、CSS、JavaScript教程官方文档
前端·javascript·css·html·学习方法