一、稀疏矩阵
1. 基本概念
稀疏矩阵 是指矩阵中大多数元素为零的矩阵。相反,稠密矩阵是指大多数元素非零的矩阵。
2. 稀疏性判断标准
通常认为,当非零元素数量占总元素数量的比例低于某个阈值(如5%)时,可以认为是稀疏矩阵。
3. 现实中的例子
密集矩阵示例(6×6,36个元素,21个非零): 1 2 0 0 0 0 3 4 5 0 0 0 0 6 7 8 0 0 0 0 9 10 11 0 0 0 0 12 13 14 0 0 0 0 15 16 非零比例:21/36 ≈ 58% → 稠密矩阵 稀疏矩阵示例(6×6,36个元素,仅6个非零): 0 0 0 0 0 0 0 5 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 9 0 0 0 非零比例:6/36 ≈ 17% → 稀疏矩阵
二、为什么需要特殊存储?
1. 传统二维数组的问题
// 6×6的矩阵,只有6个非零元素
int matrix[6][6] = {
{0, 0, 0, 0, 0, 0},
{0, 5, 0, 0, 0, 0},
{0, 0, 0, 0, 8, 0},
{0, 0, 0, 0, 0, 0},
{3, 0, 0, 0, 0, 0},
{0, 0, 9, 0, 0, 0}
};
问题:
浪费大量内存存储0值
遍历效率低(需要检查所有36个位置)
运算时对0值进行无意义计算
2. 解决方案:只存储非零元素
三、三元组表示法
1. 三元组的定义
每个非零元素用三个值表示:
行号(row):元素在矩阵中的行位置
列号(col):元素在矩阵中的列位置
值(value):元素的实际值
2. 数据结构设计
// 单个非零元素的三元组
typedef struct {
int row; // 行号(从0开始)
int col; // 列号(从0开始)
int value; // 元素值
} Triple;
// 稀疏矩阵的完整表示
typedef struct {
Triple *data; // 三元组数组
int rows; // 原矩阵总行数
int cols; // 原矩阵总列数
int num; // 非零元素个数
int capacity; // 数组容量
} SparseMatrix;
3. 存储示例
原始矩阵(6×6): 0 0 0 0 0 0 0 5 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 9 0 0 0 三元组表示: 索引 row col value 0 1 1 5 ← 第1行第1列是5 1 2 4 8 ← 第2行第4列是8 2 4 0 3 ← 第4行第0列是3 3 5 2 9 ← 第5行第2列是9 存储空间对比: 传统数组:6×6×4 = 144字节(假设int占4字节) 三元组:4个元素×3×4 = 48字节(节省66%)
四、代码实例
1. 基本操作实现
#include <stdio.h>
#include <stdlib.h>
// 三元组结构
typedef struct {
int row;
int col;
int value;
} Triple;
// 稀疏矩阵结构
typedef struct {
Triple *data;
int rows;
int cols;
int num; // 当前非零元素个数
int capacity; // 数组容量
} SparseMatrix;
// 创建稀疏矩阵
SparseMatrix* createSparseMatrix(int rows, int cols, int capacity) {
SparseMatrix *sm = (SparseMatrix*)malloc(sizeof(SparseMatrix));
sm->data = (Triple*)malloc(sizeof(Triple) * capacity);
sm->rows = rows;
sm->cols = cols;
sm->num = 0;
sm->capacity = capacity;
return sm;
}
// 销毁稀疏矩阵
void destroySparseMatrix(SparseMatrix *sm) {
if (sm) {
free(sm->data);
free(sm);
}
}
// 插入非零元素(保持行主序)
int insertElement(SparseMatrix *sm, int row, int col, int value) {
if (row < 0 || row >= sm->rows || col < 0 || col >= sm->cols) {
printf("位置(%d,%d)超出范围\n", row, col);
return 0;
}
if (value == 0) {
printf("注意:值为0的元素不应存储在三元组中\n");
return 0;
}
if (sm->num >= sm->capacity) {
printf("容量不足,无法插入新元素\n");
return 0;
}
// 查找插入位置(保持行主序:行号小的在前,同行时列号小的在前)
int i;
for (i = sm->num - 1; i >= 0; i--) {
if (sm->data[i].row > row ||
(sm->data[i].row == row && sm->data[i].col > col)) {
// 后移元素
sm->data[i+1] = sm->data[i];
} else {
break;
}
}
// 插入新元素
sm->data[i+1].row = row;
sm->data[i+1].col = col;
sm->data[i+1].value = value;
sm->num++;
return 1;
}
// 打印稀疏矩阵(三元组形式)
void printSparseMatrix(SparseMatrix *sm) {
printf("稀疏矩阵 %d×%d,有%d个非零元素:\n",
sm->rows, sm->cols, sm->num);
printf("索引\t行\t列\t值\n");
printf("------------------------\n");
for (int i = 0; i < sm->num; i++) {
printf("%d\t%d\t%d\t%d\n",
i, sm->data[i].row, sm->data[i].col, sm->data[i].value);
}
}
// 将三元组还原为完整矩阵并打印
void printAsFullMatrix(SparseMatrix *sm) {
printf("\n完整矩阵形式(%d×%d):\n", sm->rows, sm->cols);
// 创建临时矩阵并初始化为0
int **matrix = (int**)malloc(sizeof(int*) * sm->rows);
for (int i = 0; i < sm->rows; i++) {
matrix[i] = (int*)calloc(sm->cols, sizeof(int));
}
// 填充非零值
for (int i = 0; i < sm->num; i++) {
int r = sm->data[i].row;
int c = sm->data[i].col;
matrix[r][c] = sm->data[i].value;
}
// 打印矩阵
for (int i = 0; i < sm->rows; i++) {
for (int j = 0; j < sm->cols; j++) {
printf("%3d ", matrix[i][j]);
}
printf("\n");
}
// 释放内存
for (int i = 0; i < sm->rows; i++) {
free(matrix[i]);
}
free(matrix);
}
2. 转置操作
// 普通转置算法(简单但效率低)
SparseMatrix* transposeSimple(SparseMatrix *sm) {
// 创建转置矩阵(行列互换)
SparseMatrix *result = createSparseMatrix(sm->cols, sm->rows, sm->num);
result->num = sm->num;
int index = 0;
// 按列扫描原矩阵
for (int col = 0; col < sm->cols; col++) {
for (int i = 0; i < sm->num; i++) {
if (sm->data[i].col == col) {
// 找到列号为col的元素,进行转置
result->data[index].row = sm->data[i].col; // 原列变行
result->data[index].col = sm->data[i].row; // 原行变列
result->data[index].value = sm->data[i].value;
index++;
}
}
}
return result;
}
// 快速转置算法(重点掌握!)
SparseMatrix* fastTranspose(SparseMatrix *sm) {
// 创建转置矩阵
SparseMatrix *result = createSparseMatrix(sm->cols, sm->rows, sm->num);
result->num = sm->num;
if (sm->num == 0) {
return result; // 空矩阵直接返回
}
// 步骤1:统计原矩阵每列的非零元素个数
int *colCount = (int*)calloc(sm->cols, sizeof(int));
for (int i = 0; i < sm->num; i++) {
colCount[sm->data[i].col]++;
}
// 步骤2:计算转置后每行(对应原矩阵的列)的起始位置
int *rowStart = (int*)malloc(sizeof(int) * sm->cols);
rowStart[0] = 0;
for (int i = 1; i < sm->cols; i++) {
rowStart[i] = rowStart[i-1] + colCount[i-1];
}
// 步骤3:执行转置
for (int i = 0; i < sm->num; i++) {
int col = sm->data[i].col; // 原矩阵的列号
int pos = rowStart[col]; // 在转置矩阵中的位置
result->data[pos].row = sm->data[i].col; // 行列互换
result->data[pos].col = sm->data[i].row;
result->data[pos].value = sm->data[i].value;
rowStart[col]++; // 该行的下一个位置
}
free(colCount);
free(rowStart);
return result;
}
五、算法复杂度分析
1. 普通转置算法
// 时间复杂度分析
for (int col = 0; col < sm->cols; col++) { // 循环 cols 次
for (int i = 0; i < sm->num; i++) { // 每次循环 num 次
// 检查每个元素
}
}
时间复杂度:O(cols × num)
适合:非零元素非常少的情况
缺点:当列数很大时效率低
2. 快速转置算法
时间复杂度:O(cols + num)
统计每列非零元素数:O(num)
计算起始位置:O(cols)
执行转置:O(num)
空间复杂度:O(cols)(需要额外的两个数组)
六、优缺点总结
优点:
节省存储空间:只存储非零元素
提高运算效率:避免对零元素的无意义运算
适合大型稀疏矩阵:内存消耗大大减少
缺点:
随机访问慢:需要顺序查找特定位置的元素
插入/删除复杂:需要维护有序性
不适合密集矩阵:存储开销反而更大