【数据结构与算法】第18篇:数组的压缩存储:对称矩阵、三角矩阵与稀疏矩阵

一、为什么要压缩存储

1.1 问题引入

一个 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

五、存储方式对比

矩阵类型 存储空间 映射复杂度 适用场景
普通矩阵 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

稀疏矩阵快速转置的核心

  1. 统计每列非零元个数

  2. 计算每列起始位置

  3. 一次遍历完成转置

下一篇我们讲树形结构,从二叉树开始。


七、思考题

  1. 上三角矩阵的压缩存储公式是什么?尝试推导一下。

  2. 对称矩阵的压缩存储中,如果矩阵是0-based索引,映射公式应该如何修改?

  3. 稀疏矩阵的快速转置算法中,为什么要用colStart数组?它解决了什么问题?

  4. 如果稀疏矩阵的行数远大于列数(或相反),普通转置和快速转置的效率差异会更明显吗?

欢迎在评论区讨论你的答案。

相关推荐
今儿敲了吗2 小时前
51| 八皇后
c++·笔记·学习·算法·深度优先
华科易迅2 小时前
MybatisPlus乐观锁
java·开发语言·mybatis
适应规律2 小时前
强化学习笔记(赵世钰)
笔记·线性代数·概率论
Omics Pro2 小时前
端到端单细胞空间组学数据分析
大数据·数据库·人工智能·算法·数据挖掘·数据分析·aigc
迈巴赫车主2 小时前
错位排序算法
开发语言·数据结构·算法·排序算法
maverick_1112 小时前
【FPGA】关于两个数相加的“坑”
c语言·matlab·fpga开发
玖釉-2 小时前
暴力美学与极致性能:深度解析 Meshoptimizer 的 Sloppy 减面算法
c++·windows·图形渲染
炽烈小老头2 小时前
【每日天学习一点算法 2026/03/31】不同路径
学习·算法
Darkwanderor2 小时前
搜索优化——迭代加深dfs
c++·算法·深度优先·迭代加深