1. 稀疏矩阵基础与东华OJ题目解析
稀疏矩阵这个数据结构在计算机科学领域有着广泛的应用场景,特别是当处理大型科学计算、图像处理或推荐系统时。东华OJ将这道题目归为"基础题",但实际上它很好地考察了学生对数据结构本质的理解和C++基础语法的掌握程度。
稀疏矩阵的特点是矩阵中非零元素的数量远少于零元素的数量。以一个1000×1000的矩阵为例,如果只有1000个非零元素,那么非零元素占比仅为0.1%。这种情况下,使用传统的二维数组存储会浪费大量内存空间。
提示:在实际工程中,当矩阵的稀疏度(非零元素比例)低于5%时,就应该考虑使用稀疏矩阵的存储方式。
东华OJ这道题目要求用C++实现稀疏矩阵的基本操作,核心考察点包括:
- 如何高效存储稀疏矩阵
- 如何实现矩阵转置等基本运算
- C++中结构体/类的使用
- 内存管理的注意事项
2. 稀疏矩阵的存储方案对比
2.1 三元组顺序表存储法
这是最直观的存储方式,也是东华OJ题目推荐的做法。每个非零元素用一个三元组(row, col, value)表示:
cpp复制struct Triple {
int row; // 行号
int col; // 列号
int value; // 元素值
};
整个矩阵则可以用一个Triple数组来存储。例如矩阵:
code复制0 0 3 0
0 0 0 0
1 0 0 0
可以表示为:
cpp复制Triple matrix[] = {
{0, 2, 3},
{2, 0, 1}
};
优点:
- 实现简单直观
- 适合一次性构建后不再修改的场景
- 转置等操作实现方便
缺点:
- 插入/删除元素效率低(需要移动后续元素)
- 无法快速访问某一行或列的所有元素
2.2 行逻辑链接存储法
这是三元组顺序表的改进版,增加一个行起始位置数组:
cpp复制struct RLSMatrix {
Triple data[MAXSIZE]; // 非零元三元组表
int rpos[MAXROW]; // 各行第一个非零元的位置
int rows, cols, nums; // 行数、列数、非零元个数
};
这种结构在进行矩阵乘法等操作时效率更高,但实现复杂度也相应增加。
2.3 十字链表存储法
最复杂的存储方式,适合需要频繁修改矩阵的场景:
cpp复制struct OLNode {
int row, col;
int value;
OLNode *right, *down;
};
struct CrossList {
OLNode **rhead, **chead; // 行/列指针数组
int rows, cols, nums;
};
3. 稀疏矩阵转置算法实现
3.1 普通转置算法
最直接的实现方式是:
- 交换每个三元组的row和col
- 重新排序三元组数组
cpp复制void TransposeSMatrix(Triple S[], Triple T[], int m, int n, int t) {
// S是原矩阵,T存放转置结果
// m,n是原矩阵行列数,t是非零元个数
T[0].row = S[0].col;
T[0].col = S[0].row;
T[0].value = S[0].value;
if (t > 0) {
int q = 1;
for (int col = 0; col < n; ++col) {
for (int p = 1; p <= t; ++p) {
if (S[p].col == col) {
T[q].row = S[p].col;
T[q].col = S[p].row;
T[q].value = S[p].value;
++q;
}
}
}
}
}
时间复杂度:O(n×t),当t≈m×n时退化为O(mn²)
3.2 快速转置算法
利用两个辅助数组:
- num[col]:第col列非零元个数
- cpot[col]:第col列第一个非零元的位置
cpp复制void FastTransposeSMatrix(Triple S[], Triple T[], int m, int n, int t) {
T[0].row = S[0].col;
T[0].col = S[0].row;
T[0].value = S[0].value;
if (t > 0) {
int *num = new int[n](); // 初始化全0
int *cpot = new int[n];
// 统计每列非零元个数
for (int i = 1; i <= t; ++i) {
num[S[i].col]++;
}
// 计算每列起始位置
cpot[0] = 1;
for (int col = 1; col < n; ++col) {
cpot[col] = cpot[col-1] + num[col-1];
}
// 执行转置
for (int i = 1; i <= t; ++i) {
int col = S[i].col;
int q = cpot[col];
T[q].row = S[i].col;
T[q].col = S[i].row;
T[q].value = S[i].value;
cpot[col]++;
}
delete[] num;
delete[] cpot;
}
}
时间复杂度:O(n + t),效率显著提升
4. 东华OJ题目实现细节
4.1 输入输出处理
题目通常要求从标准输入读取矩阵数据,格式可能如下:
code复制3 4 3 // 行数 列数 非零元个数
0 2 3 // 第0行第2列值为3
2 0 1
1 1 2
对应的读取代码:
cpp复制Triple* ReadMatrix(int &m, int &n, int &t) {
cin >> m >> n >> t;
Triple *S = new Triple[t+1]; // 下标0存放矩阵信息
S[0] = {m, n, t};
for (int i = 1; i <= t; ++i) {
cin >> S[i].row >> S[i].col >> S[i].value;
}
return S;
}
4.2 内存管理要点
- 使用动态内存分配时,一定要记得释放:
cpp复制Triple *S = ReadMatrix(m, n, t);
// ...使用S...
delete[] S; // 必须释放
- 可以使用STL vector简化内存管理:
cpp复制vector<Triple> S(t+1);
S[0] = {m, n, t};
for (int i = 1; i <= t; ++i) {
cin >> S[i].row >> S[i].col >> S[i].value;
}
// 无需手动释放
4.3 完整解题代码示例
cpp复制#include <iostream>
#include <vector>
using namespace std;
struct Triple {
int row, col, value;
};
vector<Triple> FastTranspose(const vector<Triple> &S) {
if (S.empty()) return {};
vector<Triple> T(S.size());
T[0] = {S[0].col, S[0].row, S[0].value};
if (S[0].value > 0) {
int n = S[0].col;
vector<int> num(n, 0);
vector<int> cpot(n, 0);
for (int i = 1; i <= S[0].value; ++i) {
num[S[i].col]++;
}
cpot[0] = 1;
for (int col = 1; col < n; ++col) {
cpot[col] = cpot[col-1] + num[col-1];
}
for (int i = 1; i <= S[0].value; ++i) {
int col = S[i].col;
int q = cpot[col]++;
T[q] = {S[i].col, S[i].row, S[i].value};
}
}
return T;
}
int main() {
int m, n, t;
cin >> m >> n >> t;
vector<Triple> S(t+1);
S[0] = {m, n, t};
for (int i = 1; i <= t; ++i) {
cin >> S[i].row >> S[i].col >> S[i].value;
}
vector<Triple> T = FastTranspose(S);
// 输出转置结果
cout << T[0].row << " " << T[0].col << " " << T[0].value << endl;
for (int i = 1; i <= T[0].value; ++i) {
cout << T[i].row << " " << T[i].col << " " << T[i].value << endl;
}
return 0;
}
5. 常见问题与调试技巧
5.1 边界条件处理
- 空矩阵处理:
cpp复制if (S[0].value == 0) {
T[0] = {S[0].col, S[0].row, 0};
return;
}
- 行列索引越界:
cpp复制// 在读取输入时检查
if (row < 0 || row >= m || col < 0 || col >= n) {
cerr << "Invalid position: (" << row << "," << col << ")" << endl;
exit(1);
}
5.2 性能优化技巧
- 输入输出加速:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
- 使用reserve预分配空间:
cpp复制vector<Triple> S;
S.reserve(t+1); // 避免多次扩容
5.3 调试日志输出
在开发阶段可以添加调试输出:
cpp复制void PrintMatrix(const vector<Triple> &M) {
cout << "Matrix " << M[0].row << "x" << M[0].col
<< " with " << M[0].value << " non-zero elements:" << endl;
for (int i = 1; i <= M[0].value; ++i) {
cout << "(" << M[i].row << "," << M[i].col << ")="
<< M[i].value << endl;
}
}
5.4 测试用例设计
好的测试用例应该包括:
- 空矩阵
- 全零矩阵
- 只有一非零元的矩阵
- 对角矩阵
- 随机稀疏矩阵
- 边界情况(如最大尺寸矩阵)
示例测试用例:
code复制// 测试用例1:普通3x4矩阵
3 4 3
0 2 3
2 0 1
1 1 2
// 测试用例2:空矩阵
3 4 0
// 测试用例3:对角矩阵
4 4 4
0 0 1
1 1 2
2 2 3
3 3 4
6. 扩展思考与实际应用
稀疏矩阵在实际工程中的应用远比OJ题目复杂。以推荐系统为例,用户-物品评分矩阵通常非常稀疏(>99%的零值),这时高效的稀疏矩阵实现就至关重要。
在Eigen、SciPy等科学计算库中,稀疏矩阵支持更多高级操作:
- 矩阵乘法
- 矩阵分解(LU、QR等)
- 求解线性方程组
对于C++开发者,建议学习:
- Eigen库的SparseMatrix实现
- 压缩稀疏行(CSR)和压缩稀疏列(CSC)存储格式
- 并行稀疏矩阵算法
在解决东华OJ这道基础题后,可以尝试实现:
- 稀疏矩阵的加法运算
- 稀疏矩阵乘法(时间复杂度O(n²))
- 稀疏矩阵的行/列求和
- 稀疏矩阵的压缩存储(CSR/CSC格式)
