如果让我给软考上午题的易错点排个名,稀疏矩阵一定在我这里排到前三。这倒不是因为它背后的算法有多难,而是因为这个考点的出题角度特别刁钻:表面在考“矩阵”,实际考的是数组存储、下标换算、逻辑结构判断,甚至还能一头扎进图的存储结构里。我第一次做相关真题时,已经看完了一遍“三元组”的知识点,结果还是一口气错三道,整个人都裂开了。
今天这篇是软考备战系列的第十一篇,专门把稀疏矩阵这件事掰开揉碎讲清楚。主要面向软件设计师中级、程序员和数据库系统工程师里要考数据结构的朋友,尤其是那种“看书能看懂,一上真题就发蒙”的备考状态。文章不会只罗列概念,我会从考点定位、存储结构、典型题型和复习节奏几个角度展开,把我踩过的坑和验证过的做题方法一起放出来,希望帮你少走一段弯路。
1. 软考数据结构里的稀疏矩阵:到底在考什么
1.1 从大纲看稀疏矩阵的真实位置
软考中级软件设计师的大纲里,“数据结构与算法”是一个绕不开的大模块。这个模块下面包含线性表、树、图、排序、查找,再往下还有数组和矩阵压缩存储。很多人复习时会把注意力全部放在二叉树和图,或者排序算法上,稀疏矩阵通常只是作为“数组”章节里的一个小节出现。可恰恰是这个小节,历年真题出现的频率并不低,而且它一旦和图的邻接矩阵串起来考,迷惑性就会明显上升。
如果我们把软考中与稀疏矩阵相关的知识点拆开看,大致可以归纳为以下几种考察方向:
- 稀疏矩阵的定义和稀疏因子,要求判断哪种矩阵适合用稀疏矩阵方式存储;
- 二维数组按行优先或列优先存储时的地址计算,以及一维压缩存储后的下标换算;
- 三元组顺序表的结构、排序特点以及转置算法的流程;
- 十字链表结点的基本结构及其适用场景;
- 特殊矩阵与稀疏矩阵的区分,比如对称矩阵、三角矩阵、对角矩阵这类有规则分布的非零元;
- 稀疏矩阵在图的存储结构选择中的影响。
所以复习时不能只盯着“稀疏矩阵”这四个字,实际上它是数组、矩阵、图这几章之间的一个连接器。我见过不少备考者只背了三元组的定义就去刷题,遇到稍微绕一点的对称矩阵压缩题就乱了,原因就在于没有把这个考点放在一个更大的知识网络里。
1.2 “稀疏”不等于“零很多”:这个概念先掰清楚
教材里通常会说:如果矩阵中非零元素的个数远远小于矩阵元素的总数,而且非零元素的分布没有规律,我们一般称它为稀疏矩阵。这里的“稀疏”是一个经验指标,所以很多资料会提到稀疏因子,也就是矩阵中非零元素个数与矩阵总元素个数的比值。当这个比值小于等于0.05时,通常认为有必要用稀疏矩阵方式存储。
但是软考真正容易挖坑的地方不在比值,而在“分布没有规律”这半句话。这一点必须展开说。
如果矩阵里零元素确实很多,但非零元素的分布非常有规则,比如对称矩阵、三对角矩阵、下三角矩阵,它们虽然有大量零元素,却因为分布规则,可以使用更专门化的压缩方式。它们一般不叫稀疏矩阵,而叫特殊矩阵。这里面的区别在存储上很明显:特殊矩阵因为位置是确定的,压缩存储时可以不用保存行列号,只要保存数值即可;稀疏矩阵由于非零元随机散布,压缩存储时通常必须同时保存元素所在的行、列和值。
举个例子你就明白了。一个100乘100的矩阵,如果只有100个非零元,而且这些元素全部集中在对角线附近,那它就是三对角矩阵,压缩时开一个长度为3n-2的一维数组就够;如果这100个非零元随机分布在上千个可能的位置里,那就是典型的稀疏矩阵,直接用一个二维数组存10000个元素太浪费,存成三元组表才合适。
做题时我建议你先判断两个问题:第一,非零元占比是不是很低;第二,非零元的分布是不是没有明显规律。只要第二个条件不满足,就不能按稀疏矩阵的方式套用解题思路。这个区分在软考上午题里几乎年年都有可能踩到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 稀疏矩阵存储方案:三元组和十字链表怎么选
2.1 三元组顺序表:最可能出现在选择题里的存储
既然稀疏矩阵的非零元分布没有规律,那就不能像普通矩阵那样用“行号乘列数”的固定公式去定位每个元素,而是需要一种更灵活的表达方式。三元组就是最经典的一种思路。
所谓三元组,是指用一个结构体来记录一个非零元素的行号、列号和值。如果用C语言描述,大概是这个样子:
c复制typedef struct {
int row; // 行号
int col; // 列号
ElementType value; // 元素值
} Triple;
然后我们再把多个三元组按行优先顺序存到一个数组里,同时记录矩阵的总行数、总列数和非零元素个数,就构成了一个三元组顺序表:
c复制typedef struct {
Triple data[MAXSIZE + 1]; // data[0]未用或用于特殊情况
int mu, nu, tu; // 矩阵行数、列数、非零元个数
} TSMatrix;
软考中常见的形式是下标从1开始,也就是data[1]到data[tu]存储非零元。原因很简单——很多教材为了后面写快速转置时方便统计位置,专门空出data[0],让数组下标和位序一一对应。
用三元组表存储稀疏矩阵后,最明显的收益是空间节省。一个1000乘1000的矩阵,如果用普通二维数组存,会有100万个存储单元;如果非零元只有100个,三元组表只需要100个三元组外加几个辅助变量。这样空间占用就从百万级别降到了百级别,可以说非常夸张。
不过任何事情都有代价。三元组表失去了随机存取的能力。在一个普通二维数组里,你想访问第i行第j列的元素,直接通过下标运算就可以;但在三元组表里,你需要从头遍历,查找某个位置的元素是否存在,最坏情况下要走完整个表。这个特点在做转置问题时显得尤其明显,所以下一节我会专门讲转置。
2.2 十字链表:应对矩阵运算的另一种选择
三元组顺序表虽然紧凑,但它有一个问题:当矩阵需要频繁进行插入、删除操作,或者运行加法、乘法时,顺序表里移动元素的开销会非常大。比如原本按行优先排好的三元组,要插入一个新的非零元,这个插入位置可能要让很多后续元素后移。软考虽然不一定在下午题里让你完整实现稀疏矩阵加法,但在关于存储结构选择的题目里,十字链表经常作为一个正确答案出现。
十字链表可以理解为把每条行链表和每条列链表交叉在一起。每个非零结点除了保存行号、列号、值之外,还有两个指针,一个指向同一行的下一个非零结点,另一个指向同一列的下一个非零结点。也就是这样:
c复制typedef struct OLNode {
int row, col;
ElementType value;
struct OLNode *right, *down;
} OLNode, *OList;
从名字就能看出来,right指针沿水平方向把同一行串起来,down指针沿垂直方向把同一列串起来。这样一来,行方向上的遍历和列方向上的遍历都可以高效完成。矩阵转置时,不需要移动数据,只需要交换行号与列号,再把矩阵的行列信息对调,这在结构上要比三元组顺序表方便得多。
软考中关于十字链表的直接考法通常是给出一张结点示意图,问某个新插入的非零元到底要接在哪个指针后面;或者给你一个十字链表的结构描述,问你它的空间复杂度。遇到这类题,我的建议是把图画出来,画完就能看出right和down分别连到哪。还有一个小细节:有些题目会考“十字链表适合用于矩阵的加法、乘法等操作”,原因不是十字链表本身算得快,而是它避免了顺序表插入删除时的元素移动。
在实际备考里,你不需要把十字链表的完整建立流程背得很深,但必须能看懂它的结构定义,知道它有行指针和列指针,并能说明它跟三元组顺序表的适用范围差异。软考上午题考到这一步通常就已经足够了。
3. 三类高频题型:下标换算、转置和规则矩阵判断
3.1 数组地址计算和压缩后的下标换算
在稀疏矩阵题里,有一类题目看起来像“计算题”,本质上却是二维数组的存储地址计算。比如给你矩阵大小、每个元素占字节数,问按行优先存储时某个元素的地址;或者把一个特殊矩阵压缩成一维数组,反推某个元素在新数组中的下标。这类题最大的陷阱是数组下标的下界到底从0开始还是从1开始。
先回忆一下二维数组的地址公式。假如有一个m行n列的二维数组,以行优先方式存储,每个元素占size个存储单元,数组基地址是Loc(a00),那么aij的地址就是:
Loc(aij) = Loc(a00) + (i * n + j) * size
这个公式的基础是下标从0开始。如果软考题目说矩阵元素从a11开始编号,那就要调整成:
Loc(aij) = Loc(a11) + ((i - 1) * n + (j - 1)) * size
别小看这个区别,历年真题里有很多人不是不会算,而是算之前没有顺手判断下标起点。在稀疏矩阵话题下,真正更常考的是特殊矩阵压缩后的下标换算。我举一个对称矩阵的例子,这是最常见的考法。
一个n阶对称矩阵A,它的特点是aij等于aji。因为对称,所以只需要存上三角或下三角,就能还原整个矩阵。如果存下三角,即i大于等于j的部分,按行优先存入一维数组B,那么元素aij在一维数组中的位置可以通过累计前i-1行的元素个数再加上本行中它前面的元素个数来算。前i-1行元素总数是1+2+...+(i-1),也就是i*(i-1)/2;本行中aij前有j-1个元素,所以总下标是i*(i-1)/2加上j-1。这个结果还要不要加1,取决于B数组下标起点。
我知道很多朋友看到公式就头大。这里我提供一个更稳妥的方法:与其硬背多个公式,不如在草稿纸上画一个4阶或5阶的小矩阵,手动写出压缩后的一维数组顺序,再根据题目已知条件反推。因为软考这类题一般不会把阶数设得特别大,算出来以后还可以代入验证。这个方法听起来笨,但在考场紧张状态下反而错误率低。
3.2 三元组转置:从暴力扫描到快速转置
三元组表的转置是软考数据结构里一个很经典的算法考点。普通矩阵的转置很简单,就是把aij变成aji;但三元组表存储的非零元原本是按行优先排列的,如果只是简单地把每个三元组的行号和列号交换,得到的新三元组表很可能不再按行优先排列,因此需要额外的排序或重排逻辑。
最朴素的转置思路是这样:对于原矩阵的每一列(即转置后的每一行),依次扫描原三元组表,把所有列号等于当前列的三元组取出来,交换行列号后放入新表。这样做的时间复杂度是原矩阵列数乘以非零元个数,属于O(nu * tu)。如果非零元个数接近矩阵元素总数,这个算法甚至比普通二维数组转置还要慢。
软考更喜欢考的,是另一种“快速转置”的思路。原理是分两步:第一步先统计原矩阵每一列有多少个非零元,第二步利用这些统计结果推算每一列第一个非零元在转置后三元组表中应该存放的位置,然后一次扫描原三元组表,把每个元素直接放到它该去的位置。
快速转置的代码用C语言写出来也不是很长,逻辑是下面这样的:
c复制void FastTransposeTSMatrix(TSMatrix A, TSMatrix *B) {
int num[A.nu + 1], cpot[A.nu + 1], col, t;
B->mu = A.nu;
B->nu = A.mu;
B->tu = A.tu;
if (B->tu == 0) return;
for (col = 1; col <= A.nu; col++)
num[col] = 0;
for (t = 1; t <= A.tu; t++)
num[A.data[t].col]++;
cpot[1] = 1;
for (col = 2; col <= A.nu; col++)
cpot[col] = cpot[col - 1] + num[col - 1];
for (t = 1; t <= A.tu; t++) {
col = A.data[t].col;
int q = cpot[col];
B->data[q].row = A.data[t].col;
B->data[q].col = A.data[t].row;
B->data[q].value = A.data[t].value;
cpot[col]++;
}
}
这里cpot数组的引入是关键。cpot[col]表示原矩阵第col列的第一个非零元在转置后应存放的位置,初始时等于前col-1列非零元个数之和加1。每处理完一个元素,cpot[col]就加1,这样同一列的元素会按顺序依次放好。
软考对这个算法的考察一般不会要求你完整默写,但会问你时间复杂度和基本思路。快速转置的时间复杂度是O(nu + tu),空间上需要两个辅助数组,所以空间复杂度也是O(nu)。对比朴素转置,它的优点是把双重扫描变成单次扫描加统计,这一点值得在答案里写清楚。
3.3 特殊矩阵压缩判定:别把特例当成稀疏矩阵处理
我在前面已经提到过稀疏矩阵和特殊矩阵的区别。软考特别喜欢把这两类矩阵放在同一道选择题里,让你判断某一种说法是否正确。比如给你四个矩阵:对称矩阵、三对角矩阵、随机分布大量零元素的矩阵、单位矩阵,问哪个更适合用三元组表存储。很多人看到“零元素多”就把单位矩阵和对称矩阵也算进去,结果正好做错。
单位矩阵虽然有大量零元素,但它的非零元只在对角线上,分布规则得不能再规则,压缩时只需要存n个对角线元素就行,根本不需要行号列号。对称矩阵也是这个道理,因为aij和aji相等,最多只需要存n(n+1)/2个元素。反而是“随机分布大量零元素的矩阵”更符合稀疏矩阵的条件,用三元组表或十字链表才合适。
因此我在刷题时养成一个习惯:看到矩阵存储类的题,先在题干上圈出“分布规律”或“随机”这样的词。如果题目本身没有明确说分布是否随机,那就要看它描述非零元的方式。没有固定规律可言的,才是稀疏矩阵的适用对象;有某种对角线或三角规律可循的,应优先考虑按特殊矩阵压缩。
另外一个容易出错的地方是“对角矩阵”。严格来说,如果非零元只出现在主对角线附近,并且带宽固定,那它可以通过数组按对角线压缩。但如果带宽很宽,以至于非零元数量虽然远小于n平方却分布在对角线附近的多个位置,那个矩阵依然可以看作有规律分布的带状矩阵,不是教科书意义上的稀疏矩阵。考试中遇到这样的题,判断标准依然是那两个:占比是否小,分布是否无规律。
4. 从稀疏矩阵跳到图与算法题:软考喜欢的关联考法
4.1 为什么图里会再碰一次稀疏矩阵
软考把数据结构分成多个章节考察,但题目经常不会只考单一知识点。图的存储就是一个典型的例子,因为图的邻接矩阵本质上就是一个矩阵。如果一个图有n个顶点,那么它的邻接矩阵就是一个n乘n的方阵。当n比较大而边数相对较少时,邻接矩阵中会存在大量的零元素,形成一个稀疏矩阵。
考到这里,复习过稀疏矩阵的优势就体现出来了:你不会只知道“邻接矩阵空间是n平方”,而是会想起,当边数远小于n平方时,可以用邻接表来替代邻接矩阵。邻接表可以看作一种针对“图的稀疏性”设计的压缩存储方案。它只保存存在的边,类似三元组只保存非零元;它也保留了顶点之间的邻居关系,类似十字链表把每行每列串起来。
软考上午题经常这样出:一个图有500个顶点,边只有300条,问采用邻接矩阵和邻接表分别需要的存储空间大致是多少,或者问深度优先遍历在这种图上的复杂度表现。如果你对稀疏性敏感,就会很快判断出邻接矩阵会有大量空间浪费,邻接表才是更合适的选择。这个结论表面上是图的知识,但底层的判断逻辑跟稀疏矩阵完全一致。
4.2 数据结构设计里的“稀疏性思维”
从备考角度,我建议你不仅仅是背几个稀疏矩阵的结论,而是把“稀疏性思维”当成一种通用能力来理解。软考下午题里偶有涉及带权图、最短路径或拓扑排序,这类题目默认你已经知道:当输入规模很大、数据联系很稀疏时,算法实现往往不能简单依赖二维数组。
举个例子,如果题目要求对一个大V、边E的比较稀疏的图做深度优先遍历,你用邻接矩阵也能做通,但时间复杂度可能达到O(V平方)。换用邻接表以后,遍历所有顶点的邻接点只需要遍历所有边,时间复杂度降到O(V+E)。这个提升,本质上就是把一个稀疏矩阵问题转换成了更合理的存储结构问题。
这也就是为什么我说复习稀疏矩阵时,千万不要只记住存储形式本身。理解它的核心思想,也就是“数据稀疏时,就别为不存在的东西分配大量存储”,会让你在图算法、甚至部分数据库索引相关的考题中更有手感。软考并不要求你成为算法专家,它更看重的是你能不能根据数据特征选择合适的逻辑结构和存储结构。这种选择能力,靠的正是平时对数组、矩阵、图交叉知识点的积累。
5. 我的备考节奏调整:把稀疏矩阵复习放进整个软考计划
5.1 三轮复习中这一步应该放在哪里
我刚开始备考软件设计师时,是按照“教材从前到后”的顺序复习的,结果到了稀疏矩阵这一小节,总觉得前面的树和图更重要,于是草草看了看三元组就跳过去。后来做真题才发现,这部分虽然分值不大,但很容易造成连锁扣分,尤其是数组下标换算那种小题,错了很影响心态。
如果让我重新安排,我会把稀疏矩阵放在数据结构第一轮复习的中后段,也就是树和图之前。因为图存储要用到矩阵压缩和邻接表思想,先理解稀疏和压缩,后面看邻接表会更快。第二轮复习时,再专门把稀疏矩阵和图的存储放在一起做一次专题练习,重点观察它们之间的共同点。第三轮主要靠做真题查漏,不再单独看教材,而是把做错的相关题目整理到一个错题本里,标注错误原因。
5.2 刷题App和真题的配合使用建议
现在很多人习惯用刷题App利用碎片时间复习。这个方法本身不错,但用在稀疏矩阵上需要小心。刷题App好处是能快速刷大量选择题,但它的题目往往把单个知识点的题目反复出,容易出现“你会背答案但不会做新题”的情况。我的做法是:第一遍用刷题App过知识点,每道题都强迫自己写出理由,而不是直接猜;第二遍开始做纸质历年真题,按考试时间计时。
具体操作上,我会把稀疏矩阵相关题目分成三类:概念判断类、计算类、算法流程类。概念判断类适合用App白天零碎刷,计算类必须动笔算,算法流程类则需要拿出草稿纸模拟一遍执行过程。比如快速转置里cpot数组的变化,不亲手推一遍,只看代码是记不牢的。
5.3 几个我在实战里踩过的坑
第一个坑是坐标起点问题。很多教材在讲二维数组时从0开始,讲三元组时又从1开始,做题时被我混在一起用,结果每道题的答案都差一个常数。后来我在草稿纸右上角写大一个提示字:起点。是0还是1,先确认再下笔。
第二个坑是转置后行号列号有没有交换。三元组转置的代码逻辑不复杂,但手忙脚乱时容易把B->data[q].row写成A.data[t].row。所以我现在做题,会在看完题后先在图上标出原矩阵的一个具体元素,比如原第2行第5列的非零元,转置后应该变成第5行第2列,用具体例子带一遍,代码或者流程题基本就不会错。
第三个坑是忽略空间复杂度。软考不光考算法对不对,还经常考辅助空间。快速转置和朴素转置在时间复杂度上差别很大,在空间复杂度上也有区别。有的题目会问“在稀疏矩阵的十字链表中插入一个新结点的时间复杂度”,你如果还在想顺序表的移动,很容易选错。实际上链式结构只需要修改指针,时间复杂度是O(1)级别,前提是你已经定位到了插入位置。
第四个坑是复习时太偏科。因为我是奔着软件设计师中级去的,一开始只刷算法和程序题,后来发现上午题里数据结构的比重很稳定,稀疏矩阵这种看起来不起眼的小章节,正是上午题用来拉分的点。考场上大部分人都能搞定排序树图,却可能在矩阵压缩上丢分。把这些小点吃透,反而能帮你建立一些优势。
说到最后,如果你正处于软考备考的中间阶段,我的建议是别小看稀疏矩阵,也别被它吓住。它不像红黑树那样复杂,也不像动态规划那样烧脑,它更像一个“数据结构的十字路口”——把数组、矩阵、图的存储核心串在了一起。把这个路口摸清楚,后面很多图算法题都会顺很多。准备一份草稿纸,手动画出第一次三元组转置的cpot变化,这一篇的知识点就真正长在你脑子里了。
