前言
这次依旧是数据结构的作业题,先看一下作业的要求:
- 以三元组的形式压缩储存稀疏矩阵
- 实现矩阵的转置、加法、乘法运算
稀疏矩阵指的是矩阵中大多数的元素是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;
}