1. 稀疏矩阵存储的挑战与CSR格式的诞生
在处理大规模科学计算问题时,我们经常会遇到一个令人头疼的现象:矩阵中90%以上的元素都是零值。比如在有限元分析中,一个100万×100万的矩阵可能只有每行几十个非零元素。如果按照传统的二维数组存储,不仅浪费了海量内存,还会拖慢计算速度。
2005年,我在参与一个气候模拟项目时就踩过这个坑。当时用普通二维数组存储一个50万阶的稀疏矩阵,结果光是内存占用就超过了32GB,而实际有效数据不到200MB。正是这次经历让我意识到稀疏矩阵存储格式的重要性。
CSR(Compressed Sparse Row)格式就是为解决这个问题而生的。它的核心思想就像搬家时压缩衣物——把空气挤掉,只保留实物。具体来说,CSR通过三个精妙设计的数组,实现了稀疏矩阵的高效存储:
- values数组:像打包袋一样,把所有非零元素紧密排列
- col_indices数组:给每个物品贴上位置标签,记录它们原本所在的列
- row_ptr数组:相当于打包箱的分隔板,标记每行物品的起始位置
这种存储方式使得一个包含百万级元素的稀疏矩阵,内存占用可以缩减到传统方式的1%甚至更低。在后续的矩阵运算中,由于跳过了所有零元素的计算,性能也能获得数量级的提升。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CSR格式的三元组结构深度解析
2.1 核心数组的协同工作机制
让我们用图书馆管理系统来类比CSR的三个数组。假设图书馆(矩阵)有N个书架(行),每个书架有M个位置(列),但大部分位置是空的:
- values数组:相当于把所有图书按书架顺序排列成一个长列表
- col_indices数组:记录每本书原本在书架上的具体位置编号
- row_ptr数组:标记每个书架对应的图书在长列表中的起始位置
以文中6×4的矩阵为例:
code复制Dense矩阵:
[[4, 0, 0, 0],
[0, 2, 3, 0],
[0, 0, 5, 0],
[7, 0, 0, 8],
[0, 0, 2, 0],
[0, 9, 0, 0]]
对应的CSR存储为:
cpp复制values = [4, 2, 3, 5, 7, 8, 2, 9]
col_indices = [0, 1, 2, 2, 0, 3, 2, 1]
row_ptr = [0, 1, 3, 4, 6, 7, 8]
关键理解:row_ptr的每个元素不是行号,而是该行数据在values中的"入口地址"。比如row_ptr[3]=4表示第3行(编程中从0开始)的数据从values[4]开始。
2.2 行指针数组的构建算法
row_ptr的构建过程可以用快递分拣来理解。假设我们要分拣6个货架(row)的包裹:
- 初始化row_ptr[0]=0,表示第0个货架从第0个包裹开始
- 扫描第0个货架,发现有1个包裹(数值4),所以第1个货架从0+1=1开始
- 扫描第1个货架,发现有2个包裹(2和3),所以第2个货架从1+2=3开始
- 依此类推,最后row_ptr[6]=8表示所有包裹总数
用C++实现这个逻辑:
cpp复制vector<int> build_row_ptr(const vector<vector<double>>& matrix) {
vector<int> row_ptr = {0};
int count = 0;
