实际应用中会有很多利用高阶矩阵的问题,有的高阶矩阵已达到几十万阶,几千亿个元素。然而,多数的高阶矩阵中包含了大量的数值为零的元素,需要对这类矩阵进行压缩存储。因为合理的压缩存储不仅能有效地节省存储空间,而且可以避免进行大量的零值元素参加的运算。压缩存储即为多个相同的非零元素只分配一个存储空间,对零元素不分配空间。
对于矩阵分为两类,假若值相同的元素或者零元素在矩阵中的分布有一定的规律,则称它为特殊形状矩阵;否则称为随机稀疏矩阵。
未必所有的矩阵都可以压缩,要看其是否具备以上压缩条件
特殊形状矩阵的存储表示
特殊形状矩阵主要指的是三角形矩阵(方阵的上或下三角全为零)和带状矩阵(只有主对角线附近的若干条对角线含有非零元)。
对于对称矩阵的压缩,只保留主对角线上和主对角线以下的数据元素,这样存储可以将存储规模减少至原来一半。实现以B[n(n+1)/2] (按行序为主序)存放对称矩阵的下三角(包括对角线)中的元素,其中B[k]存放aij,可以得到 i , j 与 k 之间的转换公式。如图:
同样的方式可以用来解决三角形矩阵的存储压缩问题。
另一类特殊矩阵是带状矩阵,该种矩阵的所有数据元素都集中在以主对角线为中心的三条对角线上。其余位置为0,所以可以将其压缩到一维数组B[3n-2]之中.其中范围条件是 | i - j | <= 1;
下标转换公式为: k = 2i + j - 3 ;如下图:
随机稀疏矩阵的压缩存储
在 m * n 的矩阵中,有 t 个非零元素,则称 t / ( m * n) 表示矩阵的稀疏因子,通常认为稀疏因子不超过0.05的矩阵为稀疏矩阵。表示矩阵中的非零元素很少,0元素很多。存储这样的稀疏矩阵,可以定义一个新的数据结构名为三元组。三元组中的数据为矩阵中某个数据的行列下标和本身的数据值。用这三个数据可以唯一确定矩阵中某个数据元素。所以可以按行为主序将矩阵中的数据一次转成三元组。对于所有的三元组,有着不同的存储方法。
1.三元组顺序表
将所有的三元组以顺序存储结构存放在线性表中,则可以得到稀疏矩阵的一种存储压缩表示方法,称为三元组顺序表。
//-------稀疏矩阵的三元组顺序表存储表示--------
const int MAXSIZE = 1000; //假设非零元的个数最大值为1000
typedef struct {
int i, j; //该非零元的行下标和列下标
int e; //该非零元的元素值
}Triple;
typedef struct {
Triple data[MAXSIZE + 1]; //非零元三元组表,data[0]未用
int mu, nu, tu; //非零元的行数,列数和个数
}TSMatrix;
下面通过对稀疏矩阵的创建输出(包括以矩阵形式输出和以三元组表形式输出)以及通过矩阵M求其转置矩阵T等的操作来检验该种压缩方法的可操作性。
以下为稀疏矩阵的创建和输出
#include<stdio.h>
//-------稀疏矩阵的三元组顺序表存储表示--------
const int MAXSIZE = 1000; //假设非零元的个数最大值为1000
typedef struct {
int i, j; //该非零元的行下标和列下标
int e; //该非零元的元素值
}Triple;
typedef struct {
Triple data[MAXSIZE + 1]; //非零元三元组表,data[0]未用
int mu, nu, tu; //非零元的行数,列数和个数
}TSMatrix;
int CreateSMatrix(TSMatrix &M)
{ // 创建稀疏矩阵M
int i, m, n; //这里的 i 表示循环变量;m n 用于记录用户输入的非零元素的位置
int e; //用于接收输入的数据元素
int k; //k 用于判断输入的非零元素是否合理
printf("请输入矩阵的行数,列数,非零元素数:\n");
scanf("%d", &M.mu);
scanf("%d", &M.nu);
scanf("%d", &M.tu);
if (M.tu > MAXSIZE) //非零数大于最大范围,返回0;
return 0;
M.data[0].i = 0; //为后面的顺序比较进行铺垫
// 不能些下面的三行代码,因为后面判断输入非零值顺序时需要利用最初的data[0]中的数据
//M.data[0].i = M.mu; //三元组表的第一个元素data[0]用于存储基本信息 M.data[0].i 表示矩阵的行数
//M.data[0].j = M.nu; //M.data[0].j 表示矩阵的列数
//M.data[0].e = M.tu; //M.data[0].e 表示矩阵中非零元的个数
for (i = 1; i <= M.tu; i++)
{
do
{
printf("请按行序顺序输入第%d个非零元素所在的行(1~%d),列(1~%d),元素值:\n", i, M.mu, M.nu);
scanf("%d", &m);
scanf("%d", &n);
scanf("%d", &e);
k = 0;
if (m<1 || m>M.mu || n<1 || n>M.nu) // 行或列超出范围
{
printf("行或列超出范围!\n");
k = 1;
}
//如果输入的行数小于上一次输入的行数 或者 输入的行数等于上次输入的行数但是输入的列数小于上次输入的列数
if (m < M.data[i - 1].i || m == M.data[i - 1].i&&n <= M.data[i - 1].j) // 行或列的顺序有错
{
printf("行或列的顺序有错!\n");
k = 1;
}
} while (k);
M.data[i].i = m;
M.data[i].j = n;
M.data[i].e = e;
}
return 1;
}
void PrintSMatrix1(TSMatrix M)
{ // 按矩阵形式输出M
int i, j, k = 1;
Triple *p = M.data;
printf("矩阵形式输出如下:\n");
p++; // p指向第1个非零元素
for (i = 1; i <= M.mu; i++)
{
for (j = 1; j <= M.nu; j++)
if (k <= M.tu&&p->i == i && p->j == j) // p指向非零元,且p所指元素为当前处理元素
{
printf("%3d",p->e); // 输出p所指元素的值
p++; // p指向下一个元素
k++; // 计数器+1
}
else // p所指元素不是当前处理元素
printf("%3d",0); // 输出0
printf("\n");
}
}
void PrintSMatrix(TSMatrix M)
{ // 输出稀疏矩阵M
int i;
printf("稀疏矩阵的基本信息为:%d行%d列%d个非零元素 输出结果如下\n", M.mu, M.nu, M.tu);
printf("行 列 元素值\n");
for (i = 1; i <= M.tu; i++)
printf("%2d%4d%6d\n", M.data[i].i, M.data[i].j, M.data[i].e);
}
void main()
{
TSMatrix M;
CreateSMatrix(M);
PrintSMatrix1(M);
PrintSMatrix(M);
}
对于创建稀疏矩阵时,就是循环输入每一个三元组的各项数值。不过要注意的是,三元组表M的第一个数据 M.data[0] 不能用来存放矩阵的行数列数非零元素数等基本信息,因为后面输入时,要通过M.data[0]的各项进行比较,如果输入数值后,会导致后面的比较顺序出错。建立稀疏矩阵的流程根建立链表相似,都是在主函数中先 TSMatrix M; 定义三位组表变量,再调用建立函数对其逐个赋值。
以下为由矩阵M求其转置矩阵T的操作
两种转置的方法,一种是在M表里按照列的顺序开始扫描,然后将行列之互换在插入到T表之中。例如,在M表里先扫描得到(3,1,-3),互换后得到(1,3,-3),将其插入到T表之中。然后再继续扫描得到(6,1,15),互换后得到(1,6,15),继续将其插入到T表之中。同理,扫描列数为2,3,4.....等等。最后全部插入后完成转置。这样思路简单但是效率不高。因此,衍生出一种效率更高的算法,只用一次扫描。
快速转置:先求的M矩阵中每列有多少个非零元素,然后在转换时就可以直接将对应的三元组插入到T中相应的位置。
#include<stdio.h>
//-------稀疏矩阵的三元组顺序表存储表示--------
const int MAXSIZE = 1000; //假设非零元的个数最大值为1000
typedef struct {
int i, j; //该非零元的行下标和列下标
int e; //该非零元的元素值
}Triple;
typedef struct {
Triple data[MAXSIZE + 1]; //非零元三元组表,data[0]未用
int mu, nu, tu; //非零元的行数,列数和个数
}TSMatrix;
int CreateSMatrix(TSMatrix &M)
{ // 创建稀疏矩阵M
int i, m, n; //这里的 i 表示循环变量;m n 用于记录用户输入的非零元素的位置
int e; //用于接收输入的数据元素
int k; //k 用于判断输入的非零元素是否合理
printf("请输入矩阵的行数,列数,非零元素数:\n");
scanf("%d", &M.mu);
scanf("%d", &M.nu);
scanf("%d", &M.tu);
if (M.tu > MAXSIZE) //非零数大于最大范围,返回0;
return 0;
M.data[0].i = 0; //为后面的顺序比较进行铺垫
// 不能些下面的三行代码,因为后面判断输入非零值顺序时需要利用最初的data[0]中的数据
//M.data[0].i = M.mu; //三元组表的第一个元素data[0]用于存储基本信息 M.data[0].i 表示矩阵的行数
//M.data[0].j = M.nu; //M.data[0].j 表示矩阵的列数
//M.data[0].e = M.tu; //M.data[0].e 表示矩阵中非零元的个数
for (i = 1; i <= M.tu; i++)
{
do
{
printf("请按行序顺序输入第%d个非零元素所在的行(1~%d),列(1~%d),元素值:\n", i, M.mu, M.nu);
scanf("%d", &m);
scanf("%d", &n);
scanf("%d", &e);
k = 0;
if (m<1 || m>M.mu || n<1 || n>M.nu) // 行或列超出范围
{
printf("行或列超出范围!\n");
k = 1;
}
//如果输入的行数小于上一次输入的行数 或者 输入的行数等于上次输入的行数但是输入的列数小于上次输入的列数
if (m < M.data[i - 1].i || m == M.data[i - 1].i&&n <= M.data[i - 1].j) // 行或列的顺序有错
{
printf("行或列的顺序有错!\n");
k = 1;
}
} while (k);
M.data[i].i = m;
M.data[i].j = n;
M.data[i].e = e;
}
return 1;
}
void PrintSMatrix1(TSMatrix M)
{ // 按矩阵形式输出M
int i, j, k = 1;
Triple *p = M.data;
printf("矩阵形式输出如下:\n");
p++; // p指向第1个非零元素
for (i = 1; i <= M.mu; i++)
{
for (j = 1; j <= M.nu; j++)
if (k <= M.tu&&p->i == i && p->j == j) // p指向非零元,且p所指元素为当前处理元素
{
printf("%3d",p->e); // 输出p所指元素的值
p++; // p指向下一个元素
k++; // 计数器+1
}
else // p所指元素不是当前处理元素
printf("%3d",0); // 输出0
printf("\n");
}
}
void PrintSMatrix(TSMatrix M)
{ // 输出稀疏矩阵M
int i;
printf("稀疏矩阵的基本信息为:%d行%d列%d个非零元素 输出结果如下\n", M.mu, M.nu, M.tu);
printf("行 列 元素值\n");
for (i = 1; i <= M.tu; i++)
printf("%2d%4d%6d\n", M.data[i].i, M.data[i].j, M.data[i].e);
}
const int MAXMN = 100; //矩阵行或列的最大值为 maxmn + 1
int num[MAXMN], rpos[MAXMN]; //分别用来存放M中每一列的非零元素数量,和每一列中第一个非零元在T中的起始位置
void createRpos(TSMatrix M)
{
//求M中每一列的第一个非零元在T.data 中的起始序号
int col; //用来表示列的序号
int t; //循环变量 用于遍历M的三元组表
for (col = 1; col <= M.nu; col++)
num[col] = 0; //先给数组清零
for (t = 1; t <= M.tu; t++)
num[M.data[t].j]++; //通过遍历求得每一列的非零元个数
rpos[1] = 1; //M中第一列的第一个非零元在T中的位置肯定是1
for (col = 2; col <= M.nu; col++)
rpos[col] = rpos[col - 1] + num[col - 1]; //第i列的第一个非零元素的位置是第 i-1 列第一个元素位置加上第 i-1 列元素个数
}
int FastTransposeSMatrix(TSMatrix M, TSMatrix &T)
{
//采用三元组顺序表存储表示,求稀疏矩阵M的转置矩阵T
int p, q; //p用于表示M表中的第几行,q表示对应的位置(即转置后T表的第几行)
int col;
T.mu = M.nu;
T.nu = M.mu;
T.tu = M.tu;
if (T.tu) //如果非零元素个数不为零,则开始转置
{
createRpos(M);
for (p = 1; p <= M.tu; p++) //转置矩阵元素
{
col = M.data[p].j; //读取M的三元组表中第一个三元组 data 的列数
q = rpos[col]; //T中第col行的非零元
T.data[q].i = M.data[p].j;
T.data[q].j = M.data[p].i;
T.data[q].e = M.data[p].e;
rpos[col]++; //同一行的下一个非零元素的位置应增1
}
}
return 1;
}
void main()
{
TSMatrix M, T;
CreateSMatrix(M);
PrintSMatrix1(M);
PrintSMatrix(M);
printf("转置矩阵为:\n");
FastTransposeSMatrix(M, T);
PrintSMatrix1(T);
}
2,十字链表
当矩阵中非零元个数发生变化时,由于三元组表的顺序存储不便于新数据的插入和删除,所以改用链式存储方法。
建立一个包含5个域的结点,包含 i , j , e ,三个域用来表示其所在的行列数即其本身的元素值,再加上两个指针域,rnext指向同一行中下一个非零元结点,cnext指向同一列中下一个非零元结点。这样同一行的非零元可以通过rnext指针形成一个线性链表,同一列的非零元素也可以通过cnext指针形成一个线性链表。整个矩阵构成一个十字交叉的链表。
十字链表的类型描述如下:
typedef struct OLNode {
int i, j; //该元的行和列下标
int e; //该元本身的元素值
struct OLNode *rnext, *cnext; //该元所在行表和列表的后继指针
}OLNode,*OLink;
typedef struct {
OLink *rhead, *chead; //行和列链表头指针向量基址在建立存储结构时分配
int m, n, t; //稀疏矩阵的行数,列数和非零元素数
}CrossList;
本笔记所依据的教材为严薇敏版的《数据结构及应用算法教程》
部分图片来源于华中师范大学云课堂
所有代码在Visual Studio 2017上均可正常运行
如有错误欢迎指出