Skip to content

特殊矩阵的压缩存储

2026 大纲 三(五)特殊矩阵的压缩存储(地址公式的地基见《多维数组的存储》)。

四类矩阵、八个公式,推法只有一条

一个 n×n 的矩阵直接存要 n2 个单元。但如果它有大量重复恒为常数的元素,这些空间就白花了——压缩存储的目标是只保留有用信息,同时仍能在 O(1) 时间内反算出任一位置的值。后半句同样要紧:如果查一个元素要遍历,压缩就失去意义了。

公式看着有八个,推法其实只有一条:k1 行(列)累加 + 行(列)内偏移。这与《多维数组的存储》那条完全一样,唯一的差别是——"每一行有几个元素"不再是常数 n。三角形的行,长度是逐行递减(或递增)的,所以"前 k1 行的总数"从乘法变成了求和。

矩阵存哪一半 / 哪些位置压缩后大小关键公式(矩阵 1-based,数组 0-based)
对称矩阵(上三角,行优先)jin(n+1)2(i1)(2ni+2)2+(ji)
对称矩阵(上三角,列优先)jin(n+1)2j(j1)2+(i1)
下三角(行优先)ijn(n+1)2(三角矩阵再 +1i(i1)2+(j1)
三角矩阵一半三角 + 1 个常数 cn(n+1)2+1同对应三角;常数区全部映射到末位 n(n+1)2
三对角矩阵|ij|13n22i+j3含边界行,全体适用
稀疏矩阵(三元组)任意稀疏、无规律3t+3t 个非零)显式存 (i,j,v)无下标公式
稀疏矩阵(十字链表)任意稀疏5t+m+n链表结点,无下标公式
🔴 查任一元素的三步① 判断它在不在被存的那个三角;② 不在就先按对称性交换下标;③ 再代公式。跳过前两步直接代,是这一节最容易犯的错
🔴 对称 vs 三角的唯一实质差别对称:另一半内容重复,查它要交换下标;三角:另一半全是常数 c,查它返回末位那一格。判据是题面说"对称"还是"其余元素均为常数/0"
🔴 三对角公式对边界行也成立第 1 行的"前缀少 1、行内多 1"正好抵消,不必单独处理
前三类 vs 稀疏矩阵前三类位置能由 i,j 算出(规律性压缩);稀疏矩阵无规律,只能把位置显式存下来
压缩的目标只保留有用信息,同时仍能在 O(1) 时间内反算出任一位置的值。后半句同样重要——查一个元素要遍历,压缩就没意义了
🔴 压缩不是无条件更优三元组要 3t+3 个单元,非零占比低于约 1/3 才开始省空间;一半元素非零时反而多花约 50%
🔴 邻接矩阵不是压缩结构它是图的稠密存储n×n 一格不省。看到"矩阵"二字就往压缩存储上套是选择题的常见错法

查任一元素,固定三步:① 判断它在不在被存的那个三角;② 不在就先按对称性交换下标;③ 再代公式。跳过前两步直接代公式,是这一节最容易犯的错。

第 ② 步怎么处理,取决于矩阵的类型——对称矩阵与三角矩阵的唯一实质差别就在这里

  • 对称矩阵:另一半的内容是重复的,所以查它要交换下标再代同一个公式;
  • 三角矩阵:另一半全是常数 c,所以查它直接返回末位那一格,根本不用算。

判据是题面写的是"对称",还是"其余元素均为常数 / 均为 0"。

全文口径:矩阵元素下标从 1 开始(mi,j),压缩数组下标从 0 开始(C 语言习惯)。 题面若是其他组合,先按本口径算出结果,最后一步再 ±1 转换,不要临场改公式—— 临场改公式是这一节的主要出错来源。两个 base 互相独立,不要混成一个。

交互可视化

顶部切换「对称 / 三角 / 三对角 / 稀疏」,点击网格任意元素查看下标推导; 也可直接输入 nij 观察前缀累加与行内偏移各占多少。

加载可视化中...
对称矩阵:两个公式的完整推导与手算验证(想会推不想背就展开)

n×n 矩阵 M 满足 mi,j=mj,i。对角线两侧互为镜像,对角线上的元素自身镜像就是自己。 只存上三角(含对角线,ji)或只存下三角ij),两种策略都要存 1+2++n=n(n+1)2 个元素,压缩数组记作 N[0n(n+1)21]

上三角 · 行优先。i 行存 mi,i,mi,i+1,,mi,n,共 ni+1 个:

text
i=1 : m11 m12 m13 ... m1n        (n 个)
i=2 :     m22 m23 ... m2n        (n-1 个)
i=3 :         m33 ... m3n        (n-2 个)
...
i=n :                     mnn    (1 个)
  • i1 行的元素总数n+(n1)++(ni+2)。 这是首项 n、末项 ni+2、共 i1 项的等差数列,和为

    (i1)[n+(ni+2)]2=(i1)(2ni+2)2
  • i 行内 mi,j 之前:从 mi,i 数到 mi,j1,共 ji 个。

idx(mi,j)=(i1)(2ni+2)2+(ji),ji

一次演算12×12 对称矩阵,上三角按行优先存入下标从 0 开始的一维数组,求 m6,6 的下标。 66,已在上三角,无需交换;代入 n=12, i=6, j=6

idx=5×(246+2)2+(66)=5×202+0=50

手算验证:前 5 行共 12+11+10+9+8=50 个元素,占 N[0]N[49]; 第 6 行的第一个元素正是 m6,6,落在 N[50]——它是数组中的第 51 个元素0-based 下标为 50 ✓。

上三角 · 列优先。j 列存 m1,j,m2,j,,mj,j,共 j 个:

text
j=1 : m11                              (1 个)
j=2 : m12 m22                          (2 个)
j=3 : m13 m23 m33                      (3 个)
...
j=n : m1n m2n m3n ... mnn              (n 个)
  • j1 列的元素总数1+2++(j1)=j(j1)2
  • j 列内 mi,j 之前:从 m1,j 数到 mi1,j,共 i1 个。
idx(mi,j)=j(j1)2+(i1),ij

一次演算10×10 对称矩阵,上三角按列优先存入下标从 0 开始的一维数组,求 m7,2 的下标。 第一步(关键):只存 ij 的部分,而 m7,2 的行号 7> 列号 2落在下三角,没有被直接存储,用对称性交换:m7,2=m2,7;确认 27 ✓; 代入 n=10, i=2, j=7

idx=7×62+(21)=21+1=22

手算验证:前 6 列共 1+2+3+4+5+6=21 个元素,占 N[0]N[20]; 第 7 列依次是 m1,7,m2,7,,分别落在 N[21],N[22],, 所以 m2,7N[22] ✓。

漏掉第一步(下标交换)是这类题的典型错法。自查动作:代入公式前, 先把 ij 比一次大小,确认它满足被存三角的条件。

行优先与列优先的对照(题面会明确写出用哪种):

行优先(存上三角)列优先(存上三角)
k 行/列的长度nk+1递减k递增
k1 行/列总长(i1)(2ni+2)2j(j1)2
行内/列内偏移jii1

不必机械记忆两套公式——理解"k1 行(列)总长 + 当前行(列)内偏移"的拆分逻辑, 现场推一遍不到一分钟,而且不会记混。

三角矩阵:公式、常数位,以及与对称矩阵的并排对照(想彻底分清两者就展开)

上三角矩阵:所有 i>j(下三角部分)的元素都等于同一个常数 c(多半是 0)。

text
* * * *
0 * * *      ← 下三角全是常数 c(这里 c = 0)
0 0 * *
0 0 0 *

下三角矩阵:所有 i<j(上三角部分)的元素都等于同一个常数 c

压缩策略:存所有非常数部分的元素共 n(n+1)2 个,再额外存 1 个常数 c 放在数组末尾, 总空间 n(n+1)2+1

上三角矩阵(非常数部分是 ji,按行优先)——与对称矩阵上三角行优先完全相同

idx(mi,j)=(i1)(2ni+2)2+(ji),ji

常数 c 单独存在末尾位置 N[n(n+1)2], 即所有 i>j 的元素都映射到这一个下标。

下三角矩阵(非常数部分是 ij,按行优先):第 i 行有 i 个元素(mi,1mi,i), 前 i1 行共 1+2++(i1)=i(i1)2 个,行内偏移共 j1 个,故

idx(mi,j)=i(i1)2+(j1),ji

这也是严蔚敏教材给出的经典形式(式 4-14 的下三角情形)。

下三角 · 列优先(对偶推法,供对照):第 j 列有 nj+1 个元素, 前 j1 列共 (j1)(2nj+2)2 个,列内偏移 ij,故 idx=(j1)(2nj+2)2+(ij)—— 与"上三角行优先"互为对偶,再次说明四个公式只是同一推法的四种代入。

并排对照:同样是 n=8、同样问 m2,7、同样存下三角按行优先。

三角矩阵(上三角元素全为常数 c):7>2m2,7 落在上三角——那里全是常数 c不单独存,统一指向数组末尾那一格:idx=n(n+1)2=8×92=36。 数组总长度 36+1=37,其中 N[0]N[35] 存下三角的 36 个元素,N[36] 存常数 c ✓。 另外 m5,335 在被存区域,idx=5×42+(31)=12; 验证前 4 行共 1+2+3+4=10 个占 N[0]N[9],第 5 行依次占 N[10]N[14]m5,3N[12] ✓。

对称矩阵7>2m2,7 在上三角没有被存储,用对称性交换 → m2,7=m7,2, 确认 27 ✓,代入下三角行优先公式 idx=7×62+(21)=22

对称矩阵答案是 22,三角矩阵答案是 36。区别只在于"另一半是重复内容还是常数"。 三角矩阵的 m2,7m7,2 并不相等(前者是常数 c,后者是有效数据), 对它交换下标会得到完全错误的答案。审题时必须先确定矩阵类型,再决定是交换下标还是指向常数位。

三对角矩阵:一个公式管到底

只有满足 |ij|1 的元素可能非零,即主对角线 + 上副对角线 + 下副对角线三条带。

三对角矩阵的非零元素分布:只有主对角线 aᵢᵢ 及其紧邻的上副对角线 aᵢ,ᵢ₊₁ 与下副对角线 aᵢ,ᵢ₋₁ 上可能非零,其余位置全为 0;第一行和最后一行各只有 2 个非零元素

图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图4.3 三对角矩阵,p140

每行的非零元素数:第 1 行是 m1,1,m1,22 个m1,0 不存在); 第 2n1 行是 mi,i1,mi,i,mi,i+13 个; 第 n 行是 mn,n1,mn,n2 个。总非零数 2+3(n2)+2=3n2

行优先下标公式。 先看中间行(2in1)。第 i 行之前一共存了:

2第 1 行+3(i2)第 2 至 i1 行=3i4

i 行内的三个元素按 j=i1, i, i+1 排列,行内位置(从 0 编号)分别是 0、1、2, 所以行内偏移 =ji+1。于是

idx(mi,j)=(3i4)+(ji+1)=2i+j3,|ij|1

这个公式对第 1 行和第 n 行同样成立,不需要单独处理——理由值得说清楚:

  • 第 1 行3i-4 代入 i=11,比真实值 0 少 1;而行内偏移 ji+1=j, 比真实的 j1 多 1两处偏差正好抵消,结果仍然正确。 验证:m1,12+13=0 ✓,m1,22+23=1 ✓。
  • n3i-4 代入 i=n3n4,正是前 n1 行的真实总数 2+3(n2)=3n4 ✓;行内偏移 ji+1j=n1,n 分别给出 0、1 ✓,与实际一致。 验证(n=5):m5,410+43=11m5,510+53=12, 而总数 3×52=13,下标范围 012 ✓。

n=5 的完整对照表(可用来复核任何记忆):

k0123456789101112
mi,j1,11,22,12,22,33,23,33,44,34,44,55,45,5

逐个代入 2i+j3 核对,13 个全部吻合。

逆映射:已知 k(i,j)k=2i+j3j{i1,i,i+1}k{3i4, 3i3, 3i2},于是 k+1{3i3, 3i2, 3i1}, 因此 k+13=i1。故

i=k+13+1,j=k2i+3

验证k=87i=88/3+1=29+1=30j=8760+3=30,即 m30,30k=5i=6/3+1=3j=56+3=2,即 m3,2 ✓(对照上表)。 记成不带 +1 的版本是常见错法,用 k=0m1,1 代一次即可自查。

一次正向演算:100 阶三对角矩阵,mi,j(1i,j100) 按行优先压缩存入下标从 0 开始 的一维数组 Nm30,30 的下标 =2×30+303=87手算验证:第 30 行之前共有 2+3×28=86 个元素,占 N[0]N[85]; 第 30 行的三个元素 m30,29,m30,30,m30,31 依次占 N[86],N[87],N[88] ✓。

两个容易算错的地方:① 第 1 行只有 2 个元素,若把所有行都按 3 个算, 前缀和会得到 3×29=87 而不是正确的 86,最终答案偏移 1; ② 别把"主对角线 i=j"当成行内的第一个——m30,30 在第 30 行的中间(行内位置 1), 第一个是 m30,29。行内偏移 ji+1 里的那个 +1 就是为此而来。

稀疏矩阵:三元组表、十字链表,以及"什么时候才划算"(要写结构定义或做选型时展开)

非零元素个数 t 远小于 m×n,且非零元素分布无规律没有正式的稀疏度阈值——一般认为 t/(mn)<0.05 就算稀疏, 但题面只要说"稀疏矩阵"就按稀疏处理,不必纠结临界值。

严蔚敏教材在讲完对称、三角、对角三种特殊矩阵后指出: "在实际应用中还经常会遇到另一类矩阵,其非零元较零元少,且分布没有一定规律,称之为稀疏矩阵。 这类矩阵的压缩存储就要比特殊矩阵复杂。"——"无规律"正是它与前三类的分界线

三元组表:对每个非零元素 mi,j0 存一个三元组 (i, j, mi,j), 所有三元组连续存放,通常按行优先顺序排列。

c
typedef struct {
    int i, j;        // 非零元素的行号、列号
    int value;       // 元素值
} Triple;

typedef struct {
    int rows;        // 原矩阵 M 的行数
    int cols;        // 原矩阵 M 的列数
    int nums;        // 非零元素个数
    Triple data[MAXSIZE];
} TSMatrix;

辅助信息为什么必须是"原矩阵的行数和列数",而不是"包含非零元素的行数/列数"? 用反例:一个 5×5 矩阵只有 m1,1=1 一个非零元素,三元组只有 (1, 1, 1), 非零行数 =1、非零列数 =1;若只存这两个数,重建出来的是一个 1×1 矩阵, 原矩阵那 4 行 4 列的零元素信息全部丢失——形状错了。

三元组的设计原则:辅助信息要做到两件事——① 能完整恢复原矩阵的形状; ② 能完整恢复所有非零元素。三元组负责第二件,行数与列数负责第一件, 两者合起来无冗余、无缺失

什么时候才划算:三元组把"1 个值"换成了"3 个值"。设每个整数占 1 个单元, 朴素存储 mn 个单元,三元组表 3t+3 个单元(3 个辅助量),划算的条件是

3t+3<mnt<mn33mn3
非零个数 t100×100 矩阵,朴素 10 000 单元)三元组 3t+3是否划算
100(占 1%)303划算,省 97%
1 000(占 10%)3 003划算,省 70%
3 333(占 33%)10 002持平
5 000(占 50%)15 003反而多花 50%

三对角矩阵是一个对照:它的非零个数 3n2 相对 n2 极少,但因为位置有规律, 用 3n2 个单元就够了,连行列号都不用存——有规律时永远比三元组省。

十字链表适合频繁修改(增删非零元素)的稀疏矩阵:

c
typedef struct OLNode {
    int row, col;            // 行号、列号
    int value;               // 元素值
    struct OLNode *right;    // 指向同一行内下一个非零元素
    struct OLNode *down;     // 指向同一列内下一个非零元素
} OLNode;

每个结点同时挂在所在行链表列链表上——所以叫"十字"。 一个 m×n 稀疏矩阵有 m 个行头指针 + n 个列头指针。 遍历某一行沿 right 走一遍、某一列沿 down 走一遍;插入新的非零元素在两条链上各找位置后插入, 不需要移动其他元素。劣势是每个结点有 5 个域,单元素开销明显大于三元组。

三元组表十字链表
单个非零元素的开销小(i, j, value 三个值)大(再多 2 个指针)
插入 / 删除慢(数组要移位,O(t)快(O(行长+列长)
按行遍历快(本来就按行序存)快(沿 right
按列遍历慢(要扫全表)快(沿 down
适用场景静态稀疏矩阵频繁修改的稀疏矩阵

两者都是稀疏矩阵的压缩存储结构,常被拿来与两个"看着像但不是"的结构对比:

结构是不是稀疏矩阵的压缩存储它到底是什么
三元组表稀疏矩阵专用
十字链表稀疏矩阵专用(行/列双向遍历)
邻接矩阵的稠密存储,n×n 全存,0 也照常占位置
二叉链表二叉树结点的 lchild / rchild 链接结构

邻接矩阵的命名容易误导:它的"矩阵"二字让人以为和稀疏矩阵有关。 实际上它就是一个普通的 n×n 二维数组,完全不压缩。它本身正是"稀疏矩阵压缩" 要解决的对象之一——稀疏图的邻接矩阵用三元组或十字链表存才高效(图那一章的 邻接表十字链表 就是这个思路)。

为什么要压缩:三组量化数字(想看清收益有多大就展开)

对于 n×n 的矩阵,朴素存储要 n2 个单元。当矩阵具有某种规律性 (很多元素为 0、很多元素对称重复)时,朴素存储是浪费的:

  • 1000×1000 对称矩阵:朴素要 106 个单元,有用信息只有上三角 5×105 个——浪费一半
  • 1000×1000 三对角矩阵:朴素要 106 个单元,非零的只有 3n23×103 个——浪费 99.7%
  • 1000×1000 稀疏矩阵(非零元素 100 个):浪费 99.99%

严蔚敏教材对压缩存储的表述是:"为多个值相同的元只分配一个存储空间,对零元不分配空间。 假若值相同的元素或者零元素在矩阵中的分布有一定规律,则称此类矩阵为特殊矩阵。"

考点速记

三条会被反复调用的结论:

  1. 推法只有一条:前 k1 行(列)累加 + 行(列)内偏移。八个公式都是它在不同形状下的实例。
  2. 查元素固定三步:判断在不在被存的三角 → 不在就交换下标(对称)或直接取末位(三角)→ 代公式。
  3. 压缩不是无条件更优:三元组要 3t+3 个单元,非零占比低于约 1/3 才开始省空间

这一节在真题里被考过的形式(下方「真题练习」逐题对应):绝大多数是同一道题的变形——给定矩阵类型、存储方向和一个元素,问它在一维数组中的下标。要盯住题面里的四个变量:

  • 存的是上三角还是下三角(题面会写 1ijn 之类的范围);
  • 按行优先还是按列优先
  • 待查元素在不在被存的那一半——若不在,先按对称性交换下标。比如 10 阶对称矩阵只存上三角,问 m7,27>2 不在上三角,先换成 m2,7 再算;
  • 数组下标从 0 还是从 1 开始

三对角矩阵是另一种问法,用 2i+j3 直接代即可,边界行也适用,不必单独处理。此外还考过两道概念题:适合压缩存储稀疏矩阵的两种结构是三元组表和十字链表;以及三元组表除三元组外还必须存矩阵的行数与列数(不是"含非零元素的行数/列数"——那还原不出原矩阵的形状)。

易错元素不在被存的三角,却直接代公式。 对称矩阵要先交换下标,三角矩阵要直接取末位常数格。

易错行优先与列优先的累加对象搞混。 上三角按行存时第 i 行有 ni+1 个元素;按列存时第 j 列有 j 个元素。累加的是哪个,取决于存储方向。

易错两个 base 混成一个。 矩阵下标的 base 与数组下标的 base 互相独立,先按熟悉的口径算完,最后一步再 ±1 转换,别临场改公式。

易错看到"矩阵"就往压缩上套。 邻接矩阵是图的稠密存储,n×n 一格不省,不是压缩结构。

教材出处
  • 压缩存储的定义("为多个值相同的元只分配一个存储空间,对零元不分配空间")、 特殊矩阵的定义、对称矩阵的压缩存储(存下三角,一维数组 sa[n(n+1)/2], 式 4-14 给出 sa[k]aij 的一一对应)、三角矩阵的定义与"再加一个存储常数 c 的存储空间":严蔚敏《数据结构(C 语言版)》(第 2 版)p101「4.4.3 特殊矩阵的压缩存储」
  • 上三角矩阵的下标式 4-16、对角矩阵(三对角矩阵) 的定义与图 4.13, 以及稀疏矩阵的定义("其非零元较零元少,且分布没有一定规律"):同书 p102
  • 行优先 / 列优先的地址公式与 n 维映像函数(本篇一切压缩公式的地基):同书 p99–p100
  • 图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图4.3 三对角矩阵,p140

说明:严蔚敏《数据结构(C 语言版)》(第 2 版)在给出稀疏矩阵的定义后即写明 "这类矩阵的压缩存储就要比特殊矩阵复杂,在此不做讨论", 因此本篇三元组表与十字链表两节没有该书页码可引,其结构定义与优劣对比由本篇自行整理, 不附会页码。

相关知识

多维数组的存储(本篇所有压缩公式的地基,务必先看)| 顺序表(压缩存储的落脚点仍是一维顺序存储)| 邻接矩阵(图的稠密存储,不是压缩结构)| 邻接表十字链表(同样的"只存存在的边"思想)| 数组与特殊矩阵的压缩存储(拆分前的旧合并视图)

真题练习