題目:
7-1 稀疏矩陣 (30 分)
如果一個矩陣中,0元素占據了矩陣的大部分,那么這個矩陣稱為“稀疏矩陣”。對於稀疏矩陣,傳統的二維數組存儲方式,會使用大量的內存來存儲0,從而浪費大量內存。為此,可以用三元組的方式來存放一個稀疏矩陣。
對於一個給定的稀疏矩陣,設第r行、第c列值為v,且v不等於0,則這個值可以表示為 <r,v,c>。這個表示方法就稱為三元組。那么,對於一個包含N個非零元素的稀疏矩陣,就可以用一個由N個三元組組成的表來存儲了。
如:{<1, 1, 9>, <2, 3, 5>, <10, 20, 3>}就表示這樣一個矩陣A:A[1,1]=9,A[2,3]=5,A[10,20]=3。其余元素為0。
要求查找某個非零數據是否在稀疏矩陣中,如果存在則輸出其所在的行列號,不存在則輸出ERROR。
輸入格式:
共有N+2行輸入: 第一行是三個整數m, n, N(N<=500),分別表示稀疏矩陣的行數、列數和矩陣中非零元素的個數,數據之間用空格間隔; 隨后N行,輸入稀疏矩陣的非零元素所在的行、列號和非零元素的值; 最后一行輸入要查詢的非0數據k。
輸出格式:
如果存在則輸出其行列號,不存在則輸出ERROR。
輸入樣例:
在這里給出一組輸入。例如:
10 29 3
2 18 -10
7 1 98
8 10 2
2
輸出樣例:
在這里給出相應的輸出。例如:
8 10
分析:
在矩陣中,若數值為0的元素數目遠遠多於非0元素的數目,並且非0元素分布沒有規律時,則稱該矩陣為稀疏矩陣。
為了更好地定義稀疏矩陣,引入稀疏因子 t =num/(n*m)[其中num為矩陣中非零元素個數,n為矩陣行數,m為矩陣列數],
當t<=0.05時,該矩陣即可稱作稀疏矩陣。
壓縮稀疏矩陣的常見方法有兩種:
1.利用三元組表壓縮
優點:①代碼簡易;
②占用空間相對較小
缺點:①無法實現隨機存儲;
②在進行對矩陣的操作(如矩陣的乘法/加法)矩陣中數據改變后,需要重新定義三元組表,不具有通用性,不靈活。
2.利用十字鏈表壓縮
優點:①能夠實現隨機存儲;
②對於任意矩陣具有通用性
缺點:①代碼實現困難;
②占用空間相對較大
分析之后我們發現,相對於利用三元組表壓縮稀疏矩陣,利用十字鏈表壓縮應用的范圍更為廣闊,更具有實際操作意義。
這里我們采取十字鏈表方式進行壓縮。
十字鏈表核心(難點):

1.節點定義
(1)非零元素節點:
我們可以知道,十字鏈表說到底還是若干個循環鏈表,那么在創建十字鏈表之前我們需要構建一個合適的節點
所需信息:行(row)、列(col)、值(val),該節點又要連接行的下一個節點(*right)以及列的下一個節點(*down)
如圖:

(2)頭節點定義
頭節點與非零元素不同的是節點中row的位置存放的是原矩陣所有行數,col的位置存放的是原矩陣所有列數,且頭節點需要指向一個指針數組(用於表示行列鏈表頭)。
所需信息:行(row)、列(col)、指向指針數組*h[]的指針(*next)
如圖:

(3)行列鏈表頭節點定義:
行列鏈表頭的表示其實是利用指針數組實現的(存放指針的數組)*h[],這里為了方便,我們稱為行列頭指針。
所需信息:指向下一個行列鏈表頭節點的指針(*next)、指向該行第一個非零元素的指針(*right)、指向該列第一個非零元素的指針(*down)
如圖:

綜上所述,為了實現定義節點操作時的一致性,我們會將節點定義如下圖:

2.行列鏈表頭節點在十字鏈表中的理解

連線兩端一個看上去像行頭、另一個看上去像列頭,其實這兩個都是同一個*h[]。
我看到有建立兩個指針數組的博客 https://blog.csdn.net/zhuyi2654715/article/details/6729783,即行鏈表頭數組和列鏈表頭數組,
想必是誤解了圖的意思。
我是那么理解的:

3.當行/列中沒有非零元素時的表示
前面提到,這是由若干個循環鏈表構成的十字鏈表,那么在空行/列的時候可以用行列頭指針指向它自己本身來表示。
同理,當為行/列末時,最后一個節點的指針right/down必然是指向行/列頭節點的。
代碼:
#include<iostream> using namespace std; //定義十字鏈表節點 typedef struct matrinode{ int i, j; struct matrinode *right, *down; union{ int value; struct matrinode *next; }tag; }matrinode; //創建十字鏈表 //傳入值:頭節點指針,原矩陣行數,原矩陣列數 void createlist(matrinode *&head, int row, int column) { int max = (row>column)?row:column;//取行列的最大值 //定義指針數組(用於表示行列頭),p、q、r輔助指針 matrinode *h[max], *p, *q, *r; head = new matrinode;//申請頭節點空間 head->i = row;//將行數、列數存入頭節點 head->j = column; r = head; //行列鏈表頭 int count; for(count = 1; count<max+1; count++){//為了表示矩陣的行數和列數,這里從1開始計數 h[count] = new matrinode;//給行列鏈表頭申請空間 h[count]->down = h[count]->right = h[count]; h[count]->i = h[count]->j = 0; r->tag.next = h[count];//head->h[1]->h[2]->...->h[max] r = h[count]; } r->tag.next = head;//最后一個行列鏈表頭節點指向head int num;//非零元素個數 int i, j, v; cin>>num; for(count=1; count<num+1; count++){ p = new matrinode; cin>>i>>j>>v; p->i = i; p->j = j; p->tag.value = v; /*這里插入非零元素有兩種情況:(以在某行插入為例) ①當行鏈表為空(q->right == h[count])—或改元素列序數最大時 -> 插入到最后; ②非以上情況 -> 插入到某兩節點中間 */ //行插入 q = h[count]; while(q->right != h[count] && q->right->j < j){ q = q->right; } p->right = q->right; q->right = p; //列插入 q = h[count]; while(q->down != h[count] && q->down->i < i){ q = q->down; } p->down = q->down; q->down = p; } } //查找是否有符合條件的值 void search(matrinode *head) { int value;//輸入需要查找的值 cin>>value; matrinode *h, *p;//定義兩個輔助結點 h = new matrinode; p = new matrinode; h = head->tag.next;//h指向h[1] p = h->right;//p指向h[1]第一行第一個元素 int flag = 0;//flag == 0表示找不到匹配值 int i, j;//若找到滿足條件的值時,i、j存放該值的行和列 while(h->tag.next != head){//當前行不是最后一行時 if(p == h){//若該行為空 h = h->tag.next;//移到下一行 p = h->right; }else{//若該行非空 if(p->tag.value != value){//若p節點存放的值!=需要查找的值時 p = p->right;//p移動到該行下一個 }else{//找到滿足條件的值時 i = p->i; j = p->j; flag = 1; cout<<i<<" "<<j; break; } } } if(flag == 0){ cout<<"ERROR"; } } int main(){ matrinode *head; int row, column; cin>>row>>column; createlist(head, row, column); search(head); return 0; }
總結:
為了學會十字鏈表,查閱了很多資料,每份資料都不盡相同。這份代碼是我打的第四份代碼,因為參考的博客都不太一樣,
比如有的博客會用到struct來定義一個十字鏈表,有的博客更喜歡用class囊括所有。從一開始的照貓畫虎,到逐漸理解算法的核心思想,
需要經過不斷的鍛造,這也可能是我與代碼互相折磨、互相成長的過程吧!
參考資料:
1.https://blog.csdn.net/xiangxizhishi/article/details/79119532
2.https://blog.csdn.net/TheLegendOfZelda/article/details/80221922