1. LeetCode Hot 100矩阵问题概述
矩阵类问题在算法面试中占据重要地位,尤其在大厂技术面试中出现的频率极高。这类问题通常考察应聘者对二维数据结构的处理能力、边界条件的把控以及对空间/时间复杂度优化的敏感度。根据我多年参与大厂面试官的经验,矩阵问题在LeetCode Hot 100中约占15%的比例,是必须掌握的题型。
矩阵问题的典型特征包括:
- 输入数据以二维数组形式呈现
- 常涉及遍历顺序的特殊要求(如螺旋遍历、对角线遍历)
- 需要处理行列索引的边界条件
- 往往可以通过原地修改(in-place)来优化空间复杂度
2. 矩阵问题的核心解题框架
2.1 基础遍历方法
矩阵问题的第一步永远是正确遍历。不同于一维数组,矩阵遍历需要考虑行列两个维度。以下是三种基础遍历方式及其C++实现:
cpp复制// 顺序遍历
for(int i = 0; i < rows; ++i) {
for(int j = 0; j < cols; ++j) {
// 处理matrix[i][j]
}
}
// 对角线遍历(左上到右下)
for(int d = 0; d < rows + cols - 1; ++d) {
for(int i = max(0, d - cols + 1); i <= min(d, rows - 1); ++i) {
int j = d - i;
// 处理matrix[i][j]
}
}
// 螺旋遍历
int top = 0, bottom = rows - 1, left = 0, right = cols - 1;
while(top <= bottom && left <= right) {
// 从左到右
for(int j = left; j <= right; ++j)
// 处理matrix[top][j]
top++;
// 从上到下
for(int i = top; i <= bottom; ++i)
// 处理matrix[i][right]
right--;
if(top <= bottom) {
// 从右到左
for(int j = right; j >= left; --j)
// 处理matrix[bottom][j]
bottom--;
}
if(left <= right) {
// 从下到上
for(int i = bottom; i >= top; --i)
// 处理matrix[i][left]
left++;
}
}
2.2 常见问题类型与解法
矩阵问题大致可分为以下几类,每种类型有其对应的解题模式:
-
搜索类问题:
- 典型题目:240. 搜索二维矩阵 II
- 解法特点:利用矩阵排序特性进行高效搜索
- 时间复杂度优化关键:从右上角或左下角开始搜索
-
路径类问题:
- 典型题目:64. 最小路径和
- 解法特点:动态规划,状态转移方程通常涉及上方和左侧单元格
- 空间优化技巧:原地修改或滚动数组
-
旋转/变换类问题:
- 典型题目:48. 旋转图像
- 解法特点:找到元素位置变换规律,常需要分层处理
- 易错点:边界条件的确定和索引计算
-
岛屿类问题:
- 典型题目:200. 岛屿数量
- 解法特点:DFS/BFS遍历,注意标记已访问单元格
- 优化方向:并查集解法
3. 高频矩阵题目精解
3.1 旋转图像(48题)
这是最经典的矩阵旋转问题,要求将n×n矩阵顺时针旋转90度。关键在于发现旋转前后位置的映射关系:
code复制旋转前位置:(i, j)
旋转后位置:(j, n-1-i)
最优解法是分层旋转,从外到内逐层处理:
cpp复制void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for(int layer = 0; layer < n/2; ++layer) {
int first = layer;
int last = n - 1 - layer;
for(int i = first; i < last; ++i) {
int offset = i - first;
// 保存上边
int temp = matrix[first][i];
// 左到上
matrix[first][i] = matrix[last-offset][first];
// 下到左
matrix[last-offset][first] = matrix[last][last-offset];
// 右到下
matrix[last][last-offset] = matrix[i][last];
// 上到右
matrix[i][last] = temp;
}
}
}
注意:面试中常会追问逆时针旋转或任意角度旋转的解法,建议提前准备。
3.2 搜索二维矩阵II(240题)
这道题考察如何在行列分别排序的矩阵中高效搜索目标值。最优解法利用了矩阵的特殊排序性质:
cpp复制bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.empty() || matrix[0].empty()) return false;
int row = 0;
int col = matrix[0].size() - 1;
while(row < matrix.size() && col >= 0) {
if(matrix[row][col] == target) {
return true;
} else if(matrix[row][col] > target) {
col--;
} else {
row++;
}
}
return false;
}
时间复杂度分析:O(m+n),因为每次循环至少排除一行或一列。
3.3 岛屿数量(200题)
典型的连通分量问题,DFS解法最为直观:
cpp复制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);
}
int numIslands(vector<vector<char>>& grid) {
if(grid.empty()) return 0;
int count = 0;
for(int i = 0; i < grid.size(); ++i) {
for(int j = 0; j < grid[0].size(); ++j) {
if(grid[i][j] == '1') {
dfs(grid, i, j);
count++;
}
}
}
return count;
}
优化建议:对于大规模矩阵,可考虑使用BFS避免递归栈溢出,或使用并查集数据结构。
4. 矩阵问题的进阶技巧
4.1 空间复杂度优化
许多矩阵问题可以通过原地修改来优化空间复杂度。例如"73. 矩阵置零"问题:
cpp复制void setZeroes(vector<vector<int>>& matrix) {
bool firstRowZero = false;
bool firstColZero = false;
// 检查第一行是否有0
for(int j = 0; j < matrix[0].size(); ++j) {
if(matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// 检查第一列是否有0
for(int i = 0; i < matrix.size(); ++i) {
if(matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// 使用第一行和第一列记录0的位置
for(int i = 1; i < matrix.size(); ++i) {
for(int j = 1; j < matrix[0].size(); ++j) {
if(matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 根据记录置零
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;
}
}
}
// 处理第一行
if(firstRowZero) {
for(int j = 0; j < matrix[0].size(); ++j) {
matrix[0][j] = 0;
}
}
// 处理第一列
if(firstColZero) {
for(int i = 0; i < matrix.size(); ++i) {
matrix[i][0] = 0;
}
}
}
4.2 动态规划在矩阵中的应用
"64. 最小路径和"展示了典型的矩阵DP解法:
cpp复制int minPathSum(vector<vector<int>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int m = grid.size();
int n = grid[0].size();
// 初始化第一行和第一列
for(int i = 1; i < m; ++i) {
grid[i][0] += grid[i-1][0];
}
for(int j = 1; j < n; ++j) {
grid[0][j] += grid[0][j-1];
}
// 填充DP表
for(int i = 1; i < m; ++i) {
for(int j = 1; j < n; ++j) {
grid[i][j] += min(grid[i-1][j], grid[i][j-1]);
}
}
return grid[m-1][n-1];
}
4.3 矩阵快速幂优化
对于某些特殊问题,如"62. 不同路径"的变种,可以使用矩阵快速幂将时间复杂度优化到O(log n):
cpp复制vector<vector<long long>> matrixMultiply(vector<vector<long long>>& a, vector<vector<long long>>& b) {
int n = a.size();
vector<vector<long long>> res(n, vector<long long>(n, 0));
for(int i = 0; i < n; ++i) {
for(int j = 0; j < n; ++j) {
for(int k = 0; k < n; ++k) {
res[i][j] += a[i][k] * b[k][j];
}
}
}
return res;
}
vector<vector<long long>> matrixPower(vector<vector<long long>>& mat, int power) {
int n = mat.size();
vector<vector<long long>> res(n, vector<long long>(n, 0));
// 初始化为单位矩阵
for(int i = 0; i < n; ++i) res[i][i] = 1;
while(power > 0) {
if(power & 1) {
res = matrixMultiply(res, mat);
}
mat = matrixMultiply(mat, mat);
power >>= 1;
}
return res;
}
5. 矩阵问题的调试技巧
5.1 可视化调试方法
矩阵问题由于涉及二维结构,调试时打印中间状态特别重要。这是我常用的调试打印函数:
cpp复制void printMatrix(const vector<vector<int>>& matrix) {
for(const auto& row : matrix) {
for(int num : row) {
cout << setw(3) << num << " ";
}
cout << endl;
}
cout << "-----------------" << endl;
}
5.2 边界条件检查清单
处理矩阵问题时,必须特别注意以下边界条件:
- 空矩阵输入(行或列为0)
- 单行或单列矩阵
- 非方阵的特殊处理
- 索引越界防护
- 大数溢出问题(特别是乘积类问题)
5.3 常见错误模式
根据我的面试经验,候选人常犯的矩阵问题错误包括:
- 混淆行列索引顺序(matrix[i][j] vs matrix[j][i])
- 旋转问题时层数计算错误
- DFS/BFS中忘记标记已访问节点导致无限循环
- 原地修改时破坏后续计算需要的数据
6. 矩阵问题的扩展思考
6.1 稀疏矩阵处理
当矩阵中大部分元素为0时,可以考虑使用特殊数据结构优化:
cpp复制// 三元组表示法
struct SparseMatrix {
int rows, cols;
vector<tuple<int, int, int>> elements; // (row, col, value)
};
// 十字链表法(适合频繁修改)
struct OLNode {
int row, col;
int value;
OLNode* right, *down;
};
class CrossList {
vector<OLNode*> rowHeads, colHeads;
// ...
};
6.2 多维矩阵问题
某些问题会扩展到三维或更高维度,如"329. 矩阵中的最长递增路径":
cpp复制int longestIncreasingPath(vector<vector<int>>& matrix) {
if(matrix.empty()) return 0;
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> cache(m, vector<int>(n, 0));
int maxLen = 0;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
maxLen = max(maxLen, dfs(matrix, cache, i, j));
}
}
return maxLen;
}
int dfs(vector<vector<int>>& matrix, vector<vector<int>>& cache, int i, int j) {
if(cache[i][j] != 0) return cache[i][j];
int m = matrix.size(), n = matrix[0].size();
int dirs[] = {0, 1, 0, -1, 0};
int maxPath = 1;
for(int k = 0; k < 4; ++k) {
int x = i + dirs[k], y = j + dirs[k+1];
if(x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] > matrix[i][j]) {
maxPath = max(maxPath, 1 + dfs(matrix, cache, x, y));
}
}
cache[i][j] = maxPath;
return maxPath;
}
6.3 并行计算优化
对于大规模矩阵运算,可以考虑并行化处理。以OpenMP为例:
cpp复制#include <omp.h>
void parallelMatrixMultiply(const vector<vector<int>>& A,
const vector<vector<int>>& B,
vector<vector<int>>& C) {
int m = A.size(), n = A[0].size(), p = B[0].size();
#pragma omp parallel for collapse(2)
for(int i = 0; i < m; ++i) {
for(int j = 0; j < p; ++j) {
int sum = 0;
for(int k = 0; k < n; ++k) {
sum += A[i][k] * B[k][j];
}
C[i][j] = sum;
}
}
}
