4. 稀疏矩阵转置的 CSR 实现与性能分析

CSR 转置的核心结构与两阶段代价

CSR 用 row_ptr(长度 m+1)划分行、用 col_idx 与 vals 按行内顺序存放非零元。转置后得到的是 Aᵀ 的 CSR,等价于把 A 的每个非零元 (i, j, v) 变成 (j, i, v) 并按新行(原列)分组。直接逐元素插入会破坏行内顺序且需要动态扩容,因此标准做法是计数排序:先统计 A 每一列的非零个数得到 Aᵀ 的行长度,前缀和得到 Aᵀ 的 row_ptr,再用一个游标数组把每个元素放到 Aᵀ 对应行的下一个空位。

输入:A 的 CSR(3×4,nnz=6) row_ptr = [0, 2, 4, 6] col_idx = [0, 2, 1, 3, 0, 2] vals = [5, 7, 6, 8, 9, 4] 行0: (0,0)=5 (0,2)=7 行1: (1,1)=6 (1,3)=8 行2: (2,0)=9 (2,2)=4 阶段 1:按列计数 + 前缀和 count[j] = 列 j 的非零个数 count = [2, 1, 2, 1] 前缀和 → row_ptrᵀ row_ptrᵀ = [0, 2, 3, 5, 6] 代价 O(nnz + n),n 为列数 阶段 2:游标填充 next = copy(row_ptrᵀ[0..n-1]) for i, k in 0..nnz-1: j = col_idx[k] pos = next[j]++ col_idxᵀ[pos]=i; valsᵀ[pos]=vals[k] 代价 O(nnz),顺序写 输出:Aᵀ 的 CSR(4×3,nnz=6) row_ptrᵀ = [0, 2, 3, 5, 6] col_idxᵀ = [0, 2, 1, 0, 2, 1] valsᵀ = [5, 9, 6, 7, 4, 8] 填充轨迹(k 遍历 A 的存储顺序) k=0: (0,0)5 → j=0, pos=0 → col_idxᵀ[0]=0, valsᵀ[0]=5 k=1: (0,2)7 → j=2, pos=3 → col_idxᵀ[3]=0, valsᵀ[3]=7 k=2: (1,1)6 → j=1, pos=2 → col_idxᵀ[2]=1, valsᵀ[2]=6 k=3: (1,3)8 → j=3, pos=5 → col_idxᵀ[5]=1, valsᵀ[5]=8 k=4: (2,0)9 → j=0, pos=1 → col_idxᵀ[1]=2, valsᵀ[1]=9 验证:转置的对称性(Aᵀ 的转置应还原 A) ① 结构对称:nnz(Aᵀ)=nnz(A)=6;row_ptrᵀ 末项 = 6;col_idxᵀ 中每个值 i 出现次数 = A 第 i 行非零数 ② 数值对称:对每个 (i,j,v)∈A,存在 (j,i,v)∈Aᵀ;逐元素比对 vals 与坐标,全部一致 ③ 稠密对照:将 A 与 Aᵀ 展开为稠密矩阵,检查 (Aᵀ)ᵀ == A 且 Aᵀ == A.T,最大绝对误差为 0 ④ 行内有序:col_idxᵀ 在每个行段内严格递增,说明填充阶段保持了列索引顺序

参考实现(C++,CSR 三元组结构)

下面给出可直接编译运行的完整实现,包含转置函数与对称性验证。矩阵采用 0 基索引,row_ptr 长度为 m+1。

#include <cstdio>
#include <vector>
#include <cassert>
using namespace std;

struct CSR {
    int m, n, nnz;
    vector<int> row_ptr;   // 长度 m+1
    vector<int> col_idx;   // 长度 nnz
    vector<double> vals;   // 长度 nnz
};

// 计数排序实现 CSR 转置:返回 A^T 的 CSR
CSR transpose_csr(const CSR& A) {
    CSR T;
    T.m = A.n; T.n = A.m; T.nnz = A.nnz;
    T.row_ptr.assign(T.m + 1, 0);
    T.col_idx.assign(T.nnz, 0);
    T.vals.assign(T.nnz, 0.0);

    // 阶段 1:按列计数
    for (int k = 0; k < A.nnz; ++k)
        T.row_ptr[A.col_idx[k] + 1]++;
    // 前缀和
    for (int j = 0; j < T.m; ++j)
        T.row_ptr[j + 1] += T.row_ptr[j];

    // 阶段 2:游标填充
    vector<int> next(T.row_ptr.begin(), T.row_ptr.end() - 1);
    for (int i = 0; i < A.m; ++i) {
        for (int k = A.row_ptr[i]; k < A.row_ptr[i + 1]; ++k) {
            int j = A.col_idx[k];
            int pos = next[j]++;
            T.col_idx[pos] = i;
            T.vals[pos] = A.vals[k];
        }
    }
    return T;
}

// 稠密展开用于对照
vector<vector<double>> to_dense(const CSR& A) {
    vector<vector<double>> D(A.m, vector<double>(A.n, 0.0));
    for (int i = 0; i < A.m; ++i)
        for (int k = A.row_ptr[i]; k < A.row_ptr[i + 1]; ++k)
            D[i][A.col_idx[k]] = A.vals[k];
    return D;
}

int main() {
    // 3x4 示例矩阵
    CSR A;
    A.m = 3; A.n = 4; A.nnz = 6;
    A.row_ptr = {0, 2, 4, 6};
    A.col_idx = {0, 2, 1, 3, 0, 2};
    A.vals    = {5, 7, 6, 8, 9, 4};

    CSR T = transpose_csr(A);

    // 结构检查
    assert(T.m == 4 && T.n == 3 && T.nnz == 6);
    assert(T.row_ptr.back() == A.nnz);

    // 稠密对照:T 应等于 A 的转置
    auto DA = to_dense(A);
    auto DT = to_dense(T);
    for (int i = 0; i < A.m; ++i)
        for (int j = 0; j < A.n; ++j)
            assert(DA[i][j] == DT[j][i]);

    // 对称性:转置两次还原
    CSR TT = transpose_csr(T);
    assert(TT.m == A.m && TT.n == A.n && TT.nnz == A.nnz);
    for (int i = 0; i < A.m; ++i)
        for (int k = A.row_ptr[i]; k < A.row_ptr[i + 1]; ++k) {
            int j = A.col_idx[k];
            bool found = false;
            for (int q = TT.row_ptr[i]; q < TT.row_ptr[i + 1]; ++q)
                if (TT.col_idx[q] == j && TT.vals[q] == A.vals[k]) found = true;
            assert(found);
        }

    printf("转置与对称性验证通过\n");
    return 0;
}

复杂度分析与性能要点

阶段操作时间复杂度访存特征
计数遍历 col_idx 累加计数O(nnz)随机读 col_idx,写计数数组
前缀和对长度 n 的计数数组求前缀和O(n)顺序读写
填充按行遍历 A,写入 Aᵀ 的对应位置O(nnz)读 A 顺序,写 Aᵀ 随机(按列分散)
合计—O(nnz + n)—

关键点:填充阶段对 Aᵀ 的写入位置由 next[j] 决定,属于按列分散的随机写,缓存局部性弱于计数阶段。若矩阵列数 n 很大而每列非零数很少,随机写会显著拖慢性能;此时可考虑分块转置(按列块切分,使每块的目标行段落在较小范围内)以改善缓存命中。计数阶段只读 col_idx 并写计数数组,通常更快。

验证对称性时,除了比较 nnz 与 row_ptr 末项,还应检查 col_idxᵀ 在每个行段内严格递增——这能同时确认填充阶段没有破坏列索引顺序,是比单纯数值比对更强的结构不变量。
示例中的断言依赖精确浮点相等。若 vals 来自浮点运算结果,应改用相对误差阈值(如 1e-12)比较,避免因舍入误差导致误判。