在介紹矩陣的壓縮存儲前,我們需要明確一個概念:對於特殊矩陣,比如對稱矩陣,稀疏矩陣,上(下)三角矩陣,在數據結構中相同的數據元素只存儲一個。
三元組順序表
稀疏矩陣由於其自身的稀疏特性,通過壓縮可以大大節省稀疏矩陣的內存代價。具體操作是:將非零元素所在的行、列以及它的值構成一個三元組(i,j,v),然后再按某種規律存儲這些三元組,這種方法可以節約存儲空間 。
如下圖所示為一個稀疏矩陣,我們應該怎么樣存儲呢?
若對其進行壓縮存儲,我們可以將一個非零數組元素的三元看成一個單位存入一維數組,具體如下所示。比如(1,1,1)代表第一行第一列的元素值為1。注意,這里我們只存儲非零值。
具體代碼如下:
#include<stdio.h>
#include <stdlib.h>
#include <time.h>
#define NUMBER 3
//三元組結構體
typedef struct {
//行標r,列標c
int r,c;
//元素值
int data;
}triple;
//矩陣的結構表示
typedef struct {
//存儲該矩陣中所有非0元素的三元組
triple data[NUMBER];
//row和column分別記錄矩陣的行數和列數,num記錄矩陣中所有的非0元素的個數
int row,column,num;
}TSMatrix;
//輸出存儲的稀疏矩陣
void print(TSMatrix M);
int main() {
int i;
srand((unsigned)time(NULL));
TSMatrix M;
M.row=3;
M.column=3;
M.num=3;
//初始化矩陣
for(i=0;i<M.num;i++){
//隨機數范圍[1,3]
M.data[i].r=rand()%M.num+1;
M.data[i].c=rand()%M.num+1;
M.data[i].data=rand()%10;
}
print(M);
return 0;
}
void print(TSMatrix M){
for(int i=1;i<=M.row;i++){
for(int j=1;j<=M.column;j++){
int value =0;
for(int k=0;k<M.num;k++){
//遍歷時的r,c行列值和實際存儲row,column行列值的比較,相同的話就說明有非零元素,打印存儲的非0值
if(i == M.data[k].r && j == M.data[k].c){
printf("%d ",M.data[k].data);
value =1;
break;
}
}
if(value == 0)
printf("0 ");
}
printf("\n");
}
}
行邏輯鏈接的順序表
使用三元組順序表存儲稀疏矩陣,我們每次訪問其中一個元素都要遍歷整個矩陣,效率比較低。我們可以使用一個一維數組來存儲每行第一個非零元素在一維數組中的位置,這樣就可以提升訪問效率。這樣的表就叫做行邏輯鏈接的順序表。
下圖為一個稀疏矩陣,當使用行邏輯鏈接的順序表對其進行壓縮存儲時,需要做以下兩個工作:
1.將矩陣中的非 0 元素采用三元組的形式存儲到一維數組 data 中:
2.使用數組 rpos 記錄矩陣中每行第一個非 0 元素在一維數組中的存儲位置。
通過以上兩步操作,即實現了使用行邏輯鏈接的順序表存儲稀疏矩陣。
此時,如果想從行邏輯鏈接的順序表中提取元素,則可以借助 rpos 數組提高遍歷數組的效率。
例如,提取圖 1 稀疏矩陣中的元素 2 的過程如下:
由 rpos 數組可知,第一行首個非 0 元素位於data[1],因此在遍歷此行時,可以直接從第 data[1] 的位置開始,一直遍歷到下一行首個非 0 元素所在的位置(data[3])之前;
同樣遍歷第二行時,由 rpos 數組可知,此行首個非 0 元素位於 data[3],因此可以直接從第 data[3] 開始,一直遍歷到下一行首個非 0 元素所在的位置(data[4])之前;遍歷第三行時,由 rpos 數組可知,此行首個非 0 元素位於 data[4],由於這是矩陣的最后一行,因此一直遍歷到 rpos 數組結束即可(也就是 data[tu],tu 指的是矩陣非 0 元素的總個數)。
具體代碼如下
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAXSIZE 12500
#define MAXRC 100
typedef struct
{
//行,列
int r,c;
//元素值
int e;
}Triple;
typedef struct
{
//矩陣中元素的個數
Triple data[MAXSIZE];
//每行第一個非零元素在data數組中的位置
int rpos[MAXRC];
//行數,列數,元素個數
int row,column,number;
}RLSMatrix;
//矩陣的輸出函數
void print(RLSMatrix M){
for(int i=1;i<=M.row;i++){
for(int j=1;j<=M.column;j++){
int value=0;
if(i+1 <=M.row){
for(int k=M.rpos[i];k<M.rpos[i+1];k++){
if(i == M.data[k].r && j == M.data[k].c){
printf("%d ",M.data[k].e);
value=1;
break;
}
}
if(value==0){
printf("0 ");
}
}else{
for(int k=M.rpos[i];k<=M.number;k++){
if(i == M.data[k].r && j == M.data[k].c){
printf("%d ",M.data[k].e);
value=1;
break;
}
}
if(value==0){
printf("0 ");
}
}
}
printf("\n");
}
}
int main()
{
int i;
//srand((unsigned)time(NULL));
RLSMatrix M;
//矩陣的大小
M.number = 4;
M.row = 3;
M.column = 4;
//每一行首個非零元素在一維數組中的位置
M.rpos[1] = 1;
M.rpos[2] = 3;
M.rpos[3] = 4;
M.data[1].e = 3;
M.data[1].r = 1;
M.data[1].c = 2;
M.data[2].e = 5;
M.data[2].r = 1;
M.data[2].c = 4;
M.data[3].e = 1;
M.data[3].r = 2;
M.data[3].c = 3;
M.data[4].e = 2;
M.data[4].r = 3;
M.data[4].c = 1;
//輸出矩陣
print(M);
return 0;
}
十字鏈表法
關於十字鏈表法,具體可看下圖。我們把矩陣的每一行每一列分別看成一個鏈表,然后將每一行和每一列的鏈表的第一個元素存放在一個數組中。這個數組就叫行鏈表的頭指針數組,列鏈表的頭指針數組。當我們訪問矩陣的時候,就可以從行/列頭指針數組中取出對應的指針,就可以訪問這一行或者這一列的元素了。
鏈表中節點的結構應如下圖。所以,除了定義三元組的行,列,數值外,我們還需要定義指向行的指針,指向列的指針。最后還需要定義一個存放行/列鏈表頭結點的數組專門存放各行各列的頭結點。具體代碼如下。
typedef struct CLNode
{
//矩陣三元組i代表行 j代表列 e代表當前位置的數據
int r, c, data;
//指針域 行指針 列指針
struct CLNode *prow, *pcolumn;
}CLNode, *CLink;
typedef struct
{
//行和列鏈表頭數組 CLink rhead[] 這樣寫也可以。寫成指針是為了方便動態分配內存
CLink *rhead, *chead;
//矩陣的行數,列數和非零元的個數
int rows, columns, num;
}CrossList;
下面我們將要根據用戶輸入的行數,列數,非零元素的值,來創建矩陣。
//注意檢查用戶的輸入
do {
flag = 1;
printf("輸入矩陣的行數、列數和非0元素個數:");
scanf("%d%d%d",&m,&n,&t);
if (m<0 || n<0 || t<0 || t>m*n)
flag = 0;
}while (!flag);
M.rows = m;
M.columns = n;
M.num = t;
用戶輸入合法的情況下我們要創建並初始化存放行列鏈表頭的數組。
//因為下標從1開始,所以頭結點指針多分配一個內存
if (!(M.rhead = (CLink*)malloc((m + 1) * sizeof(CLink))) || !(M.chead = (CLink*)malloc((n + 1) * sizeof(CLink))))
{
printf("初始化矩陣失敗\r\n");
exit(0);
}
// 初始化行頭指針向量;各行鏈表為空鏈表
for (r = 1; r <= m; r++)
{
M.rhead[r] = NULL;
}
// 初始化列頭指針向量;各列鏈表為空鏈表
for (c = 1; c <= n; c++)
{
M.chead[c] = NULL;
}
存放行列鏈表頭的數組准備好了,接下來我們就要創建數據節點了。根據用戶輸入的行號,列好,數值創建節點。這里同樣要檢查用戶的輸入。
for (scanf("%d%d%d", &r,&c,&data); ((r<=0)||(c<=0)); scanf("%d%d%d", &r,&c,&data))
{
if (!(p = (CLNode*)malloc(sizeof(CLNode))))
{
printf("初始化三元組失敗");
exit(0);
}
p->r = r;
p->c = c;
p->data= data;
}
當創建好一個節點之后,我們就要放到行或者列的正確的位置。根據輸入的行號列號確定插入的位置。那么應該怎樣去插入?分兩種情況
1、當這一行中沒有結點的時候,那么我們直接插入
2、當這一行中有結點的時候我們插入到正確的位置
對於第一個問題,因為之前已經對頭結點數組進行了初始化NULL,所以直接根據NULL==M->rhead[i]
就可以判斷一行中有沒有節點。
對於第二個問題,當行中有節點的時候,無非是插入到某個節點之前或者某個節點之后。什么時候插入到節點前?什么時候插入到節點后呢?
1.插入節點前:當我們要插入的節點的列號小於已經存在的節點的列號,這個時候就要插入到這個節點之前了。
2.插入節點后:當我們要插入的節點的列號大於已經存在的節點的列號,這個時候就要插入到這個節點之后了。
對於第一種情況,代碼如下。
//p為准備插入的節點,要插入到M.rhead[r]之前。
if (NULL == M.rhead[r] || M.rhead[r]->c> c)
{
p->prow = M.rhead[r];
//M.rhead[r]要始終指向行的第一個節點
M.rhead[r] = p;
}
對於第二種情況,我們要插入的節點插入到已有節點的后面,那么,已有將要插入節點的列號必定大於已有節點的列號。我們只要找到一個節點比我們將要插入節點的列號大的節點就好,然后插入到這個節點的前面。如果現有的結點沒有一個結點列號是大於要插入的節點的列號的,那么我們就應該插入到最后一個結點之后!
//我們要找到一個比q節點大的節點。在這個節點之前插入
for (q = M.rhead[r]; (q->prow) && q->prow->c < c; q = q->prow);
p->prow = q->prow;
q->prow = p;
對於列的插入同樣如此,就不一一分析了,下面給出具體代碼。
//鏈接到列的指定位置
if (NULL == M.chead[c] || M.chead[c]->r> r)
{
p->pcolumn = M.chead[c];
M.chead[c] = p;
}
else
{
for (q = M.chead[c]; (q->pcolumn) && q->pcolumn->r < r; q = q->pcolumn);
p->pcolumn = q->pcolumn;
q->pcolumn = p;
}
打印矩陣
對於十字鏈表矩陣的打印,我們每次從行/列頭結點數組中取出每一行或者每一列的第一個節點依次往下訪問就可以了,和普通的鏈表訪問沒有區別。如果對鏈表不熟悉的可以參考這篇文章史上最全單鏈表的增刪改查反轉等操作匯總以及5種排序算法(C語言)
void PrintClist(CrossList M)
{
for (int i = 1; i <= M.num; i++)
{
if (NULL != M.chead[i])
{
CLink p = M.chead[i];
while (NULL != p)
{
printf("%d\t%d\t%d\n",p->r, p->c, p->data);
p = p->pcolumn;
}
}
}
}
完整代碼如下:
/*
* @Description: 十字鏈表存儲壓縮矩陣
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-05-26 16:43:48
* @LastEditors: Carlos
* @LastEditTime: 2020-05-28 14:40:19
*/
#include<stdio.h>
#include<stdlib.h>
typedef struct CLNode
{
//矩陣三元組i代表行 j代表列 e代表當前位置的數據
int r, c, data;
//指針域 行指針 列指針
struct CLNode *prow, *pcolumn;
}CLNode, *CLink;
typedef struct
{
//行和列鏈表頭指針
CLink *rhead, *chead;
//矩陣的行數,列數和非零元的個數
int rows, columns, num;
}CrossList;
CrossList InitClist(CrossList M)
{
CLNode *p,*q;
int r,c,data;
int m, n, t;
int flag;
// printf("輸入矩陣的行數、列數和非0元素個數:");
// scanf("%d%d%d",&m,&n,&t);
do {
flag = 1;
printf("輸入矩陣的行數、列數和非0元素個數:");
scanf("%d%d%d",&m,&n,&t);
if (m<0 || n<0 || t<0 )
flag = 0;
}while (!flag);
M.rows = m;
M.columns = n;
M.num = t;
//因為下標從1開始,所以頭結點指針多分配一個內存
if (!(M.rhead = (CLink*)malloc((m + 1) * sizeof(CLink))) || !(M.chead = (CLink*)malloc((n + 1) * sizeof(CLink))))
{
printf("初始化矩陣失敗\r\n");
exit(0);
}
// 初始化行頭指針向量;各行鏈表為空鏈表
for (r = 1; r <= m; r++)
{
M.rhead[r] = NULL;
}
// 初始化列頭指針向量;各列鏈表為空鏈表
for (c = 1; c <= n; c++)
{
M.chead[c] = NULL;
}
//行數列數不為0
for (scanf("%d%d%d", &r,&c,&data); ((r<=0)||(c<=0)); scanf("%d%d%d", &r,&c,&data))
{
if (!(p = (CLNode*)malloc(sizeof(CLNode))))
{
printf("初始化三元組失敗");
exit(0);
}
p->r = r;
p->c = c;
p->data= data;
//鏈接到行的指定位置。
if (NULL == M.rhead[r] || M.rhead[r]->c> c)
{
p->prow = M.rhead[r];
M.rhead[r] = p;
}
else
{
for (q = M.rhead[r]; (q->prow) && q->prow->c < c; q = q->prow);
p->prow = q->prow;
q->prow = p;
}
//鏈接到列的指定位置
if (NULL == M.chead[c] || M.chead[c]->r> r)
{
p->pcolumn = M.chead[c];
M.chead[c] = p;
}
else
{
for (q = M.chead[c]; (q->pcolumn) && q->pcolumn->r < r; q = q->pcolumn);
p->pcolumn = q->pcolumn;
q->pcolumn = p;
}
}
return M;
}
void PrintClist(CrossList M)
{
for (int i = 1; i <= M.num; i++)
{
if (NULL != M.chead[i])
{
CLink p = M.chead[i];
while (NULL != p)
{
printf("%d\t%d\t%d\n",p->r, p->c, p->data);
p = p->pcolumn;
}
}
}
}
int main()
{
CrossList M;
M.rhead = NULL;
M.chead = NULL;
M = InitClist(M);
PrintClist(M);
return 0;
}
文中代碼均已測試,有任何意見或者建議均可聯系我。歡迎學習交流!
如果覺得寫的不錯,請點個贊再走,謝謝!
有任何問題,均可通過公告中的二維碼聯系我