一、73. 矩阵置零
🔗 题目链接
📝 题目描述
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] 输出:[[1,0,1],[0,0,0],[1,0,1]] 输入: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]]
🧠 思路分析
最直观的想法是用两个布尔数组记录哪些行和列需要置零,空间复杂度 O(m + n)。但题目要求原地算法,所以需要利用矩阵本身来存储标记信息。一个巧妙的做法是:用第一行和第一列作为标记数组。先单独记录第一行和第一列本身是否包含 0,然后遍历除了第一行第一列之外的矩阵,如果某个位置是 0,就把对应行的第一个元素和对应列的第一个元素标记为 0。第二次遍历时,根据这些标记来置零。最后再处理第一行和第一列的标记。这样空间复杂度降到了 O(1)。
💻 代码实现(C++)
cpp
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
bool firstRowZero = false, firstColZero = false;
// 检查第一行是否有0
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) firstRowZero = true;
}
// 检查第一列是否有0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) firstColZero = true;
}
// 用第一行和第一列作为标记
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;
}
}
};
📚 相关学习资源
-
文章 :LeetCode第73题矩阵置零(阿里云开发者社区)------ 从暴力解法到空间优化,详细介绍了用第一行第一列做标记的技巧
-
文章 :☆打卡算法☆LeetCode 73、矩阵置零 算法解析(腾讯云开发者社区)------ 分析了 O(m+n) 和 O(1) 两种方案的实现细节
-
文章 :〖LeetCode Hot100 刷题日记(18/100)〗73. 矩阵置零(掘金)------ 从暴力解法到 O(m+n) 空间优化再到 O(1) 常数空间终极优化,递进式讲解
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(m×n),两次遍历矩阵。
-
空间复杂度:O(1),只用了常数个变量。
二、54. 螺旋矩阵
🔗 题目链接
📝 题目描述
给你一个 m 行 n 列的矩阵 matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5] 输入: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]
🧠 思路分析
螺旋遍历的核心在于控制四个边界:上边界 top、下边界 bottom、左边界 left、右边界 right。按顺时针顺序遍历:先从左到右遍历上边界(top 不变,left 到 right),然后上边界 top++;再从上到下遍历右边界(right 不变,top 到 bottom),然后右边界 right--;如果 top <= bottom,继续从右到左遍历下边界;如果 left <= right,继续从下到上遍历左边界。每遍历完一条边,收缩对应的边界,直到所有元素都被访问。这种方法直观且容易调试,边界条件的处理是关键。
💻 代码实现(C++)
cpp
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res;
if (matrix.empty()) return res;
int top = 0, bottom = matrix.size() - 1;
int left = 0, right = matrix[0].size() - 1;
while (top <= bottom && left <= right) {
// 从左到右遍历上边界
for (int j = left; j <= right; j++) res.push_back(matrix[top][j]);
top++;
// 从上到下遍历右边界
for (int i = top; i <= bottom; i++) res.push_back(matrix[i][right]);
right--;
if (top <= bottom) {
// 从右到左遍历下边界
for (int j = right; j >= left; j--) res.push_back(matrix[bottom][j]);
bottom--;
}
if (left <= right) {
// 从下到上遍历左边界
for (int i = bottom; i >= top; i--) res.push_back(matrix[i][left]);
left++;
}
}
return res;
}
};
📚 相关学习资源
- 文章 :LeetCode 54. 螺旋矩阵:两种解法吃透顺时针遍历逻辑(掘金)------ 详细拆解了方向标记法和边界收缩法两种解法,配有完整可运行代码
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(m×n),每个元素被访问一次。
-
空间复杂度:O(1),不计输出数组。
三、48. 旋转图像
🔗 题目链接
📝 题目描述
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]
🧠 思路分析
不能使用额外矩阵,就需要找到元素之间的映射规律。观察发现,顺时针旋转 90 度等价于两步操作:先沿主对角线翻转(转置),再沿垂直中线翻转(左右镜像)。或者先上下翻转再转置也能达到同样效果。具体步骤:首先将矩阵转置,即 matrix[i][j] 与 matrix[j][i] 交换;然后将每一行的元素左右翻转。这个解法利用了矩阵变换的分解思想,只需要 O(1) 的额外空间。面试中这是最推荐的写法,代码简洁且不易出错。
💻 代码实现(C++)
cpp
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 先转置
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
// 再左右镜像
for (int i = 0; i < n; i++) {
for (int j = 0; j < n / 2; j++) {
swap(matrix[i][j], matrix[i][n - 1 - j]);
}
}
}
};
📚 相关学习资源
-
文章 :LeetCode刷题实战48:旋转图像(腾讯云开发者社区)------ 从坐标变换规律推导到转置+镜像的简洁解法
-
文章 :☆打卡算法☆LeetCode 48、旋转图像 算法解析(腾讯云开发者社区)------ 分析了辅助数组法和原地旋转法两种思路
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(n²),两次遍历矩阵。
-
空间复杂度:O(1),原地交换。
四、240. 搜索二维矩阵 II
🔗 题目链接
📝 题目描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:每行的元素从左到右升序排列,每列的元素从上到下升序排列。
示例:
输入: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
🧠 思路分析
利用矩阵的特殊性质,可以从右上角或左下角开始搜索。以右上角为例,当前元素是所在行的最大值、所在列的最小值。如果当前元素大于 target,说明当前列的所有元素都大于 target,可以向左移动一列;如果当前元素小于 target,说明当前行的所有元素都小于 target,可以向下移动一行;如果相等则找到目标。这样每次比较都能排除一行或一列,时间复杂度降为 O(m + n)。暴力解法 O(m×n) 在数据量大的时候会超时。
💻 代码实现(C++)
cpp
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int i = 0, j = n - 1; // 从右上角开始
while (i < m && j >= 0) {
if (matrix[i][j] == target) return true;
if (matrix[i][j] > target) j--;
else i++;
}
return false;
}
};
📚 相关学习资源
-
文章 :LeetCode 240. 搜索二维矩阵 II 超详细题解(高效解法+思路推导)(CSDN)------ 详细分析了矩阵特性拆解、暴力解法和双指针高效解法
-
文章 :图解LeetCode------240. 搜索二维矩阵 II(腾讯云开发者社区)------ 用图解展示了从右上角搜索的完整过程
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(m + n),每次比较排除一行或一列。
-
空间复杂度:O(1)。
五、200. 岛屿数量
🔗 题目链接
📝 题目描述
给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。你可以假设网格的四个边均被水包围。
示例:
输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1
🧠 思路分析
遍历网格,每遇到一个 '1' 就计数加一,然后用 DFS 或 BFS 把这个岛屿中所有相连的陆地标记为已访问(比如置为 '0' 或用 visited 数组)。DFS 递归地访问上下左右四个方向,遇到边界或水就返回。BFS 用队列记录每个陆地节点的位置,层层扩散。两种方法都能在 O(m×n) 时间内解决问题。如果数据量特别大,递归的 DFS 可能因为栈深度过深而溢出,这时 BFS 更安全。面试中两种写法最好都掌握。
💻 代码实现(C++)
cpp
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
int count = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
count++;
dfs(grid, i, j);
}
}
}
return count;
}
void dfs(vector<vector<char>>& grid, int i, int j) {
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] != '1') return;
grid[i][j] = '0'; // 标记为已访问
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
};
📚 相关学习资源
-
文章 :200. 岛屿数量 (DFS)(华为云开发者社区)------ 提供了标准的深度优先搜索(DFS)解法,代码简洁,思路清晰,非常适合入门理解。
-
文章 :LeetCode200. 岛屿数量C++题解,附思维导图和流程图,一题带你弄懂图的dfs遍历算法(CSDN)------ 包含思维导图和流程图,非常直观地展示了DFS遍历和岛屿计数的全过程。
-
文章 :LeetCode 200. 岛屿数量 - DFS/BFS与并查集详解(CSDN)------ 内容全面,同时提供了DFS、BFS和并查集三种经典解法,适合进阶学习和对比理解。
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(m×n),每个格子最多被访问一次。
-
空间复杂度:最坏 O(m×n),递归栈深度。
六、994. 腐烂的橘子
🔗 题目链接
📝 题目描述
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:值 0 代表空单元格;值 1 代表新鲜橘子;值 2 代表腐烂橘子。每分钟,腐烂的橘子会使其上下左右四个方向相邻的新鲜橘子腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
示例:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]] 输出:4
🧠 思路分析
这是一道典型的多源 BFS 问题。初始时可能有多个腐烂的橘子,它们同时向四周扩散。用队列记录所有腐烂橘子的位置和当前时间,同时统计新鲜橘子的总数。BFS 每层扩散一次,时间加一,腐烂一个新鲜橘子就减少新鲜橘子计数。当队列为空时,如果新鲜橘子数量为 0,则返回扩散的总时间;否则返回 -1。多源 BFS 的关键在于一开始就把所有腐烂橘子都放入队列,这样它们会同步扩散,模拟了"每分钟"的真实过程。
💻 代码实现(C++)
cpp
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> q;
int fresh = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) q.push({i, j});
else if (grid[i][j] == 1) fresh++;
}
}
if (fresh == 0) return 0;
int dirs[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}};
int minutes = -1;
while (!q.empty()) {
int size = q.size();
minutes++;
for (int i = 0; i < size; i++) {
auto [x, y] = q.front(); q.pop();
for (auto& dir : dirs) {
int nx = x + dir[0], ny = y + dir[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
grid[nx][ny] = 2;
fresh--;
q.push({nx, ny});
}
}
}
}
return fresh == 0 ? minutes : -1;
}
};
📚 相关学习资源
- 文章 :LeetCode 994. 腐烂的橘子 - 多源BFS与图层传播详解(CSDN)------ 详细讲解了多源 BFS 的核心思路和代码实现
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(m×n),每个格子最多入队一次。
-
空间复杂度:O(m×n),队列在最坏情况下存储所有格子。
结语
矩阵与图论进阶篇的六道题覆盖了矩阵操作和图论 BFS/DFS 的核心技巧。矩阵置零 考察原地算法的标记技巧;螺旋矩阵 和旋转图像 考验对矩阵边界和变换规律的把握;搜索二维矩阵 II 利用有序特性用双指针高效查找;岛屿数量 是图论连通性问题的基础;腐烂的橘子则是多源 BFS 的经典应用。把这六道题吃透,面试中矩阵和图论类的题目基本都能应对。
下一篇将进入 栈、队列与堆的妙用篇(有效的括号、最小栈、字符串解码、每日温度、接雨水、数组中的第K个最大元素等),敬请期待。
如果本文对你有帮助,欢迎点赞、收藏、转发,你的支持是我持续创作的动力 ❤️
免责声明:本文部分解题思路参考了力扣官方题解及社区优秀文章,相关链接均来自公开网络。若存在侵权问题,请联系删除。