当前位置: 首页 > news >正文

第6章串数组:稀疏矩阵三元组顺序表的行逻辑连接顺序表

2. 行逻辑链接的顺序表

(1)传统矩阵乘法及其在稀疏场景下的局限

首先回顾标准的矩阵乘法。假设有两个矩阵 A=(aij)m×r\pmb{A}=(a_{ij})_{m\times r}A=(aij)m×rB=(bij)r×n\pmb{B}=(b_{ij})_{r\times n}B=(bij)r×n,它们的乘积为一个新矩阵 C=(cij)m×n\pmb{C}=(c_{ij})_{m\times n}C=(cij)m×n,记作 C=AB\pmb{C}=\pmb{AB}C=AB

根据定义,结果矩阵 C\pmb{C}C 中的任意一个元素 cijc_{ij}cij(第 iii 行第 jjj 列的元素)是通过将矩阵 A\pmb{A}A 的第 iii 行与矩阵 B\pmb{B}B 的第 jjj 列的对应元素相乘再求和得到的。其数学公式为:
cij=∑k=1raikbkj=ai1b1j+ai2b2j+⋯+airbrj c_{ij} = \sum_{k=1}^{r} a_{ik}b_{kj} = a_{i1}b_{1j} + a_{i2}b_{2j} + \cdots + a_{ir}b_{rj} cij=k=1raikbkj=ai1b1j+ai2b2j++airbrj
这个定义可以直接转化为一个通用的计算算法,如下所示(注意:此处伪代码的数组索引从1开始,以贴合数学定义):

// 计算 C = A * B
for(i = 1; i <= m; ++i){      // 遍历A的每一行for(j = 1; j <= n; ++j){  // 遍历B的每一列C[i][j] = 0;          // 初始化累加器for(k = 1; k <= r; ++k){ // 计算点积C[i][j] += A[i][k] * B[k][j];}}
}

该算法的时间复杂度为 O(m×n×r)O(m \times n \times r)O(m×n×r),因为它对 C\pmb{C}C 矩阵的 m×nm \times nm×n 个元素中的每一个都执行了 rrr 次乘法和加法。

然而,当这个算法应用于稀疏矩阵(即大部分元素为零的矩阵)时,其效率会变得非常低下。原因在于,无论 aika_{ik}aikbkjb_{kj}bkj 是否为零,上述算法都会执行一次乘法运算。在稀疏矩阵中,大量的乘法都是 非零项 * 00 * 0,这些都是无效计算,因为它们的乘积必然为零,对最终的累加和没有任何贡献,却白白消耗了计算资源。

(2)基于三元组表示的思考与新挑战

为了节省存储空间,通常使用三元组顺序表来表示稀疏矩阵,只存储非零元素的位置(行、列)和值。看一个具体的例子。假设有两个稀疏矩阵 M\pmb{M}MN\pmb{N}N,求它们的乘积 Q=MN\pmb{Q}=\pmb{MN}Q=MN

M=[30050−1002000]3×4,N=[0210−2400]4×2  ⟹  Q=MN=[06−1004]3×2 \pmb{M}=\begin{bmatrix}3&0&0&5\\0&-1&0&0\\2&0&0&0\end{bmatrix}_{3\times4}, \quad \pmb{N}=\begin{bmatrix}0&2\\1&0\\-2&4\\0&0\end{bmatrix}_{4\times2} \quad \implies \quad \pmb{Q}=\pmb{MN}=\begin{bmatrix}0&6\\-1&0\\0&4\end{bmatrix}_{3\times2} M=3020100005003×4,N=012020404×2Q=MN=0106043×2
它们对应的三元组顺序表 M.data, N.data, Q.data 如图 6.7.2 所示:

在这里插入图片描述

图 6.7.2 稀疏矩阵乘法的三元组顺序表

为了避免无效计算,新思路应该是:只对两个矩阵中的非零元进行运算。

根据乘法公式 cij=∑aikbkjc_{ij} = \sum a_{ik}b_{kj}cij=aikbkj,要计算出结果矩阵 Q\pmb{Q}Q 中的一个非零元 QijQ_{ij}Qij,需要找到所有满足“M\pmb{M}M 的列号等于 N\pmb{N}N 的行号”的非零元对 (Mik,Nkj)(M_{ik}, N_{kj})(Mik,Nkj),然后将它们的乘积 Mik×NkjM_{ik} \times N_{kj}Mik×Nkj 累加起来。

例如,计算 Q12Q_{12}Q12Q12=M11N12+M12N22+M13N32+M14N42Q_{12} = M_{11}N_{12} + M_{12}N_{22} + M_{13}N_{32} + M_{14}N_{42}Q12=M11N12+M12N22+M13N32+M14N42,在三元组表中,只关心非零项:

  • 矩阵 M\pmb{M}M 的第1行有两个非零元:M11=3M_{11}=3M11=3M14=5M_{14}=5M14=5

  • 需要找到 N\pmb{N}N 中行号为 1 和 4 的非零元。

    • 遍历 N.data,发现行号为 1 的非零元是 N12=2N_{12}=2N12=2
    • 遍历 N.data,没有发现行号为 4 的非零元。
  • 因此,Q12=M11×N12=3×2=6Q_{12} = M_{11} \times N_{12} = 3 \times 2 = 6Q12=M11×N12=3×2=6

这个过程暴露了一个新的性能瓶颈:为了找到矩阵 N\pmb{N}N 中特定行(例如第 kkk 行)的所有非零元,必须从头到尾扫描整个 N.data 三元组表。如果矩阵 M\pmb{M}M 有很多非零元,这个查找过程需要反复进行,效率极低。

(3)解决方案:行逻辑链接的顺序表 (RLSMatrix)

为了解决上述查找效率低下的问题,需要一种能快速定位到矩阵 N\pmb{N}N 任意一行的所有非零元的数据结构。一个巧妙的解决方案是增加一个辅助数组,称之为 rpos (row position),用来记录每一行的第一个非零元在三元组表 data 中的起始位置。将这种带有 rpos 数组的三元组表称为行逻辑链接的顺序表(Row-wise Logically Linked Sequential List)。

其数据结构定义如下:

typedef struct {Triple data[MAXSIZE + 1];  // 非零元三元组表(依然是核心)int rpos[MAXRC + 1];       // 各行第一个非零元的位置表(新增的“索引”)int mu, nu, tu;            // 矩阵的行数、列数和非零元个数
} RLSMatrix;

这里的“行逻辑链接”并非指真正的指针链表,而是通过 rpos 数组建立了行号与 data 数组下标之间的逻辑关系,实现了快速跳转。

以矩阵 N\pmb{N}N 为例,它的 rpos 数组如下表所示:

行号 row12345 (额外项)
rpos[row]12355

这个 rpos 数组的含义是:

  • rpos[1] = 1:第1行的第一个非零元在 N.data 的第1个位置。
  • rpos[2] = 2:第2行的第一个非零元在 N.data 的第2个位置。
  • rpos[3] = 3:第3行的第一个非零元在 N.data 的第3个位置。
  • rpos[4] = 5:第4行没有非零元。我们约定,如果某一行没有非零元,它的 rpos 值等于下一行非零元的起始位置。

一个非常关键的设计是,可以利用 rpos 数组轻松确定任一行的非零元区间:矩阵第 i 行的所有非零元,存储在 data 数组的 [rpos[i], rpos[i+1] - 1] 这个区间内。

  • 第1行的非零元区间:[rpos[1], rpos[2] - 1][1, 1]
  • 第2行的非零元区间:[rpos[2], rpos[3] - 1][2, 2]
  • 第3行的非零元区间:[rpos[3], rpos[4] - 1][3, 4]
  • 第4行的非零元区间:[rpos[4], rpos[5] - 1][5, 4],这是一个空区间,表示该行没有非零元。

为了让这个规则对最后一行也通用,通常会增加一个“哨兵”项 rpos[nu+1] = tu + 1。这样,就能用统一的方式处理所有行,而无需在代码中为最后一行编写特殊逻辑。

(4)高效的稀疏矩阵乘法算法

有了行逻辑链接顺序表,就可以设计出高效的乘法算法了。其核心思想是:逐行计算结果矩阵 Q\pmb{Q}Q

【算法步骤】

  1. 初始化:创建结果矩阵 Q\pmb{Q}Q,确定其维度为 m×nm \times nm×n
  2. 逐行处理:遍历矩阵 M\pmb{M}M 的每一行(设为第 arow 行)。
  3. 设立累加器:为即将计算的 Q\pmb{Q}Q 的第 arow 行,准备一个临时的累加器数组 ctemp(长度为 Q\pmb{Q}Q 的列数 n),并全部清零。
  4. 计算乘积累加:
    a. 遍历 M\pmb{M}M 在第 arow 行的所有非零元 Marow,kM_{arow, k}Marow,k
    b. 对于每一个 Marow,kM_{arow, k}Marow,k,它的列号是 k。我们利用 N.rpos 快速定位到 N\pmb{N}N 的第 k 行的所有非零元 Nk,jN_{k, j}Nk,j
    c. 将乘积 Marow,k×Nk,jM_{arow, k} \times N_{k, j}Marow,k×Nk,j 累加到 ctemp[j] 中。
  5. 压缩存储:当 M\pmb{M}M 的第 arow 行处理完毕后,ctemp 数组就完整地存储了 Q\pmb{Q}Q 的第 arow 行的最终结果。此时,遍历 ctemp 数组,将其中不为零的元素 (arow, j, ctemp[j]) 作为新的三元组存入 Q.data 中。
  6. 循环:重复步骤 2-5,直到 M\pmb{M}M 的所有行都处理完毕。

【算法描述】

Status MultSMatrix(RLSMatrix M, RLSMatrix N, RLSMatrix &Q) {// 检查矩阵乘法条件if (M.nu != N.mu) return ERROR;// 初始化结果矩阵 QQ.mu = M.mu; Q.nu = N.nu; Q.tu = 0;if (M.tu == 0 || N.tu == 0) return OK; // 如果任一矩阵为空,结果必为空// 为 N 创建 rpos 数组(假设 N 已经是以 RLSMatrix 形式传入)// (若 N 不是 RLSMatrix,则需先预处理 N,生成其 rpos 数组)// 逐行计算 Q 的元素for (arow = 1; arow <= M.mu; ++arow) {// 1. 临时累加器数组,用于存放 Q 矩阵第 arow 行的计算结果float ctemp[Q.nu + 1] = {0};// 记录 Q 中当前行的第一个非零元将要存放的位置Q.rpos[arow] = Q.tu + 1;// 2. 遍历 M 矩阵第 arow 行的所有非零元// M 第 arow 行的非零元在 M.data 中的范围是 [M.rpos[arow], M.rpos[arow+1]-1]int p_end = (arow < M.mu) ? M.rpos[arow + 1] : M.tu + 1;for (p = M.rpos[arow]; p < p_end; ++p) {int brow = M.data[p].j; // M 元素的列号,即 N 元素的行号// 3. 利用 N.rpos 快速找到 N 矩阵第 brow 行的所有非零元并相乘累加int q_end = (brow < N.mu) ? N.rpos[brow + 1] : N.tu + 1;for (q = N.rpos[brow]; q < q_end; ++q) {int ccol = N.data[q].j; // 乘积元素在 Q 中的列号ctemp[ccol] += M.data[p].e * N.data[q].e;} // for q} // for p// 4. 将 ctemp 数组中的非零元压缩存储到 Q.data 中for (ccol = 1; ccol <= Q.nu; ++ccol) {if (ctemp[ccol] != 0) {if (++Q.tu > MAXSIZE) return ERROR; // 检查容量Q.data[Q.tu] = {arow, ccol, ctemp[ccol]}; // 构造并存入三元组}} // for ccol} // for arow// 设置 rpos 的哨兵项Q.rpos[Q.mu + 1] = Q.tu + 1;return OK;
}

算法要点总结:

  • 累加后压缩:由于多个非零项相乘再累加后,结果可能恰好为零(例如 3 * 2 + (-6) * 1 = 0),所以不能在计算过程中直接生成 Q\pmb{Q}Q 的三元组。必须先用临时数组 ctemp 将一整行的结果完全计算出来,再判断哪些元素最终非零,然后进行存储。
  • 行主序优势:由于算法是逐行处理 M\pmb{M}M 的,计算出的 Q\pmb{Q}Q 的非零元也是天然按行有序的,无需后续排序。
  • 效率提升:该算法的核心优势在于,通过 N.rpos 将“在整个 N.data 中搜索特定行”这一耗时操作,变为了“直接访问 N.data 的一个连续子区间”,极大地提升了运算效率。

说明:关于稀疏矩阵的压缩方法,除了这里介绍的方法之外,还有其他方法。并且,有关稀疏矩阵的运算,在实际的开发工作中,可以使用有关的专门进行科学计算的库。这方面的详细内容,请参阅《机器学习数学基础》(齐伟,电子工业出版社)中对稀疏矩阵的专题介绍。

http://www.dtcms.com/a/430733.html

相关文章:

  • MATLAB仿真:编程基础实验全解析——从入门到实战
  • [特殊字符]灵感补给站 | pinterest 设计灵感分享 UI版面设计2
  • seo与网站建设php网站开发文档模板
  • GameObject 常见类型详解 -- 任务给予对象(QUESTGIVER)
  • docker部署mssql
  • 网站备案能查到什么wordpress关键字设置
  • LAMA(2014): 一项对SCADE模型进行基于SMT验证的开源方案
  • 从AGI到ASI演化的路径与启示
  • 重庆孝爱之家网站建设邢台是哪个省的城市
  • 【Linux学习笔记】线程概念和控制(二)
  • AES-128 CMAC:保障嵌入式通信安全的认证算法
  • Oumi:开源的AI模型一站式开发平台,涵盖训练、评估和部署模型
  • 大数据消息中间件选型终极指南:深度解析Kafka、Pulsar、RocketMQ架构与性能
  • 网站推广排名收费南昌做企业网站
  • 【Mosquitto的数据流程架构】
  • 新手学网站建设视频教程共30课高清版做网站需要编程
  • Kubernetes实战:MariaDB误删恢复与数据持久化
  • 开源 C# 快速开发(十五)进程--windows消息
  • Linux Shell 变量扩展进阶:深入理解 ${} 特殊用法
  • 04.CSS 动画效果| 仅使用 HTML 和 CSS
  • Matlab通过GUI实现点云的快速全局配准(FGR)
  • 晋城网站开发合肥网站策划
  • EfficientNet模型:高效卷积神经网络的革命性突破
  • 软件测试基础-day1
  • Linux安装centos8及基础配置
  • OpenSpeedy官网下载 - 百度网盘加速器,开箱即用的游戏变速器
  • 【MySQL】MySQL环境搭建
  • HEFrame.WpfUI :一个现代化的 开源 WPF UI库
  • Stanford CS336 Lecture3 | Architectures, hyperparameters
  • NotoSansSC-Regular.otf介绍与下载