1. 问题分析与解题思路
今天我们来解决LeetCode第1380题"矩阵中的幸运数"。这道题看似简单,但其中蕴含着一些有趣的算法思维和优化技巧。题目要求我们找出矩阵中同时满足两个条件的元素:既是所在行的最小值,又是所在列的最大值。
1.1 问题重述
给定一个m×n的矩阵,我们需要找出所有满足以下条件的元素:
- 该元素是其所在行的最小值
- 该元素是其所在列的最大值
这样的元素被称为"幸运数",我们需要返回所有幸运数的列表。
1.2 解题思路分析
最直观的解法是:
- 先找出每一行的最小值,存储在一个数组row中
- 再找出每一列的最大值,存储在一个数组col中
- 最后遍历矩阵中的每个元素,检查它是否等于对应行的row值和对应列的col值
这种解法的时间复杂度是O(m×n),因为我们需要遍历矩阵三次:
- 第一次遍历所有行求最小值
- 第二次遍历所有列求最大值
- 第三次遍历所有元素检查条件
空间复杂度是O(m+n),用于存储行最小值和列最大值。
2. 代码实现与详细解析
2.1 完整代码实现
cpp复制class Solution {
public:
vector<int> luckyNumbers(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size(), mi, mx, a;
vector<int> row(m), col(n);
// 找出每行的最小值
for(int i = 0; i < m; i++) {
mi = INT_MAX;
for(int j = 0; j < n; j++)
mi = min(mi, matrix[i][j]);
row[i] = mi;
}
// 找出每列的最大值
for(int i = 0; i < n; i++) {
mx = INT_MIN;
for(int j = 0; j < m; j++)
mx = max(mx, matrix[j][i]);
col[i] = mx;
}
// 检查幸运数
vector<int> ret;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
a = matrix[i][j];
if(a == row[i] && a == col[j])
ret.push_back(a);
}
}
return ret;
}
};
2.2 代码逐行解析
-
变量初始化:
m和n分别表示矩阵的行数和列数row数组存储每行的最小值,大小为mcol数组存储每列的最大值,大小为n
-
求行最小值:
- 外层循环遍历每一行
- 内层循环遍历当前行的每个元素,使用
min函数找出最小值 - 初始值设为
INT_MAX确保第一个元素会被选中
-
求列最大值:
- 外层循环遍历每一列
- 内层循环遍历当前列的每个元素,使用
max函数找出最大值 - 初始值设为
INT_MIN确保第一个元素会被选中
-
检查幸运数:
- 遍历矩阵中的每个元素
- 检查当前元素是否等于对应行的最小值和对应列的最大值
- 如果满足条件,则加入结果列表
3. 算法优化与性能分析
3.1 时间复杂度分析
- 求行最小值:O(m×n)
- 求列最大值:O(n×m)
- 检查幸运数:O(m×n)
- 总时间复杂度:O(3×m×n) = O(m×n)
3.2 空间复杂度分析
- 存储行最小值:O(m)
- 存储列最大值:O(n)
- 总空间复杂度:O(m+n)
3.3 可能的优化方向
虽然这个解法已经相当高效,但我们还可以考虑以下优化:
- 合并遍历:可以在一次遍历中同时记录行最小值和列最大值,减少遍历次数
- 提前终止:如果在找行最小值时发现某个元素比当前列最大值还大,可以提前终止
- 位运算优化:对于特定类型的数据,可以使用位运算加速比较操作
4. 边界条件与测试用例
4.1 常见边界情况
- 矩阵只有1行或1列
- 矩阵所有元素相同
- 矩阵中存在多个幸运数
- 矩阵中不存在幸运数
4.2 测试用例示例
cpp复制// 测试用例1:普通情况
vector<vector<int>> matrix1 = {{3,7,8},{9,11,13},{15,16,17}};
// 预期输出:[15]
// 测试用例2:多幸运数
vector<vector<int>> matrix2 = {{1,10,4,2},{9,3,8,7},{15,16,17,12}};
// 预期输出:[12]
// 测试用例3:无幸运数
vector<vector<int>> matrix3 = {{1,2,3},{4,5,6},{7,8,9}};
// 预期输出:[]
// 测试用例4:单行矩阵
vector<vector<int>> matrix4 = {{7,8,1}};
// 预期输出:[1]
// 测试用例5:单列矩阵
vector<vector<int>> matrix5 = {{7},{8},{1}};
// 预期输出:[8]
5. 常见问题与解决技巧
5.1 常见错误
-
行列索引混淆:在访问矩阵元素时容易把行和列索引搞混
- 正确:
matrix[i][j]表示第i行第j列 - 错误:
matrix[j][i]除非是特意访问列
- 正确:
-
初始值设置不当:
- 求最小值时初始值应设为
INT_MAX - 求最大值时初始值应设为
INT_MIN
- 求最小值时初始值应设为
-
空矩阵处理:
- 题目保证矩阵不为空,但实际应用中需要检查
5.2 调试技巧
- 打印中间结果:在求出行最小值和列最大值后,可以先打印出来验证是否正确
- 小规模测试:先用小矩阵测试,确保基本逻辑正确
- 边界测试:专门测试单行、单列、全相同元素等特殊情况
5.3 性能优化技巧
- 循环展开:对于小矩阵可以手动展开循环
- 并行计算:对于大矩阵可以考虑并行计算行最小值和列最大值
- 缓存友好:按行优先顺序访问矩阵元素,利用CPU缓存局部性
6. 类似题目推荐
-
LeetCode 378. 有序矩阵中第K小的元素
- 同样涉及矩阵遍历和元素比较
- 可以使用类似的预处理思路
-
LeetCode 240. 搜索二维矩阵 II
- 在特殊结构的矩阵中搜索元素
- 可以利用行列有序的性质优化搜索
-
LeetCode 74. 搜索二维矩阵
- 在完全有序的矩阵中搜索元素
- 可以看作是一维二分搜索的扩展
-
LeetCode 54. 螺旋矩阵
- 涉及矩阵的遍历顺序
- 需要处理更复杂的访问模式
7. 实际应用场景
这种矩阵处理技巧在实际开发中有广泛应用:
- 图像处理:寻找图像中的极值点
- 数据分析:在数据表中找出特殊数据点
- 游戏开发:地图或网格系统中的特殊位置检测
- 机器学习:特征矩阵中的异常值检测
8. 个人实现心得
在实际编码过程中,我发现以下几点特别重要:
- 清晰的变量命名:使用
row和col分别存储行最小值和列最大值,避免混淆 - 初始值的选择:使用
INT_MAX和INT_MIN确保第一个元素会被正确比较 - 边界检查:虽然题目保证矩阵不为空,但养成检查的习惯很重要
- 测试驱动:先写测试用例再实现代码,可以更快发现问题
这个解法之所以能达到100%的耗时表现,主要是因为:
- 算法时间复杂度已经是理论最优
- 没有使用额外的数据结构
- 内存访问模式对缓存友好
- 比较操作非常高效
对于想要进一步提高的开发者,我建议:
- 尝试用不同语言实现,比较性能差异
- 思考如何进一步减少内存使用
- 尝试编写并行版本,利用多核处理器加速