从零开始写算法——矩阵类题:矩阵置零 + 螺旋矩阵

在算法面试和日常练习中,矩阵(二维数组)类的题目出现频率非常高。这类题目通常不涉及特别复杂的数据结构,考察的重点往往是空间优化技巧逻辑模拟能力

今天我们通过两道经典题目------"矩阵置零"和"螺旋矩阵",来深入探讨一下如何优雅地处理矩阵变换与遍历。

第一题:矩阵置零 (Set Matrix Zeroes)

题目分析

题目的要求很简单:给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。

常见的痛点

最直观的做法是:新建一个同样的矩阵,或者用两个数组分别记录哪一行、哪一列需要置零。但这会占用额外的空间(O(mn) 或 O(m+n))。如果题目要求我们使用 O(1) 的额外空间,即"原地"修改,该怎么办呢?

优化的解题思路

我们可以利用矩阵本身的第一行第一列作为"记事本",用来标记剩余部分的行或列是否需要置零。

核心步骤:

  1. 预判首行首列 :首先检查第一行和第一列本身是否包含 0,用两个布尔变量 rowZerocolZero 记录下来。这是因为我们后面要征用这两块地盘做标记,得先记住它们"原本"的状态。

  2. 利用首行首列做标记 :遍历剩余的矩阵(从 i=1, j=1 开始)。如果发现 matrix[i][j] == 0,就在它对应的行头 matrix[i][0] 和列头 matrix[0][j] 填入 0 做记号。

  3. 根据标记置换:再次遍历矩阵(不含首行首列),只要发现行头或列头是 0,就把当前元素置为 0。

  4. 处理首行首列:最后,根据第 1 步记录的布尔变量,决定是否把第一行和第一列全部刷成 0。

代码实现

C++代码实现:

cpp 复制代码
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        bool rowZero = false, colZero = false;
        
        // 1. 遍历并标记
        // 判断第一行和第一列本身是否有0,并利用它们标记内部的0
        for (int i = 0; i < matrix.size(); ++i) {
            for (int j = 0; j < matrix[0].size(); ++j) {
                if (matrix[i][j] == 0) {
                    matrix[0][j] = matrix[i][0] = 0; // 在首行首列做记号
                    if (i == 0) rowZero = true;      // 记录第一行原本状态
                    if (j == 0) colZero = true;      // 记录第一列原本状态
                }
            }
        }

        // 2. 根据标记,处理内部矩阵(从下标1开始,保护标记位)
        for (int i = 1; i < matrix.size(); ++i) {
            for (int j = 1; j < matrix[0].size(); ++j) {
                if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                    matrix[i][j] = 0;
                }
            }
        }

        // 3. 最后处理第一行和第一列
        for (int j = 0; rowZero && j < matrix[0].size(); ++j) {
            matrix[0][j] = 0;
        }
        for (int i = 0; colZero && i < matrix.size(); ++i) {
            matrix[i][0] = 0;
        }
    }
};

第二题:螺旋矩阵 (Spiral Matrix)

题目分析

这就好比玩"贪吃蛇",要求我们按照右 -> 下 -> 左 -> 上的顺时针顺序,从外向内螺旋式地打印出矩阵的所有元素。

解题思路:模拟法

这道题不需要复杂的算法,核心在于模拟行走的路径。

  1. 方向控制 :我们可以定义两个数组 dxdy 来表示移动的方向。

    • dx 代表行的变化(垂直移动),dy 代表列的变化(水平移动)。

    • 顺序是:右 {0, 1}, 下 {1, 0}, 左 {0, -1}, 上 {-1, 0}

  2. 边界判断 :我们需要一个"试探"机制。在真正移动之前,先计算下一步的坐标 xy

    • 如果下一步越界(比如撞墙了)。

    • 或者下一步走到了已访问过的格子(这就需要我们标记走过的路)。

    • 只要满足上述任一条件,就通过 (di + 1) % 4 来切换下一个方向。

  3. 防止走回头路 :为了节省空间,我们可以在遍历过的格子上直接赋值为一个特殊值(例如 INT_MAX),这样就不用开辟额外的 visited 数组了。

代码实现

C++代码实现:

cpp 复制代码
class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        // 定义四个方向:右、下、左、上
        // dx控制行(i)的变化,dy控制列(j)的变化
        vector<int> dx = {0, 1, 0, -1};
        vector<int> dy = {1, 0, -1, 0};
        
        vector<int> ans;
        int di = 0; // 当前方向的索引
        int i = 0;  // 当前行坐标
        int j = 0;  // 当前列坐标
        
        // 矩阵总元素个数即为循环次数
        for (int k = 0; k < matrix.size() * matrix[0].size(); ++k) {
            ans.push_back(matrix[i][j]);
            matrix[i][j] = INT_MAX; // 标记已访问,避免走回头路
            
            // 试探下一步的位置
            int x = i + dx[di];
            int y = j + dy[di];
            
            // 如果越界 或者 遇到了已访问过的格子(INT_MAX)
            if (x < 0 || x >= matrix.size() || 
                y < 0 || y >= matrix[0].size() || 
                matrix[x][y] == INT_MAX) {
                // 顺时针换方向
                di = (di + 1) % 4;
            } 
            
            // 正式移动到下一个位置
            i = i + dx[di];
            j = j + dy[di];
        }
        return ans;
    }
};

总结

这两道题目展示了处理矩阵问题的两个重要技巧:

  1. 原地标记:在不破坏逻辑的前提下,利用输入数据本身的结构(如首行首列)来存储状态,从而节省空间。

  2. 方向数组 :在处理由于路径移动(如旋转、迷宫、搜索)的问题时,使用 dx/dy 数组配合取模运算 (di + 1) % 4,可以让代码逻辑比写一大堆 if-else 清晰得多。

相关推荐
小熳芋20 分钟前
单词搜索- python-dfs&剪枝
算法·深度优先·剪枝
Xの哲學33 分钟前
Linux SLAB分配器深度解剖
linux·服务器·网络·算法·边缘计算
bu_shuo36 分钟前
MATLAB中的转置操作及其必要性
开发语言·算法·matlab
高洁011 小时前
图神经网络初探(2)
人工智能·深度学习·算法·机器学习·transformer
爱装代码的小瓶子1 小时前
算法【c++】二叉树搜索树转换成排序双向链表
c++·算法·链表
思成Codes1 小时前
数据结构:基础线段树——线段树系列(提供模板)
数据结构·算法
虾..3 小时前
Linux 简单日志程序
linux·运维·算法
Trent19853 小时前
影楼精修-眼镜祛反光算法详解
图像处理·人工智能·算法·计算机视觉·aigc
蓝色汪洋3 小时前
经典修路问题
开发语言·c++·算法
csuzhucong3 小时前
122魔方、123魔方
算法