一 稀疏矩陣的存儲
1.三元組順序表
三元組表示法就是在存儲非零元的同時,存儲該元素所對應的行下標和列下標。稀疏矩陣中的每一個非零元素由一個三元組(i,j,aij)唯一確定。矩陣中所有非零元素存放在由三元組組成的順序表中(通常用數組)。所以三元組的邏輯結構如下:
//————稀疏矩陣的三元組表示法————// #define MAX_SIZE 1500 //表示稀疏矩陣的非零元素的最大個數 class Triple { int i,j;//表示非零元素的行下表和列下標 int val;//非零元素的值,此處以int類型為例 }; class TSMatrix { Triple data[MAX_SIZE]; int row_num,col_num,cnt;//稀疏矩陣的行數、列數以及非零元素的個數 };
注意,此處的非零元素的三元組是以行序為主序順序排列的。
2.行邏輯鏈接順序表
行邏輯鏈接順序表的實質就是在三元組順序表的基礎上加了一個數組,這個數組用於存儲稀疏矩陣中每行的第一個非零元素的在三元組順序表中的位置(此處一定要理解對,是在三元組順序表中的位置)。所以其邏輯結構如下:
//————稀疏矩陣的行邏輯鏈接表示法————// #define MAX_SIZE 1500 //表示稀疏矩陣的非零元素的最大個數 #define MAX_ROW 1500 //表示稀疏矩陣的行數的最大個數 class Triple { int i,j;//表示非零元素的行下表和列下標 int val;//非零元素的值,此處以int類型為例 }; class RLSMatrix { Triple data[MAX_SIZE]; //非零元三元組表 int rpos[MAX_ROW];//每行第一個非零元素的位置 int row_num,col_num,cnt;//稀疏矩陣的行數、列數以及非零元素的個數 };
3.十字鏈表
當稀疏矩陣的非零元個數和位置在操作過程中變化較大時,就不易采用順序存儲結構來表示三元組的線性表。對於這種類型得矩陣,采用鏈式存儲結構表示三元組的線性表更為恰當。
在鏈表中,每個非零元可用一個含5個域的結點表示,其中$i$、$j$、$val$這三個域分別表示該非零元所在的行、列和非零元的值(就是三元組中的那三個域),向右域right用以鏈接同一行中下一個非零元,向下域down用以鏈接同一行中的下一個非零元。
所以,同一行的非零元通過right域鏈接成一個線性鏈表,同一列的非零元通過down域鏈接成一個線性表,每個非零元既是某個行鏈表中的一個結點,又是某個列鏈表中的一個結點,整個矩陣構成了一個十字交叉的鏈表,所以才稱為十字鏈表。十字鏈表中,可以使用兩個分別存儲行鏈表的頭指針和列鏈表的頭指針的一維數組表示。所以,其邏輯結構如下:
//————稀疏矩陣的十字鏈表表示法————// class CrossNode { int i,j;//表示非零元素的行下表和列下標 int val;//非零元素的值,此處以int類型為例 CrossNode *right,*down; //非零元所在行表和列表的后繼指針域 }; class CrossList { CrossNode *row_head,*col_head; //行鏈表和列鏈表的頭指針 int row_num,col_num,cnt;//稀疏矩陣的行數、列數以及非零元素的個數 };
二 稀疏矩陣的乘法
1.一般矩陣的乘法操作
對於一般矩陣而言,通常用二維數組表示矩陣。對於矩陣$M∈R^{m \times n}$和矩陣$N∈R^{n \times p}$而言,它們的乘法操作表示如下:
#include<iostream> #include<unordered_map> #include<queue> #include<cstring> #include<cstdlib> #include<cmath> #include<algorithm> #include<sstream> #include<set> #include<map> using namespace std; #define MAX_NUM 2 void MatrixMultiply(int M[][MAX_NUM],int N[][MAX_NUM],int rs[][MAX_NUM],int m,int n,int p) { for(int i = 0; i < m; i++) { for(int j = 0; j < p; j++) { rs[i][j] = 0;//初始化第行第j列的元素 for(int k = 0;k < n;k++) { rs[i][j] += (M[i][k] * N[k][j]);//第i行與第j列相乘求和 } } } } int main() { int M[][2] = {{1,1},{2,3}}; int N[][2] = {{2,2},{4,5}}; int rs[][2] ={{0,0},{0,0}}; MatrixMultiply(M,N,rs,2,2,2); for(int i = 0 ;i < 2;i++) { for(int j = 0 ; j < 2; j++) cout<<rs[i][j]<<" "; cout<<endl; } }
這種最通用的矩陣乘法操作算法的時間復雜度為O(mnp)。在這個算法中,最關鍵之處在於求解rs[i][j]的值,但是仔細觀察可以知道,上述算法中不論M[i][k]和N[k][j]是否為零,都要進行一次乘法運算,而實際上,這兩者有一個值為零,其乘積必然為零。所以,在進行矩陣乘法時,應免去這種操作(如果用這種方法來進行稀疏矩陣的乘法運算的話,更應該免去這種無效操作),相當於在第三個循環中加入判斷條件條件:
if(M[i][k] != 0 && N[k][j] != 0)
2.稀疏矩陣的乘法操作
對於稀疏矩陣而言,我們可以通過上述1中的通用的矩陣乘法操作進行運算,但是這樣往往會有很多不必要的運算。所以我們可以進一步優化,最常見的是用行邏輯鏈接的順序表的表示形式進行乘法操作,本文也只講解這種表示形式的矩陣乘法的操作。
實現代碼如下(參考嚴蔚敏《數據結構》P102):
#define MAX_SIZE 1500 //表示稀疏矩陣的非零元素的最大個數 #define MAX_ROW 1500 //表示稀疏矩陣的行數的最大個數 class Triple { public: int i,j;//表示非零元素的行下表和列下標 int val;//非零元素的值,此處以int類型為例 }; class RLSMatrix { public: Triple data[MAX_SIZE]; //非零元三元組表 int rpos[MAX_ROW];//每行第一個非零元素的位置 int row_num,col_num,cnt;//稀疏矩陣的行數、列數以及非零元素的個數 }; void MultRLSMatrix(RLSMatrix M,RLSMatrix N,RLSMatrix &rs){ int arow,brow,p,q,ccol,ctemp[MAX_ROW + 1],t,tp; if(M.col_num != N.row_num){//不能相乘 return; } if(0 == M.cnt * N.cnt ){//有一個是零矩陣 return; } //rs初始化 rs.row_num = M.row_num; rs.col_num = N.col_num; rs.cnt = 0; //從M的第一行開始到最后一行,arow是M的當前行 for(arow = 1;arow <= M.row_num;arow++){ for(ccol=1;ccol <= rs.col_num;ccol++){ ctemp[ccol] = 0;//rs的當前行的各列元素清零 } rs.rpos[arow] = rs.cnt + 1;//開始時從第一個存儲位置開始存,后面是基於前面的 if(arow < M.row_num){ tp = M.rpos[arow+1];//下一行的起始位置 }else{ tp = M.cnt + 1;//最后一行的邊界 } for(p = M.rpos[arow];p < tp;p++){ //對M當前行的每一個非零元 //找到對應元素在N中的行號,即M中當前元的列號 brow = M.data[p].j; //原理同上 if(brow < N.row_num){ t = N.rpos[brow + 1]; }else{ t = N.cnt + 1; } for(q = N.rpos[brow];q < t;q++){ ccol = N.data[q].j;//乘積元素在rs中列的位置 ctemp[ccol] += M.data[p].val * N.data[q].val; }//for_q }//for_p //該壓縮存儲該行非零元了 for(ccol = 1;ccol <= rs.col_num;ccol++){ if(0 != ctemp[ccol]){ if(++rs.cnt > MAX_SIZE){//注意這里有個++ return; } rs.data[rs.cnt].i = arow; rs.data[rs.cnt].j = ccol; rs.data[rs.cnt].val = ctemp[ccol]; } } }//for_arow }
算法的主要思想如下:
實際上它的求解過程與1中的算法本質是一樣的,只不過充分利用了三元組的性質和rpos數組的信息。為了得到非零元的乘積,只要對M.data[1,2...]中的每個元素即$(i,k,M[i]k])$,找到N.data中所有相應的元素$(k,j,N[k][j])$相乘即可,為此
只需在N.data中找到矩陣N中第k行的所有非零元;而rpos數組正好提供了相關的信息。rpos[row]表示矩陣N中第row行,第一個非零元在N.data中的序號;而rpos[row+1]-1表示矩陣N中第row行最后一個非零元素在N.data中的序號(因為data數組中存儲的全都是非零元素,而rpos[row+1]是第row+1行中第一個非零元素在data中的位置,所以它的前一個位置一定是第row行中最后一個非零元素在data中的位置)。
此外,有幾點需要注意:
(1)由於兩個稀疏矩陣相乘的結果矩陣不一定是稀疏矩陣,所以結果矩陣的表示方式不一定要用行邏輯鏈接順序表,也可以用二維數組。
(2)該算法的時間復雜度具體分析見《數據結構》p103,總之,矩陣越稀疏,該算法時間復雜度越低,能達到O(mp)數量級。