【力扣hot100】矩阵题通关复盘

刷题的时候我发现矩阵题很容易给人一种 "全靠找规律" 的错觉。但刷完几道 Hot100 矩阵题后会发现:很多坑不是难在规律有多隐蔽,而是操作会污染原始数据,干扰后续判断。下面按照我做题真实思考流程复盘四道高频矩阵题:矩阵置零、螺旋矩阵、旋转矩阵、搜索二维矩阵 II。 配套 C++ 完整代码,附带关键注释。

一、矩阵置零:从暴力直接修改 → 先标记后修改

给定一个 mxn 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。**

示例 1:

复制代码
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]

刚开始做矩阵置零这道题时,我的第一反应非常直白:遍历整个矩阵,只要遇到一个 0,就直接把它所在的整行、整列全部置零

这是正常人的直觉思路,简单粗暴,看起来完全没问题。

但大家可以先停下来思考一个问题:为什么这个看似正确的暴力方法,完全过不了题?

核心致命坑点:数据污染 。 如果我们遍历的时候,发现一个 0 就立刻当场置零行列,原本正常的数字会被提前改成 0。 当循环走到后面的位置时,程序根本分不清:这个 0 是矩阵原本就有的 0,还是我刚刚手动置零出来的 0? 于是后续的遍历会被前面的操作误导,产生连锁错误,整张矩阵最后会被改得面目全非。

这也是矩阵题型最经典的陷阱:遍历过程中不能直接修改原矩阵,修改会破坏遍历依据。

想通这个问题后,我立刻换了一个反向思维: 既然不能边遍历边修改,那我就「先记录、后修改」。 我们不需要当场置零任何元素,只需要提前开好两个数组:

  • 一个记录「哪些行需要置零」
  • 一个记录「哪些列需要置零」

第一次遍历矩阵:只做标记,不修改原数据。遇到原始 0,就把对应行、列标记为 true。 第二次遍历矩阵:根据之前的标记,统一批量置零。

先统计、后更新,全程不会破坏原始矩阵数据,彻底解决数据污染问题。

这道题教会了我矩阵题第一个通用核心思想: 矩阵遍历最怕即时修改,但凡修改会影响后续判断,一律采用「先标记、后统一更新」的策略。

cpp 复制代码
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        vector<bool> row(m, false);
        vector<bool> col(n, false);

        // 第一轮:只标记,不修改原矩阵
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(matrix[i][j] == 0){
                    row[i] = true;
                    col[j] = true;
                }
            }
        }
        // 第二轮:依据标记批量置零
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(row[i] || col[j]){
                    matrix[i][j] = 0;
                }
            }
        }
    }
};

二、螺旋矩阵:从无脑模拟走方向 → 分层圈遍历,警惕边界与剩余线段

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

复制代码
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

拿到螺旋矩阵,第一直觉:模拟走路,右、下、左、上循环走,碰到边界或者已经访问过的元素就转向。

大家可以想一想,这个直观模拟思路会遇到什么麻烦?

直接模拟行走最大问题:需要额外开辟一个visited标记矩阵记录哪些位置已经访问过。一旦标记数组不能维护好,就会重复遍历或者越界;如果想不开额外数组,直接修改原矩阵做标记,又会遇到上一题同样的数据污染问题,原始值被覆盖丢失。

于是我转向分层转圈的思路,一圈一圈剥离矩阵外圈。 每完整走完一圈,就向内收缩一层。每一圈分别执行:从左到右、从上到下、从右到左、从下到上四趟遍历。

这里我当时踩了一个很隐蔽的坑:圈循环跑完之后,矩阵不一定全部遍历完成!

结论:取行列的较小值min(rows,cols)

  • 如果较小值是偶数:全部外圈遍历完成,没有剩余元素;
  • 如果较小值是奇数:剥完所有外圈,中间会剩下一条单行或者单列,正方形矩阵会退化成单个中心点,必须单独循环补上这一部分。

如果忽略这个剩余线段的处理,部分样例就会少输出数字。

这道题带给我的感悟:

模拟方向容易写烂,分层处理是矩阵遍历常用手段;不要写完主循环就万事大吉,一定要思考循环结束后,会不会残留未处理的区域。

cpp 复制代码
class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>> &matrix)
    {
        vector<int> ans;
        if (matrix.empty()) return ans;
        int rows = matrix.size(), cols = matrix[0].size();
        int n = min(rows, cols);
        int circle = n / 2;
        int cnt = 0;

        while (circle--)
        {
            // 上边:左→右
            for (int i = cnt; i < cols - cnt; i++)
                ans.push_back(matrix[cnt][i]);
            // 右边:上+1→下
            for (int i = cnt + 1; i < rows - cnt; i++)
                ans.push_back(matrix[i][cols - cnt - 1]);
            // 下边:右-1→左
            for (int i = cols - cnt - 2; i >= cnt; i--)
                ans.push_back(matrix[rows - cnt - 1][i]);
            // 左边:下-1→上+1
            for (int i = rows - cnt - 2; i > cnt; i--)
                ans.push_back(matrix[i][cnt]);
            cnt++;
        }

        // min(行,列)为奇数,存在中间剩余线段
        if (n % 2 != 0)
        {
            if (rows >= cols)
            {
                // 剩余竖线
                for (int i = cnt; i < rows - cnt; i++)
                    ans.push_back(matrix[i][cnt]);
            }
            else
            {
                // 剩余横线
                for (int i = cnt; i < cols - cnt; i++)
                    ans.push_back(matrix[cnt][i]);
            }
        }
        return ans;
    }
};

三、旋转矩阵:从直接移动元素引发覆盖丢失 → 两种原地变换思路

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在**原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

示例 1:

复制代码
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

看到 "把方阵顺时针旋转 90°,原地修改",我第一反应:直接把元素挪到旋转后对应的位置。

很快就遇到问题:四个位置循环轮换,如果直接赋值覆盖,没有临时变量保存,原始值直接丢失,也就是前面聊到的数据污染。 比如 A 位置的值要搬到 D,D 搬到 C,C 搬到 B,B 搬到 A;如果直接连续赋值,还没把旧 A 放到 B,A 的值就已经被覆盖。 于是诞生四向轮换写法:用临时变量缓存第一个元素,完成四个位置循环交换,规避数据丢失。

但四向轮换的下标非常繁琐,坐标很容易写错。有没有更简单的原地方案?

我拿 3 阶小样例手写尝试: 原矩阵做主对角线转置,观察转置后的矩阵和目标旋转矩阵,发现只需要把每一行再做一次逆序,结果正好等于顺时针旋转 90°。

先转置,再每行 reverse,全部依靠两两 swap 交换。swap 不会单向覆盖,天然不会产生数据污染。

这里要特别注意顺序! 提问:如果我先每行逆序,再做转置,得到的是顺时针旋转 90 度吗?

答案是否定的,得到的是逆时针旋转 90°。矩阵变换属于复合变换,操作顺序不能随意调换

两种方案对比:

  1. 四向轮换:几何直观模拟旋转,但下标易错;
  2. 复合基础变换:转置 + 翻转,代码简短,需要能看透坐标映射关系。

感悟:矩阵原地变换,尽量优先使用两两 swap;直接赋值覆盖几乎一定会带来数据丢失问题。

cpp 复制代码
// 方案1:转置 + 每行翻转(推荐,代码简洁)
#include <algorithm>
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        // 主对角线转置
        for(int i = 0; i < n; i++)
        {
            for(int j = 0; j < i; j++)
            {
                swap(matrix[i][j], matrix[j][i]);
            }
        }
        // 每行逆序
        for(int i = 0; i < n; i++)
        {
            reverse(matrix[i].begin(), matrix[i].end());
        }
    }
};
cpp 复制代码
// 方案2:四向轮换
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        for(int layer = 0; layer < n / 2; layer++)
        {
            int first = layer;
            int last = n - layer - 1;
            for(int j = first; j < last; j++)
            {
                int offset = j - first;
                int A_x = first,        A_y = j;
                int B_x = last - offset,B_y = first;
                int C_x = last,         C_y = last - offset;
                int D_x = first + offset,D_y = last;

                int temp = matrix[A_x][A_y];
                matrix[A_x][A_y] = matrix[B_x][B_y];
                matrix[B_x][B_y] = matrix[C_x][C_y];
                matrix[C_x][C_y] = matrix[D_x][D_y];
                matrix[D_x][D_y] = temp;
            }
        }
    }
};

四、搜索二维矩阵 II:暴力遍历到利用矩阵单调性,右上角起点 ≈ 二叉搜索树

编写一个高效的算法来搜索 m xn 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

示例 1:

复制代码
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true

矩阵性质:每行从左到右升序,每列从上到下升序,查找目标值是否存在。

最朴素想法:双重循环全部遍历一遍,肯定能找到答案,但是时间复杂度 O (m*n),完全浪费有序条件。

那能不能像一维数组二分,直接对整个矩阵二分?

答案:不可以。和搜索二维矩阵 I 不一样,本题下一行第一个元素不一定大于上一行最后一个元素,整体不满足全局单调,全局二分失效。

那怎么充分利用「行递增、列递增」的性质? 这里引入一个非常巧妙的视角:我们从主要问题出发(如何在矩阵找到全局单调),既然从左上角不满足全局单调,那我们观察一下:

右上角:往左matrix变小,往下matrix变大

左下角:往右matrix变大,往上matrix变小

右下角:往左往上移动,matrix都变小

可以得出结论:从右上角或者左下角出发,都可以可以得到单调的路径,那么我们选取右上角。

把矩阵右上角看作一棵二叉搜索树 (BST) 的根节点!

  • 二叉搜索树:左子树更小,右子树更大;
  • 当前矩阵:向左走数值变小,向下走数值变大。

映射关系: 向左移动 → 访问左子树 向下移动 → 访问右子树

操作逻辑:起点选定右上角

  • 当前值 == target,直接 return true;
  • 当前值 > target:目标一定不在当前列,向左走(访问左子树);
  • 当前值 < target:目标一定不在当前行,向下走(访问右子树)。

为什么选右上角?右上角是一行最大值、一列最小值。一次比较直接排除一整行或者一整列,没有选择歧义。 如果随便选左上角,元素小于 target 时,既可以向下又可以向右,无法缩小搜索范围。

这道题没有修改矩阵,不存在数据污染,核心思想是利用矩阵特性剪枝,把二维搜索转化成 BST 单向遍历,避免无脑暴力

cpp 复制代码
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        if(matrix.empty() || matrix[0].empty()) return false;
        int m = matrix.size();
        int n = matrix[0].size();
        // 起点:右上角,等价二叉搜索树根
        int x = 0, y = n - 1;
        while(x < m && y >= 0)
        {
            if(matrix[x][y] == target)
            {
                return true;
            }
            else if(matrix[x][y] > target)
            {
                y--; // 向左,左子树
            }
            else
            {
                x++; // 向下,右子树
            }
        }
        return false;
    }
};
相关推荐
东方佑1 小时前
Ripple Model:用“矩阵涟漪”重新定义序列模型,四大定理直指SSM与Transformer的本质!
线性代数·矩阵·transformer
退休倒计时1 小时前
【每日一题】LeetCode 207. 课程表 TypeScript
算法·leetcode·typescript
稚南城才子,乌衣巷风流2 小时前
树的重心:概念、性质与算法详解
算法·深度优先·图论
不会就选b2 小时前
算法日常・每日刷题--<哈希>1
算法·哈希算法
zander2582 小时前
LeetCode 207. 课程表
算法·深度优先
萌动的小火苗2 小时前
深度学习中的损失函数与优化算法基础知识
人工智能·深度学习·算法
happyprince2 小时前
02_OpenCodeReview 具体观:八种算法机制与背后的论文谱系
算法
变量未定义~2 小时前
图论+动态规划——魔法阵
算法
大爱编程♡2 小时前
C++基础-类和对象
c++·算法