數據結構(13)矩陣及其壓縮存儲
前言
矩陣我們平時接觸得比較少,但是在計算機中如何存儲表示它也值得思考。通常在高級語言裏,用二維數組來存儲矩陣元素。但是在矩陣中有許多值相同的元素或者零元素,使用二維數組來保存可能會浪費空間,因此牽扯到矩陣的壓縮存儲問題。
所謂的壓縮存儲是指,爲多個值相同的元只分配一個存儲空間,對零元不分配存儲空間。
特殊矩陣與稀疏矩陣
矩陣有特殊矩陣和稀疏矩陣的區別。
特殊矩陣是指:值相同的元素或者零元素在矩陣的分佈中有一定規律
而稀疏矩陣是指:零元素的數量遠多於非零元素的數量,並且非零元素的分佈沒有規律可言
特殊矩陣的壓縮存儲
特殊矩陣中,非零元素的分佈都有一定的規律,我們可以根據規律將其壓縮到一維數組中,並且找到每個非零元素在一維數組中的對應關係。以對稱矩陣爲例,由於對稱矩陣沿對角線兩側的數據相等,因此我們可以只存儲矩陣中對角線一側(包含對角線)的數據即可。
按行元素存儲到一維數組中,可以得到如下的一維數組
矩陣中的元素和一維數組中的元素是存在着一一對應的關係的,設某元素所在的位置爲( i , j ),它的值與數組中對應值skr[k]的關係如下:
注:矩陣元素中的行和列從1開始
例如,現在要求位於(3,1)處的元素值,代入公式:
得k值爲3,代入一維數組skr[3] = 3,與矩陣相符。
以上參考:矩陣(稀疏矩陣)壓縮存儲(3種方式)
除了對稱矩陣外,還有諸如三角矩陣、對角矩陣等都可以找到規律來壓縮存儲,這裏不詳談了,下面主要講稀疏矩陣的壓縮存儲。
稀疏矩陣的壓縮存儲
用三元組來保存非零元素
按照壓縮存儲的概念,我們只存儲其非零元素的值,而稀疏矩陣的問題在於其非零元素的分佈沒有規律。因此除了值本身之外,我們還需要存儲它所在的行和列的位置。這實際上需要一個結構體來表示,一般稱爲三元組。
//三元組
typedef struct Triple{
//行
int i;
//列
int j;
//元素值
ElemType e;
}Triple;
例如,如圖所示的稀疏矩陣
可以由如下的三元組表來記錄
當然,一般還需要記錄下稀疏矩陣的行數、列數。
通過三元組表可以來表示稀疏矩陣,而如何來存儲表示三元組表,則可以引申出稀疏矩陣不同的壓縮存儲方法。
- 三元組順序表:即用順序存儲結構來存儲三元組表,也是本文代碼中使用的方式
- 行邏輯連接的順序表:行邏輯連接的順序表也是用順序存儲結構來存儲,但是在提取效率上比三元組順序表更高。不詳談,可參考:行邏輯鏈接的順序表(壓縮存儲稀疏矩陣)詳解
- 十字鏈表:用鏈式存儲結構來存儲的三元組表。不詳談,可參考:十字鏈表法,十字鏈表壓縮存儲稀疏矩陣詳解
以上三種方法除了都存儲行、列和元素值外,在結點或者整個表結構的設計上會有所區別,例如十字鏈表法的結點中還會額外設置兩個指針域。而三元組順序表,一般會記錄整個稀疏矩陣的行數、列數和非零元的個數。
//稀疏矩陣
typedef struct SMatrix{
//一個數組用於保存三元組
Triple data[MAXSIZE];
//行數
int mu;
//列數
int nu;
//非零元素的個數
int tu;
}SMatrix;
三元組順序表矩陣的創建
這裏所說的創建矩陣,實際上就是將一個稀疏矩陣轉化爲三元組順序表的過程。我們默認從文件中讀取一個矩陣,第一行爲矩陣的行數和列數,文件內容如下(矩陣爲上圖展示的矩陣M):
首先讀取出矩陣的行和列
//讀取出矩陣的行和列
fscanf(fp, "%d %d",&m->mu,&m->nu);
根據行列遍歷矩陣,讀取到每一個值後,判斷是否爲零,若不是則記錄下行數、列數和該值即可。
//讀取數據
int value;
//行
for (int i = 0; i < m->mu; i++) {
//列
for (int j = 0; j < m->nu; j ++) {
fscanf(fp, "%d",&value);
if (value != 0) {
//保存到三元組中
m->data[m->tu].e = value;
m->data[m->tu].i = i;
m->data[m->tu].j = j;
//非零元個數
m->tu ++;
}
}
}
三元組順序表矩陣的轉置
轉置是矩陣的運算之一,設矩陣M爲m行n列,則其轉置的矩陣T爲n行m列,且 T( i , j ) = M( j , i ) 。如圖所示。
如圖,a.data表示的是矩陣M,b.data表示的是矩陣T,如何將a轉成b呢?
可以看到對於矩陣M來說,a.data是按行來依次輸出,而b.data是按列來依次輸出,並且由於轉置運算,需要將 i 和 j 對調。
那麼我們的轉置算法可以這樣設計:
- 用一個 col 循環來查找每一列,即依次從1 2 3 4…開始找
- 遍歷整個三元組表,比較與當前循環次數相等的 j 下標
- 如果相等說明是該列的元素,需要行列對調,然後保存到新表中
可以發現,這個方法需要的循環次數過多,假如列數和非零元個數很多,就沒有效率,下面介紹矩陣的快速轉置方法。
三元組順序表矩陣的快速轉置
矩陣的快速轉置方法,它的基本思想是:先計算出每一列元素在b.data裏所佔的位置,這樣在每轉置一個元素時,可以直接得其在b.data的位置,存入即可
爲了實現這個算法,我們需要額外設置兩個數組,一個用於保存每一列中非零元素的個數,進而得到另一個用於保存每一列的第一個非零元素在b.data中位置的數組。
得到如上數組後,我們只需要遍歷一次a.data,依次把元素放入b.data的相應位置就好了。需要注意的是,在存完一個元素後,對應列的第一個非零元素在b.data的位置應該加一,例如在存完(2,1,12)後,下一個(2,5,18)應該保存到4的位置。這個”每一列第一個非零元素“,應該是”剩下還沒轉置“的第一個。
全部代碼
需要說明一下:矩陣的行和列從1開始,爲了方便說明包括數組也是從1開始的,但是在代碼中爲了方便,行列值都是從0開始
Matrix.txt
6 7
0 12 9 0 0 0 0
0 0 0 0 0 0 0
-3 0 0 0 0 14 0
0 0 24 0 0 0 0
0 18 0 0 0 0 0
15 0 0 -7 0 0 0
SpareMatrix.h
#ifndef SparseMatrix_h
#define SparseMatrix_h
#include <stdio.h>
#include <memory.h>
#include <stdlib.h>
#include <assert.h>
#define ElemType int
#define MAXSIZE 20
//三元組
typedef struct Triple{
//行
int i;
//列
int j;
//元素值
ElemType e;
}Triple;
//稀疏矩陣
typedef struct SMatrix{
//一個數組用於保存三元組
Triple data[MAXSIZE];
//行數
int mu;
//列數
int nu;
//非零元素的個數
int tu;
}SMatrix;
//創建矩陣
void CreateMatrix(SMatrix *m);
//打印矩陣
void PrintMatrix(SMatrix *m);
//拷貝矩陣->將m拷貝到t中
void CopyMatrix(SMatrix *m,SMatrix *t);
//矩陣轉置
void TransposeMatrix(SMatrix *m,SMatrix *t);
//矩陣快速轉置
void FastTransposeMatrix(SMatrix *m,SMatrix *t);
#endif /* SparseMatrix_h */
SpareMatrix.c
#include "SparseMatrix.h"
//創建矩陣
void CreateMatrix(SMatrix *m){
//從文件中讀取數據
FILE *fp = fopen("/Matrix.txt", "r");
if (fp == NULL) {
exit(1);
}
//讀取出矩陣的行和列
fscanf(fp, "%d %d",&m->mu,&m->nu);
//讀取數據
int value;
//行
for (int i = 0; i < m->mu; i++) {
//列
for (int j = 0; j < m->nu; j ++) {
fscanf(fp, "%d",&value);
if (value != 0) {
//保存到三元組中
m->data[m->tu].e = value;
m->data[m->tu].i = i;
m->data[m->tu].j = j;
//非零元個數
m->tu ++;
}
}
}
fclose(fp);
}
//打印矩陣(只打印三元組表
void PrintMatrix(SMatrix *m){
printf("row:%d col:%d\n",m->mu,m->nu);
for (int i = 0; i < m->tu; i ++) {
printf("(%d,%d,%d)\n",m->data[i].i,m->data[i].j,m->data[i].e);
}
}
//拷貝矩陣->將m拷貝到t中
void CopyMatrix(SMatrix *m,SMatrix *t){
t->mu = m->nu;
t->nu = m->nu;
t->tu = m->tu;
for (int i = 0; i < m->tu; i ++) {
t->data[i].i = m->data[i].i;
t->data[i].j = m->data[i].j;
t->data[i].e = m->data[i].e;
}
}
//矩陣轉置
void TransposeMatrix(SMatrix *m,SMatrix *t){
//先轉置行數和列數
t->mu = m->nu;
t->nu = m->mu;
t->tu = m->tu;
//轉置數值
int k = 0;
if (m->tu != 0) {
//從每一列開始找
for (int col = 0; col < m->nu ; col++) {
//遍歷整個三元組表,尋找j座標和當前列標一致的元素
for (int i = 0; i < m->tu; i++) {
if (m->data[i].j == col) {
//交換i、j值,保存數據
t->data[k].i = m->data[i].j;
t->data[k].j = m->data[i].i;
t->data[k].e = m->data[i].e;
k ++;
}
}
}
}
}
//矩陣快速轉置
void FastTransposeMatrix(SMatrix *m,SMatrix *t){
//先轉置行數和列數
t->mu = m->nu;
t->nu = m->mu;
t->tu = m->tu;
//記錄每一列(即轉置後每一行)的非零元素個數
int *num = (int *)malloc(sizeof(int) * m->tu);
assert(num != NULL);
//記錄每一列(即轉置後每一行)的第一個非零元素在轉置後的位置
int *cpot = (int *)malloc(sizeof(int)*m->nu);
assert(cpot != NULL);
if (t->tu != 0) {
//先將計數矩陣初始化爲零
for (int col = 0; col < m->nu; col++) {
num[col] = 0;
}
//統計每一列非零元素的個數
for (int t = 0; t < m->tu; t++) {
num[m->data[t].j]++;
}
//計算每一列第一個元素轉置後的起始位置
//第0列直接爲0
cpot[0] = 0;
for (int col = 1; col < m->nu; col++) {
//此後的每一列起始位置 = 上一列的起始位置+上一列非零元素的個數
cpot[col] = cpot[col -1] + num[col -1];
}
int q = 0;
for (int p = 0; p < m->tu; p ++) {
//得到列數
int col = m->data[p].j;
//得到本列非零元素的起始位置
q = cpot[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;
//起始位置自增
cpot[col] ++;
}
}
free(num);
free(cpot);
}
Main.c
#include "SparseMatrix.h"
int main(int argc, const char * argv[]) {
SMatrix sm,sm1;
//全部初始化爲0
memset(&sm, 0, sizeof(sm));
CreateMatrix(&sm);
PrintMatrix(&sm);
//拷貝
// CopyMatrix(&sm, &sm1);
// PrintMatrix(&sm1);
//轉置
//TransposeMatrix(&sm, &sm1);
FastTransposeMatrix(&sm, &sm1);
PrintMatrix(&sm1);
return 0;
}