一、为什么要压缩存储
1.1 问题引入
一个 n × n 的矩阵,如果用二维数组存储,需要 n² 个空间。但当矩阵有以下特征时,存在大量浪费:
-
对称矩阵 :
a[i][j] = a[j][i],只有一半元素是独立的 -
三角矩阵:一半区域全是常数(通常是0)
-
稀疏矩阵:非零元素极少(通常 < 5%)
压缩存储的核心:只存储独立或不重复的元素,通过映射函数将二维下标转换为一维下标。
二、对称矩阵的压缩存储
2.1 定义
若 n 阶矩阵满足 a[i][j] = a[j][i],称为对称矩阵。只需存储下三角(包括对角线)即可。
下三角元素个数:n(n+1)/2
2.2 映射公式
将下三角元素按行优先存放到一维数组 B 中:
text
位置映射:k = i*(i-1)/2 + j-1 (假设i,j从1开始)
或从0开始:
text
k = i*(i+1)/2 + j (i≥j,下三角区域)
示例:3阶对称矩阵
text
原始: 压缩存储(下三角):
[1 2 3] [1]
[2 4 5] → [2 4]
[3 5 6] [3 5 6]
2.3 代码实现
c
#include <stdio.h>
#include <stdlib.h>
// 对称矩阵压缩存储
typedef struct {
int *data; // 一维数组
int n; // 阶数
} SymmetricMatrix;
// 初始化
void initSymmetric(SymmetricMatrix *m, int n) {
m->n = n;
int size = n * (n + 1) / 2;
m->data = (int*)calloc(size, sizeof(int));
}
// 计算一维下标(i,j从0开始)
int getIndex(int i, int j) {
if (i < j) { // 交换为上三角
int temp = i;
i = j;
j = temp;
}
// 现在 i >= j,存储在下三角
return i * (i + 1) / 2 + j;
}
// 设置元素
void setSym(SymmetricMatrix *m, int i, int j, int val) {
int idx = getIndex(i, j);
m->data[idx] = val;
}
// 获取元素
int getSym(SymmetricMatrix *m, int i, int j) {
int idx = getIndex(i, j);
return m->data[idx];
}
// 打印矩阵
void printSym(SymmetricMatrix *m) {
for (int i = 0; i < m->n; i++) {
for (int j = 0; j < m->n; j++) {
printf("%4d", getSym(m, i, j));
}
printf("\n");
}
}
// 释放内存
void destroySym(SymmetricMatrix *m) {
free(m->data);
m->data = NULL;
}
int main() {
SymmetricMatrix m;
initSymmetric(&m, 3);
// 设置下三角
setSym(&m, 0, 0, 1);
setSym(&m, 1, 0, 2);
setSym(&m, 1, 1, 4);
setSym(&m, 2, 0, 3);
setSym(&m, 2, 1, 5);
setSym(&m, 2, 2, 6);
printf("对称矩阵:\n");
printSym(&m);
destroySym(&m);
return 0;
}
运行结果:
text
对称矩阵:
1 2 3
2 4 5
3 5 6
三、三角矩阵的压缩存储
3.1 定义
-
上三角矩阵:下三角区域(不含对角线)全是常数c(通常是0)
-
下三角矩阵:上三角区域全是常数c
压缩存储时,只需存储三角区域 + 一个常数。
3.2 映射公式(下三角矩阵)
下三角区域(含对角线)按行优先存储,外加一个位置存常数。
text
下三角元素:k = i*(i+1)/2 + j (i≥j)
常数位置:k = n*(n+1)/2
3.3 代码实现
c
// 下三角矩阵压缩存储
typedef struct {
int *data;
int n;
int constant; // 上三角区域的常数
} LowerTriMatrix;
void initLower(LowerTriMatrix *m, int n, int constant) {
m->n = n;
m->constant = constant;
int size = n * (n + 1) / 2 + 1; // 多一个位置存常数
m->data = (int*)calloc(size, sizeof(int));
}
int getLower(LowerTriMatrix *m, int i, int j) {
if (i >= j) {
int idx = i * (i + 1) / 2 + j;
return m->data[idx];
} else {
return m->constant;
}
}
四、稀疏矩阵的三元组存储
4.1 三元组表示法
稀疏矩阵只存储非零元素,每个元素用三元组 (row, col, value) 表示。
text
矩阵:
[0 0 3]
[0 5 0]
[0 0 0]
三元组表:
(0,2,3)
(1,1,5)
4.2 结构定义
c
#define MAX_TERMS 100 // 最大非零元个数
typedef struct {
int row; // 行号
int col; // 列号
int value; // 值
} Triple;
typedef struct {
Triple data[MAX_TERMS]; // 三元组表
int rows; // 矩阵行数
int cols; // 矩阵列数
int terms; // 非零元个数
} SparseMatrix;
4.3 创建稀疏矩阵
c
void createSparse(SparseMatrix *m, int rows, int cols) {
m->rows = rows;
m->cols = cols;
m->terms = 0;
}
void addElement(SparseMatrix *m, int row, int col, int value) {
if (m->terms >= MAX_TERMS) {
printf("三元组表已满\n");
return;
}
if (value == 0) return; // 不存储零
m->data[m->terms].row = row;
m->data[m->terms].col = col;
m->data[m->terms].value = value;
m->terms++;
}
4.4 稀疏矩阵转置
普通转置:遍历列,将行和列交换。
c
SparseMatrix transpose(SparseMatrix *m) {
SparseMatrix result;
createSparse(&result, m->cols, m->rows);
// 按列扫描原矩阵,按行存入结果
for (int col = 0; col < m->cols; col++) {
for (int i = 0; i < m->terms; i++) {
if (m->data[i].col == col) {
addElement(&result, m->data[i].col, m->data[i].row, m->data[i].value);
}
}
}
return result;
}
时间复杂度:O(terms × cols),如果cols很大,效率低。
4.5 快速转置(推荐)
核心思想:先统计每列的非零元个数,再计算每列在结果中的起始位置,然后一次遍历完成转置。
c
SparseMatrix fastTranspose(SparseMatrix *m) {
SparseMatrix result;
createSparse(&result, m->cols, m->rows);
result.terms = m->terms;
if (m->terms == 0) return result;
int *colSize = (int*)calloc(m->cols, sizeof(int)); // 每列非零元个数
int *colStart = (int*)malloc(m->cols * sizeof(int)); // 每列起始位置
// 1. 统计每列非零元个数
for (int i = 0; i < m->terms; i++) {
colSize[m->data[i].col]++;
}
// 2. 计算每列起始位置
colStart[0] = 0;
for (int i = 1; i < m->cols; i++) {
colStart[i] = colStart[i-1] + colSize[i-1];
}
// 3. 转置
for (int i = 0; i < m->terms; i++) {
int col = m->data[i].col;
int pos = colStart[col];
result.data[pos].row = m->data[i].col;
result.data[pos].col = m->data[i].row;
result.data[pos].value = m->data[i].value;
colStart[col]++; // 指向下一个位置
}
free(colSize);
free(colStart);
return result;
}
时间复杂度:O(terms + cols),远优于普通转置。
4.6 完整代码
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_TERMS 100
typedef struct {
int row, col, value;
} Triple;
typedef struct {
Triple data[MAX_TERMS];
int rows, cols, terms;
} SparseMatrix;
void createSparse(SparseMatrix *m, int rows, int cols) {
m->rows = rows;
m->cols = cols;
m->terms = 0;
}
void addElement(SparseMatrix *m, int row, int col, int value) {
if (m->terms >= MAX_TERMS) return;
if (value == 0) return;
m->data[m->terms].row = row;
m->data[m->terms].col = col;
m->data[m->terms].value = value;
m->terms++;
}
void printSparse(SparseMatrix *m) {
printf("矩阵 %dx%d,非零元个数: %d\n", m->rows, m->cols, m->terms);
printf("行\t列\t值\n");
for (int i = 0; i < m->terms; i++) {
printf("%d\t%d\t%d\n", m->data[i].row, m->data[i].col, m->data[i].value);
}
}
void printMatrix(SparseMatrix *m) {
int idx = 0;
for (int i = 0; i < m->rows; i++) {
for (int j = 0; j < m->cols; j++) {
if (idx < m->terms && m->data[idx].row == i && m->data[idx].col == j) {
printf("%4d", m->data[idx].value);
idx++;
} else {
printf("%4d", 0);
}
}
printf("\n");
}
}
SparseMatrix fastTranspose(SparseMatrix *m) {
SparseMatrix result;
createSparse(&result, m->cols, m->rows);
result.terms = m->terms;
if (m->terms == 0) return result;
int *colSize = (int*)calloc(m->cols, sizeof(int));
int *colStart = (int*)malloc(m->cols * sizeof(int));
for (int i = 0; i < m->terms; i++) {
colSize[m->data[i].col]++;
}
colStart[0] = 0;
for (int i = 1; i < m->cols; i++) {
colStart[i] = colStart[i-1] + colSize[i-1];
}
for (int i = 0; i < m->terms; i++) {
int col = m->data[i].col;
int pos = colStart[col];
result.data[pos].row = m->data[i].col;
result.data[pos].col = m->data[i].row;
result.data[pos].value = m->data[i].value;
colStart[col]++;
}
free(colSize);
free(colStart);
return result;
}
int main() {
SparseMatrix m;
createSparse(&m, 4, 5);
addElement(&m, 0, 2, 3);
addElement(&m, 1, 1, 5);
addElement(&m, 2, 4, 7);
addElement(&m, 3, 0, 9);
printf("原矩阵:\n");
printMatrix(&m);
printf("\n三元组表:\n");
printSparse(&m);
SparseMatrix trans = fastTranspose(&m);
printf("\n转置后的矩阵:\n");
printMatrix(&trans);
printf("\n转置后的三元组表:\n");
printSparse(&trans);
return 0;
}
运行结果:
text
原矩阵:
0 0 3 0 0
0 5 0 0 0
0 0 0 0 7
9 0 0 0 0
三元组表:
矩阵 4x5,非零元个数: 4
行 列 值
0 2 3
1 1 5
2 4 7
3 0 9
转置后的矩阵:
0 0 0 9
0 5 0 0
3 0 0 0
0 0 0 0
0 0 7 0
转置后的三元组表:
矩阵 5x4,非零元个数: 4
行 列 值
0 3 9
1 1 5
2 0 3
4 2 7
五、存储方式对比
| 矩阵类型 | 存储空间 | 映射复杂度 | 适用场景 |
|---|---|---|---|
| 普通矩阵 | n² | O(1) | 稠密矩阵 |
| 对称矩阵 | n(n+1)/2 | O(1) | 距离矩阵、邻接矩阵 |
| 三角矩阵 | n(n+1)/2+1 | O(1) | 线性方程组求解 |
| 稀疏矩阵 | 3×terms | O(1)访问(需查找) | 大规模稀疏数据 |
六、小结
这一篇我们学习了特殊矩阵的压缩存储:
| 矩阵类型 | 压缩策略 | 核心要点 |
|---|---|---|
| 对称矩阵 | 只存下三角 | 映射公式:k = i(i+1)/2 + j |
| 三角矩阵 | 存三角区域+常数 | 多一个位置存常数 |
| 稀疏矩阵 | 三元组(row,col,val) | 快速转置用colSize+colStart |
稀疏矩阵快速转置的核心:
-
统计每列非零元个数
-
计算每列起始位置
-
一次遍历完成转置
下一篇我们讲树形结构,从二叉树开始。
七、思考题
-
上三角矩阵的压缩存储公式是什么?尝试推导一下。
-
对称矩阵的压缩存储中,如果矩阵是0-based索引,映射公式应该如何修改?
-
稀疏矩阵的快速转置算法中,为什么要用colStart数组?它解决了什么问题?
-
如果稀疏矩阵的行数远大于列数(或相反),普通转置和快速转置的效率差异会更明显吗?
欢迎在评论区讨论你的答案。