第9章 数组与广义表:存储结构与压缩技巧
数组和广义表,这两个概念看起来简单,但实际用起来门道不少。我在做嵌入式系统开发时,就曾因为数组存储顺序搞反,导致整个图像数据错位——那画面,简直像毕加索的画。今天咱们就把这块彻底理清楚。
9.1 数组的存储结构
数组在内存里怎么放?说白了就两种方式:行优先和列优先。你想想看,二维数组在物理上是一维的,总得有个规则把逻辑位置映射到物理地址。
9.1.1 行优先存储
C语言用的就是行优先。什么意思?就是先存完第一行所有元素,再存第二行。比如一个3×4的数组:
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
内存中的顺序是:1,2,3,4,5,6,7,8,9,10,11,12。
地址计算公式:
Loc(a[i][j]) = Loc(a[0][0]) + (i * n + j) * sizeof(element)
其中n是列数。这个公式我建议你背下来,面试经常考。
9.1.2 列优先存储
Fortran和MATLAB用的是列优先。先存第一列,再存第二列。同样的数组,列优先顺序是:1,5,9,2,6,10,3,7,11,4,8,12。
地址公式:
Loc(a[i][j]) = Loc(a[0][0]) + (j * m + i) * sizeof(element)
m是行数。
9.2 特殊矩阵的压缩存储
实际项目中,很多矩阵是特殊的。比如对称矩阵、三角矩阵、稀疏矩阵。如果按普通二维数组存,浪费空间不说,还慢。我做过一个有限元分析的项目,矩阵规模10万×10万,不压缩根本存不下。
9.2.1 对称矩阵
对称矩阵满足a[i][j] = a[j][i]。我们只需要存一半,比如下三角部分(包括对角线)。
压缩为一维数组后,下标映射公式:
k = i*(i+1)/2 + j (i ≥ j)
举个例子,3×3对称矩阵:
原始矩阵:
1 2 3
2 4 5
3 5 6
压缩后一维数组:[1, 2, 4, 3, 5, 6]
对应位置:k0, k1, k2, k3, k4, k5
9.2.2 三角矩阵
上三角矩阵:对角线以下全为0。下三角矩阵:对角线以上全为0。存储方式类似对称矩阵,但只存非零部分。
下三角矩阵(含对角线)的压缩公式:
k = i*(i+1)/2 + j (i ≥ j)
上三角矩阵的压缩公式:
k = i*(2n - i - 1)/2 + j (i ≤ j)
嗯,这里要注意:上三角的公式稍微复杂点,我建议你手推一遍,理解比死记硬背重要。
9.2.3 稀疏矩阵
稀疏矩阵的非零元素很少,比如5%以下。这时候用三元组存储最合适。
三元组结构:
typedef struct {
int row; // 行号
int col; // 列号
int value; // 非零元素值
} Triple;
typedef struct {
Triple data[MAXSIZE]; // 三元组表
int rows; // 总行数
int cols; // 总列数
int nums; // 非零元素个数
} SparseMatrix;
举个例子:
原始稀疏矩阵:
0 0 3 0
0 5 0 0
0 0 0 7
三元组表示:
(0,2,3)
(1,1,5)
(2,3,7)
9.3 广义表
广义表是线性表的推广。普通线性表的元素都是原子,广义表的元素可以是子表。说白了,就是可以嵌套。
9.3.1 广义表的定义
广义表记作:LS = (a1, a2, ..., an)
其中ai可以是原子,也可以是子表。比如:
A = () // 空表
B = (a, b) // 只有原子
C = (a, (b, c)) // 包含子表
D = (A, B, C) // 嵌套广义表
9.3.2 广义表的存储结构
我习惯用链式存储。每个节点要么是原子节点,要么是子表节点。
typedef enum { ATOM, LIST } NodeType;
typedef struct GLNode {
NodeType tag; // 标志域:ATOM或LIST
union {
char atom; // 原子值
struct {
struct GLNode *hp; // 表头指针
struct GLNode *tp; // 表尾指针
} ptr;
} data;
} GLNode;
这种结构叫头尾链表存储法。每个子表节点有两个指针:hp指向表头,tp指向表尾。
9.3.3 广义表的基本操作
常用的操作有:
- 取表头:GetHead(L) —— 返回第一个元素
- 取表尾:GetTail(L) —— 返回除第一个元素外的子表
- 求深度:递归计算嵌套层数
- 求长度:最外层元素个数
举个例子:
L = (a, (b, c, d), e)
GetHead(L) = a
GetTail(L) = ((b, c, d), e)
深度 = 2
长度 = 3
9.4 知识体系总览
下面这张图把本章的核心知识点串起来了:
9.5 实战建议
学完这些理论,我建议你动手做几件事:
- 手写一个稀疏矩阵乘法 —— 用三元组实现,别用二维数组。做完你就知道压缩存储的威力了。
- 实现广义表的深度递归计算 —— 递归是广义表操作的核心,写一遍能加深理解。
- 对比行优先和列优先的缓存性能 —— 写个测试程序,用大数组跑一下,看看时间差多少。
数组和广义表这块,说白了就是「怎么把数据放好、找得快」。理解了存储的本质,后面学树、图这些复杂结构就轻松多了。
公众号:蓝海资料掘金营,微信deep3321