前言
這次依舊是數據結構的作業題,先看一下作業的要求:
- 以三元組的形式壓縮儲存稀疏矩陣
- 實現矩陣的轉置、加法、乘法運算
稀疏矩陣指的是矩陣中大多數的元素是0,而且非0元素的分佈沒有規律的矩陣。而這個三元組其實就是指這樣的結構體的數組:
typedef struct TupNode
{
int r; //行號
int c; //列號
int data;//數據
} Tup;
整個的數據結構定義如下:
#define M 5
#define N 5
#define MAXSIZE 25
typedef struct TupNode
{
int r;
int c;
int data;
} Tup;
typedef struct TSMatrix
{
int rows;//整個矩陣的行數
int cols;//整個矩陣的列樹
int nums;//三元組中儲存的元素個數,其實就代表着矩陣中的非零元素個數
TupNode arr[MAXSIZE];//三元組本體
} Mat;
這個Mat就是我們將稀疏矩陣壓縮後的三元組數據結構,爲了方便,這裏直接統一矩陣爲5*5方陣。
先實現一些輔助函數
先實現創建三元組對象的create函數、輸出三元組的print函數、銷燬三元組的destroy函數:
Mat *createMat(int (&a)[M][N])//根據傳進來的二維數組引用創建三元組
{
Mat *mat = new Mat;
int i, j;
mat->cols = N;
mat->rows = M;
mat->nums = 0;
for (i = 0; i < M; i++)
{
for (j = 0; j < N; j++)
{
if (a[i][j] != 0)
{
mat->arr[mat->nums].r = i;
mat->arr[mat->nums].c = j;
mat->arr[mat->nums].data = a[i][j];
mat->nums++;
}
}
}
return mat;
}
Mat *createMat(Mat *mat)//根據傳進來的三元組創建三元組的拷貝,若傳NULL,則返回空三元組
{
Mat *res = new Mat;
res->cols = N;
res->rows = M;
res->nums = 0;
if (mat)
*res = *mat;
return res;
}
void print(Mat *mat)//輸出整個矩陣
{
int cur = 0;
for (int i = 0; i < M; i++)
{
for (int j = 0; j < N; j++)
{
cur = 0;
for (int k = 0; k < mat->nums; k++)
{
if (mat->arr[k].r == i && mat->arr[k].c == j)
{
cur = mat->arr[k].data;
}
}
cout << cur << " ";
}
cout << endl;
}
cout << endl;
}
void printTup(Mat *mat)//以三元組形式輸出,並輸出對應數組下標
{
for (int i = 0; i < mat->nums; i++)
{
cout << i << "(" << mat->arr[i].r << "," << mat->arr[i].c << "," << mat->arr[i].data << ") ";
}
cout << endl;
}
void destroy(Mat *mat)//銷燬三元組
{
delete mat;
}
csdn的縮進比較迷,如果實在看不了可以複製到ide裏看,我是用vscode寫的。
矩陣的轉置
原理比較簡單,其實就是將矩陣的每個元素的行號和列號互換就可以了,但是用三元組儲存的話需要多考慮一些問題:
- 首先要明確的就是,矩陣的轉置不會改變矩陣裏元素的值,那麼矩陣裏非零元素的個數也不會改變,0還是0,非0元素轉置後還是非0元素。所以我們不需要增刪三元組的元素。
- 其次,三元組的儲存是根據行號和列號順序儲存的,如果我們只是簡單地寫個for交換每個元素的行列,這樣三元組的順序會被打亂,雖說還原後還是一樣能得到正確的矩陣,但是這樣不利於我們後面寫查找算法。所以我們需要在交換完行列以後再對三元組進行排序。
實現代碼:
void insertionSortMat(Tup (&arr)[MAXSIZE], int n)//三元組的插入排序
{
Tup tmp;
for (int i = 1; i < n; i++)
{
tmp = arr[i];
int j;
//注意for的條件
for (j = i; j > 0 && arr[j - 1].r > tmp.r && arr[j - 1].c > tmp.c; j--)
{
arr[j] = arr[j - 1];
}
arr[j] = tmp;
}
}
void tran(Mat *mat)
{
int t = 0;
for (int i = 0; i < mat->nums; i++)
{
t = mat->arr[i].c;
mat->arr[i].c = mat->arr[i].r;
mat->arr[i].r = t;
}
insertionSortMat(mat->arr, mat->nums);
}
爲了方便,這裏直接用插入排序來實現排序,排序中的判斷條件我根據三元組進行了一些修改。如果這裏用快排或者歸併的話,整個轉置的複雜度(不計行列互換的for的複雜度,因爲才O(n),大頭是排序)能降到約O(nlogn),這裏因爲有插入排序的原因,複雜度爲O(n^2)。
查找三元組中的元素
這個是題目中沒有要求的,但是想想後面的矩陣相加和相乘都需要在三元組中根據行列來查找某個元素的功能,我索性就先把它實現了吧。要注意,這裏要實現的是在三元組中根據元素在稀疏矩陣裏的行列號來查找元素,是在三元組中根據行列來找,此時三元組的數組下標對於我們的需求來說是沒有意義的,因爲我們三元組裏元素的行列號都不是連續的,自然就沒法直接用下標來查找。
先從樸素的方法講起,我們怎麼從三元組中查找行號爲 r 列號爲 c 的元素?
很簡單,我們可以先根據行號來找,當行號匹配時再找匹配的列號,這樣時間複雜度是O(n),n爲稀疏矩陣中非0的元素數量。代碼如下:
int k = 0;
while (k < mat->nums && r > mat->arr[k].r)k++;
while (k < mat->nums && c > mat->arr[k].c)k++;
其實這裏可以用更爲簡單的寫法,就是for掃一遍數組判斷r和c,但是寫都寫了就這樣吧。其中k爲要查找的元素的下標,r爲要查找的元素的行號,c爲列號。
有沒有效率更高的方法?
我們稍微思考下,三元組是數組,數組裏行號和列號都是有序的且爲順序排列的,那麼答案就呼之欲出了——二分查找
利用二分查找,我們可以把查找元素的複雜度從O(n)降到約O(log(n)),其中n爲矩陣中非0元素的數量,雖說題目中這個數據量從n到log(n)的提升也不明顯,但是多練練手、多思考總是好的。代碼實現:
// returnLastPos is a flag indicating the last search index is returned when the search fails.
// isSucc is a pointer to which var will save the searching result.
inline int binSearch(Mat *mat, int r, int c, bool returnLastPos, bool *isSucc)
{
int a = 0, b = mat->nums - 1;
int mid;
while (a <= b)
{
mid = (a + b) / 2;
if (mat->arr[mid].r == r && mat->arr[mid].c == c)
{
if (isSucc) *isSucc = true;
return mid;
}
else if (mat->arr[mid].r < r || (mat->arr[mid].r == r && mat->arr[mid].c < c))
{
a = mid + 1;
}
else if (mat->arr[mid].r > r || (mat->arr[mid].r == r && mat->arr[mid].c > c))
{
b = mid - 1;
}
}
if (isSucc) *isSucc = false;
return returnLastPos ? mid : -1;
}
我對二分做了一些修改來滿足我的需求,首先是二分中a>b的判定問題,對於三元組來說,要使得元素a>元素b的話,首先a的行號要大於b的行號,或者,在a和b的行號相等的情況下,a的列號大於b的列號,小於的判定也同理,不再贅述。
此外,爲了使查找失敗後依舊能夠返回一個期望的元素位置(即是查找失敗後返回查找失敗時的位置),我用參數returnLastPos來標記是否需要返回這個“查找失敗後,期望的元素位置”,如果此參數爲false,則在查找失敗後返回-1;查找成功則返回目標元素出現的位置。
那麼這樣的話又有一個問題,假如我將true傳給returnLastPos後,此時無論查找是否成功,都不會返回唯一的標識符-1,那麼如何判斷查找成功了呢?所以我加了一個參數 isSucc,以傳址方式接受一個布爾值變量,用來返回查找是否成功。如果不需要得到一個“查找失敗後,期望的元素位置”的話,後兩個參數直接傳false和NULL即可。
修改矩陣中某個元素的值
這裏有以下幾種情況需要考慮:
- 修改的元素本身是0,也就是三元組裏並沒有這個元素,我們要把它修改成非0值,那麼就要在三元組裏插入這個元素。
- 修改的元素是非0的,我們將它修改爲0了,此時需要在三元組裏刪除它。
- 修改的元素是非0的,我們將它修改爲非0值,此時只需要簡單得修改值就行。
- 修改的元素本身是0,我們再將它修改爲0,此時我們什麼都不做。
我們要謹慎一點分析其中的邏輯。因爲我們之前實現過二分查找元素的函數,這裏直接拿來用了,代碼實現:
void value(Mat *mat, int data, int r, int c)//參數r和c都是從1開始的邏輯位置
{
if (r > M || c > N || r < 1 || c < 1)//參數合法性檢驗
{
cout << "Parameter illegal." << endl;
return;
}
r--, c--;//直接自減以對齊物理下標
int k = binSearch(mat, r, c, false, NULL);//不需要返回期望位置
if (k != -1 && data != 0)//三元組中能找到該值,即原值非0,且修改後不爲0的情況
{
mat->arr[k].data = data;
return;
}
if (k == -1 && data != 0)//原值爲0且修改後爲非0的情況,需要插入元素
{
for (int l = mat->nums; l > k; l--)//挪出空位
mat->arr[l] = mat->arr[l - 1];
mat->arr[k].r = r;
mat->arr[k].c = c;
mat->arr[k].data = data;
++mat->nums;
return;
}
if (k != -1 && data == 0)//原值非0且修改後爲0的情況,需要刪除元素
{
for (int i = k; i < mat->nums - 1; i++)//刪除元素
mat->arr[i] = mat->arr[i + 1];
--mat->nums;
}
}
上述代碼是沒有情況4的條件判斷分支的,情況4對應 k == -1 && data == 0 的情況,此時不進入任何if分支也就是什麼都不做。代碼中我寫了詳細的註釋,不懂的可以仔細看看。
矩陣的加法
矩陣加法實現起來比較簡單,這裏用三元組儲存的話需要考慮更多的細節問題。
矩陣相加就是矩陣中各元素相加,對於0元素,即是0+0的情況,我們是不用在結果矩陣裏考慮的,而對於非0元素的相加需要考慮兩種情況,一是兩個加數都是非0的,那麼結果就是 a + b,二是其中一個加數爲0,那麼結果就是a或者b。爲什麼要考慮這些簡單的問題?因爲值爲0的元素在三元組裏是找不到的,是不儲存的。此外,要特別注意兩個非0元素相加等於0的情況,此時結果三元組中應該不儲存該元素。
基於上面這些考慮,我們可以選擇元素較多的三元組的拷貝作爲結果三元組的底,以這個底的基礎上來做加法。
代碼實現:
Mat *MatADD(Mat *m1, Mat *m2)
{
Mat *maxx, *minn;
if (m1->nums > m2->nums)//找出元素多的矩陣
{
maxx = m1;
minn = m2;
}
else
{
maxx = m2;
minn = m1;
}
maxx = createMat(maxx);//拷貝一份作爲結果矩陣
for (int i = 0; i < minn->nums; i++)
{
bool isSucc = false;
int j = binSearch(maxx, minn->arr[i].r, minn->arr[i].c, true, &isSucc);
if (isSucc)//兩個非0元素相加
maxx->arr[j].data += minn->arr[i].data;
else//maxx裏的是0元素,而minn裏的是非0元素,此時需要在maxx裏添加元素
{
for (int k = maxx->nums; k > j; k--)
maxx->arr[k] = maxx->arr[k - 1];
maxx->arr[j].data = minn->arr[i].data;
maxx->arr[j].r = minn->arr[i].r;
maxx->arr[j].c = minn->arr[i].c;
++maxx->nums;
}
}
//加法算完後,移除結果爲0的元素
int k = 0;
for (int i = 0; i < maxx->nums; i++)
{
if (!maxx->arr[i].data)
k++;
else
maxx->arr[i - k] = maxx->arr[i];
}
maxx->nums -= k;
return maxx;
}
我的這個實現可能邏輯不是很清晰,其實直接點的話可以創建一個結果矩陣res,然後對應着掃m1和m2,遇到兩邊都有的元素就加起來放到res裏,還要判斷一下結果是否爲0;一邊有一邊沒有的就把有的一遍邊的值放到res裏。這樣邏輯比較直接一些,而且因爲少了事後處理0值元素的過程,效率可能會高點。
矩陣的乘法
實現起來有點繁瑣,不過思路和二維數組的矩陣乘法實現一樣,也是三層for的實現,只不過這裏多了一個搜素矩陣元素是否存在的過程,而且用了一個變量cur來標記三元組中當前元素的下標。
直接上代碼:
Mat *multiply(Mat *m1, Mat *m2)
{
Mat *res = createMat(NULL);
int sum = 0;
int i1 = -1, i2 = -1, cur = 0;
for (int i = 0; i < m1->rows; i++)
{
for (int j = 0; j < m2->cols; j++)
{
sum = 0;
for (int k = 0; k < m1->cols; k++)
{
i1 = binSearch(m1, i, k, false, NULL);
i2 = binSearch(m2, k, j, false, NULL);
if (i1 != -1 && i2 != -1)//只有兩個因數都非0的時候才處理
{
sum += m1->arr[i1].data * m2->arr[i2].data;
}
}
if (sum)
{
res->arr[cur].data = sum;
res->arr[cur].r = i;
res->arr[cur].c = j;
cur++;
}
}
}
//更新結果矩陣的信息
res->rows = m1->rows;
res->cols = m2->cols;
res->nums = cur;
return res;
}
完整代碼
以上實現的函數我都是單獨測試過的,都沒有問題,但是如果要把測試的代碼以及結果一個個都貼出來的話就太麻煩了,這裏就不貼了,main函數裏我就只留兩個矩陣數據吧。
#include <iostream>
#define M 5
#define N 5
#define MAXSIZE 25
using namespace std;
typedef struct TupNode
{
int r;
int c;
int data;
} Tup;
typedef struct TSMatrix
{
int rows;
int cols;
int nums;
TupNode arr[MAXSIZE];
} Mat;
Mat *createMat(int (&a)[M][N])
{
Mat *mat = new Mat;
int i, j;
mat->cols = N;
mat->rows = M;
mat->nums = 0;
for (i = 0; i < M; i++)
{
for (j = 0; j < N; j++)
{
if (a[i][j] != 0)
{
mat->arr[mat->nums].r = i;
mat->arr[mat->nums].c = j;
mat->arr[mat->nums].data = a[i][j];
mat->nums++;
}
}
}
return mat;
}
Mat *createMat(Mat *mat)
{
Mat *res = new Mat;
res->cols = N;
res->rows = M;
res->nums = 0;
if (mat)
*res = *mat;
return res;
}
void print(Mat *mat)
{
int cur = 0;
for (int i = 0; i < M; i++)
{
for (int j = 0; j < N; j++)
{
cur = 0;
for (int k = 0; k < mat->nums; k++)
{
if (mat->arr[k].r == i && mat->arr[k].c == j)
{
cur = mat->arr[k].data;
}
}
cout << cur << " ";
}
cout << endl;
}
cout << endl;
}
void printTup(Mat *mat)
{
for (int i = 0; i < mat->nums; i++)
{
cout << i << "(" << mat->arr[i].r << "," << mat->arr[i].c << "," << mat->arr[i].data << ") ";
}
cout << endl;
}
void destroy(Mat *mat)
{
delete mat;
}
// returnLastPos is a flag indicating the last search index is returned when the search fails.
// isSucc is a pointer to which var will save the searching result.
inline int binSearch(Mat *mat, int r, int c, bool returnLastPos, bool *isSucc)
{
int a = 0, b = mat->nums - 1;
int mid;
while (a <= b)
{
mid = (a + b) / 2;
if (mat->arr[mid].r == r && mat->arr[mid].c == c)
{
if (isSucc)
*isSucc = true;
return mid;
}
else if (mat->arr[mid].r < r || (mat->arr[mid].r == r && mat->arr[mid].c < c))
{
a = mid + 1;
}
else if (mat->arr[mid].r > r || (mat->arr[mid].r == r && mat->arr[mid].c > c))
{
b = mid - 1;
}
}
if (isSucc)
*isSucc = false;
return returnLastPos ? mid : -1;
}
void value(Mat *mat, int data, int r, int c)
{
if (r > M || c > N || r < 0 || c < 0)
{
cout << "Parameter illegal." << endl;
return;
}
r--, c--;
int k = binSearch(mat, r, c, false, NULL);
if (k != -1 && data != 0)
{
mat->arr[k].data = data;
return;
}
if (k == -1 && data != 0)
{
for (int l = mat->nums; l > k; l--)
mat->arr[l] = mat->arr[l - 1];
mat->arr[k].r = r;
mat->arr[k].c = c;
mat->arr[k].data = data;
++mat->nums;
return;
}
if (k != -1 && data == 0)
{
for (int i = k; i < mat->nums - 1; i++)
mat->arr[i] = mat->arr[i + 1];
--mat->nums;
}
}
void insertionSortMat(Tup (&arr)[MAXSIZE], int n)
{
Tup tmp;
for (int i = 1; i < n; i++)
{
tmp = arr[i];
int j;
for (j = i; j > 0 && arr[j - 1].r > tmp.r && arr[j - 1].c > tmp.c; j--)
{
arr[j] = arr[j - 1];
}
arr[j] = tmp;
}
}
void tran(Mat *mat)
{
int t = 0;
for (int i = 0; i < mat->nums; i++)
{
t = mat->arr[i].c;
mat->arr[i].c = mat->arr[i].r;
mat->arr[i].r = t;
}
insertionSortMat(mat->arr, mat->nums);
}
Mat *MatADD(Mat *m1, Mat *m2)
{
Mat *maxx, *minn;
m1->nums > m2->nums ? maxx = m1, minn = m2 : maxx = m2, minn = m1;
maxx = createMat(maxx);
for (int i = 0; i < minn->nums; i++)
{
bool isSucc = false;
int j = binSearch(maxx, minn->arr[i].r, minn->arr[i].c, true, &isSucc);
if (isSucc)
maxx->arr[j].data += minn->arr[i].data;
else
{
for (int k = maxx->nums; k > j; k--)
maxx->arr[k] = maxx->arr[k - 1];
maxx->arr[j].data = minn->arr[i].data;
maxx->arr[j].r = minn->arr[i].r;
maxx->arr[j].c = minn->arr[i].c;
++maxx->nums;
}
}
int k = 0;
for (int i = 0; i < maxx->nums; i++)
{
if (!maxx->arr[i].data)
k++;
else
maxx->arr[i - k] = maxx->arr[i];
}
maxx->nums -= k;
return maxx;
}
Mat *multiply(Mat *m1, Mat *m2)
{
Mat *res = createMat(NULL);
int sum = 0;
int i1 = -1, i2 = -1, cur = 0;
for (int i = 0; i < m1->rows; i++)
{
for (int j = 0; j < m2->cols; j++)
{
sum = 0;
for (int k = 0; k < m1->cols; k++)
{
i1 = binSearch(m1, i, k, false, NULL);
i2 = binSearch(m2, k, j, false, NULL);
if (i1 != -1 && i2 != -1)
{
sum += m1->arr[i1].data * m2->arr[i2].data;
}
}
if (sum)
{
res->arr[cur].data = sum;
res->arr[cur].r = i;
res->arr[cur].c = j;
cur++;
}
}
}
res->rows = m1->rows;
res->cols = m2->cols;
res->nums = cur;
return res;
}
int main()
{
int arr[M][N] = {{0, 0, 0, 5, 1}, {0, 5, 0, 3, 0}, {0, 5, 7, 0, 1}, {0, 5, 0, 1, 2}, {5, 0, 1, 0, 0}};
int arr2[M][N] = {{0, 0, 0, 1, 1}, {0, 1, 0, 1, 5}, {1, 1, 0, 2, -1}, {0, -3, 0, 0, 2}, {2, 0, 1, 2, 0}};
Mat *mat = createMat(arr);
Mat *mat2 = createMat(arr2);
return 0;
}