前言
数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。
也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。
此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。
欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。
这一节,我们来讨论一个常用的数学工具——矩阵,尤其是它的存储结构
不管我们承认与否,矩阵就是一种数据组织的结构,他有自己独特的运算规律和逻辑结构。
从数据结构学科来说,我们关心各种数据结构的逻辑结构和存储结构。矩阵的逻辑结构,属于线性代数学的内容,在这里不需要赘述。所以,作为数据结构学的内容,我们更关心矩阵的存储结构,也就是以何种方式,将人类可识别的矩阵放置在计算机中,并允许他们进行运算。
数组
其实说来,我们来看看矩阵的样子:
[
a
11
a
12
a
13
a
21
a
22
a
23
a
31
a
32
a
33
]
\begin{bmatrix} a_{11}&a_{12} &a_{13} \\ a_{21}&a_{22} &a_{23} \\ a_{31}&a_{32} &a_{33} \end{bmatrix}
⎣⎡a11a21a31a12a22a32a13a23a33⎦⎤
这个方方正正的结构,有着确定的行数和列数。总是能够让我们能够不由自主的对应一种基本的数据结构——数组,不仅是数组,而且是二维数组。
一个矩阵有两个维度,行和列。可以分别对应二维数组中的两个维度。更进一步的,假设一个矩阵,他有
n
n
n行
m
m
m列,第
i
i
i行
j
j
j列元素为
a
i
j
a_{ij}
aij,那么使用二维数组可表示为a[i][j]
这样,一个矩阵就可以装进一个二维数组中,这就是最朴素的“矩阵的数组表示”

不过,二者还是存在一些不那么相互兼容的地方。比如较为突出的:在高级语言中,数组的下标是从0开始计数的,而矩阵的下标则是从1开始计数的。不过好在数组的兼容性更强,只要将空间多开一些,就可以解决这个问题,所以无伤大雅。
来举个例子:
示例一,读入一个n行m列的整数矩阵,计算矩阵中元素的和并输出该矩阵
int a[N + 1][M + 1];//未满足矩阵从1开始计数,数组范围要开大一些,至少多一,建议再多开一些
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++){//先行后列读入矩阵中元素
for(int j = 1;j <= m;j++){
scanf("%d",&a[i][j]);
sum += a[i][j];
}
}
printf("%d",sum);
int sum = 0;
for(int i = 1;i <= n;i++){//先行后列遍历矩阵中元素,打印并计算加和
for(int j = 1;j <= m;j++){
printf("%-2d",a[i][j]);
}
printf("\n");
}
}
运行结果如下

三元组表
用数组存储矩阵很香,香就香在他太简洁了。但香也有香的代价,那就是数组的空间问题。尤其是当矩阵的规模非常大且内容非常稀疏,存在大面积的0。这时候,使用数组存储矩阵的问题就显现出来了。
就像是偌大的操场,整整齐齐的排列着 100 × 100 = 10000 100\times 100 = 10000 100×100=10000个座位,但是来参加活动的同学只有寥寥数人( ≤ 100 \leq100 ≤100)。。。
这时如果我们是管理人员,要记录每个同学坐在那个位置,最好的方法不是画一张
100
×
100
100\times 100
100×100的表格然后填写学生姓名。而是将它们的姓名写在一起,同时在后面跟着写上他们座位的坐标。

对于稀疏矩阵的存储方式,采取的方法正是像记录学生姓名一样,只不过矩阵元素是整数而非字符串。但是不论如何,都可以使用如下三元组表示一个元素:
(
v
a
l
,
r
o
w
,
c
o
l
)
即
(
值
,
行
,
列
)
(val,row,col)\\ 即\\ (值,行,列)
(val,row,col)即(值,行,列)
将矩阵中所有的非零元素按照上述形式放在一个线性表中,就成为了矩阵的一种压缩表示——三元组表
举个例子:
给定两个n行m列的矩阵,输出他们相加矩阵的三元素表,元素顺序任意
int a[N][3],sizeA = 0;//矩阵A
int b[N][3],sizeB = 0;//矩阵B
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++){//读入矩阵A并转化为三元组表
for(int j = 1,x;j <= m;j++){
scanf("%d",&x);
if(x != 0){
a[sizeA][0] = x;
a[sizeA][1] = i;
a[sizeA][2] = j;
sizeA++;
}
}
}
for(int i = 1;i <= n;i++){//读入矩阵B并转化为三元组表
for(int j = 1,x;j <= m;j++){
scanf("%d",&x);
if(x != 0){
b[sizeB][0] = x;
b[sizeB][1] = i;
b[sizeB][2] = j;
sizeB++;
}
}
}
for(int i = 0;i < sizeB;i++){//将矩阵B加到矩阵A上
for(int j = 0;j < sizeA;j++){
if(b[i][2] == a[j][2] && b[i][1] == a[j][1]){//如果位置匹配,相加并执行下个元素
a[j][0] += b[i][0];
break;
}
if(j == sizeA - 1){//如果枚举完A中最后一个也没匹配到,则说明A中该位置为0,应该在三元组中新加入元素
a[sizeA][0] = b[i][0];
a[sizeA][1] = b[i][1];
a[sizeA][2] = b[i][2];
sizeA++;
break;
}
}
}
for(int i = 0;i < sizeA;i++){
printf("%-3d%-3d%-3d\n",a[i][0],a[i][1],a[i][2]);
}
}
运行结果如下:

(这个示例的编码方式上存在许多可以优化的地方,有些步骤写的冗余是为了更好地演示三元组表)
另外,上面的三元组表实现方式是顺序表,也就是数组。但在上文中我们提到过存放他的可以是线性表,也就是说除了数组,我们也可以使用链表来完成对三元组表的存储,从而更加便于对三元组表中元素的增删。
十字链表
在上文中,提到了稀疏矩阵面临的问题,就是定长的数组中存在着大量多余的元素。对于这样的情景,在讨论线性表的时候,就使用了链表来代替顺序表也就是数组。
在这里,我们仍然可以复刻这个思想,使用链表来代替二维数组。可是,传统的链表只有一维,不能很好地表示二维的数组。
如果将元素都穿成一串进行存储,那么不能快速的遍历某一行或某一列全部的元素。
如果将元素按照行进行链表存储,那么访问某一列的元素又成为了问题。
所以为了解决这个问题,更好地存储且便于访问,我们有必要对链表结点进行修改。
在之前,每个链表只有一个维度的指针,最多也就是前驱后继都做记录。现在,我们在结点中多加入一个维度的指针。让结点既可以访问同行的后继,也可以访问同列的后继。

同时给每行和每列增设头结点,将链表约定为单向循环链表。
按照这样的约定,如下矩阵:
[
0
7
0
0
0
5
0
1
0
0
0
0
15
0
0
0
0
0
3
0
]
\begin{bmatrix} 0 &7 &0 &0 &0 \\ 5 &0 &1 &0 &0 \\ 0 &0 &15 &0 &0 \\ 0 &0 &0 &3 &0 \end{bmatrix}
⎣⎢⎢⎡050070000115000030000⎦⎥⎥⎤
表示成为十字链表就是:

看着挺不错。接着就来用代码实现这个结构,首先,需要定义结点结构和哨兵结点类
class Node{//结点结构体
int value;//结点值
int col;//列下标
int row;//行下标
Node * right;//右方元素,即行后继
Node * down;//下方元素,即列后继
};
很显然,十字链表包含很多相关内容,不妨定义一个类来封装十字链表表示的矩阵。这样可以将结点类定义成为十字链表的内部类来增强封装性。
class LinkedMatrix{//十字链表实现的矩阵类
private:
class Node{//结点结构体
public:
int value;//结点值
int col;//列下标
int row;//行下标
Node * right;//右方元素,即行后继
Node * down;//下方元素,即列后继
};
Node ** rows;//行哨兵结点
Node ** cols;//列哨兵结点
int col;//列数
int row;//行数
}
初始化链表的构造方法:
LinkedMatrix(int n,int m){
row = n;
col = m;
//创建行列哨兵结点数组,矩阵下标从1开始,不要忘记了
rows = new Node *[n + 1];
cols = new Node *[m + 1];
//初始化行哨兵结点
for(int i = 1;i <= row;i++){
rows[i] = new Node();
rows[i]->right = rows[i];//循环链表初始化
rows[i]->down = NULL;
rows[i]->row = 0;//行下标默认置零
}
//初始化列哨兵结点
for(int i = 1;i <= col;i++){
cols[i] = new Node();
cols[i]->down = cols[i];//循环链表初始化
cols[i]->right = NULL;
cols[i]->col = 0;//列下标默认置零
}
}
接着再创造两个公共方法:添加和查询
public:
bool add(int val,int r,int c){//插入一个元素
if(c <= 0 || r <= 0 || c > col || r > row){
return false;
}
//创建并初始化结点
Node * node = new Node();
node->value = val;
node->col = c;
node->row = r;
//将该元素插入行
node->right = rows[r]->right;
rows[r]->right = node;
//将该元素插入列
node->down = cols[c]->down;
cols[c]->down = node;
}
int get(int r,int c){//获取r行c列的元素值
//遍历c列元素,寻找是否有r行元素(也可以遍历行,寻找列)
Node * node= cols[c]->down;
while(node != cols[c]){
if(node->row == r){
return node->value;
}
node = node->down;
}
return 0;//如果没有找到,则该元素为0
}
完整的类定义如下:
class LinkedMatrix{//十字链表实现的矩阵类
private:
class Node{//结点结构体
public:
int value;//结点值
int col;//列下标
int row;//行下标
Node * right;//右方元素,即行后继
Node * down;//下方元素,即列后继
};
Node ** rows;//行哨兵结点
Node ** cols;//列哨兵结点
int col;//列数
int row;//行数
public:
LinkedMatrix(int n,int m){
row = n;
col = m;
//创建行列哨兵结点数组,矩阵下标从1开始,不要忘记了
rows = new Node *[n + 1];
cols = new Node *[m + 1];
//初始化行哨兵结点
for(int i = 1;i <= row;i++){
rows[i] = new Node();
rows[i]->right = rows[i];//循环链表初始化
rows[i]->down = NULL;
rows[i]->row = 0;//行下标默认置零
}
//初始化列哨兵结点
for(int i = 1;i <= col;i++){
cols[i] = new Node();
cols[i]->down = cols[i];//循环链表初始化
cols[i]->right = NULL;
cols[i]->col = 0;//列下标默认置零
}
}
bool add(int val,int r,int c){//插入一个元素
if(c <= 0 || r <= 0 || c > col || r > row){
return false;
}
//创建并初始化结点
Node * node = new Node();
node->value = val;
node->col = c;
node->row = r;
//将该元素插入行
node->right = rows[r]->right;
rows[r]->right = node;
//将该元素插入列
node->down = cols[c]->down;
cols[c]->down = node;
}
int get(int r,int c){//获取r行c列的元素值
//遍历c列元素,寻找是否有r行元素(也可以遍历行,寻找列)
Node * node= cols[c]->down;
while(node != cols[c]){
if(node->row == r){
return node->value;
}
node = node->down;
}
return 0;//如果没有找到,则该元素为0
}
};
封装好了十字链表类,下面就用一个示例来小试牛刀:
输入格式:
第一行两个整数n,m表示矩阵行数和列数
第二行一个整数k,表示矩阵中非零元素数量
接下来k行,每行三个元素v,r,c表示一个非零元素的三元组
接下来一行,一个整数q表示有q个询问
接下来q行,每行两个整数r,c,询问r行c列元素值
输出格式:
输出共有q行,本别是每个询问的答案
主函数代码如下:
int main(){
int n,m;
cin >> n >> m;
LinkedMatrix * lm = new LinkedMatrix(n,m);
int k;
cin >> k;
int v,c,r;
for(int i = 0;i < k;i++){
cin >> v >> r >> c;
lm->add(v,r,c);
}
cin >> k;
for(int i = 0;i < k;i++){
cin >> r >> c;
cout << lm->get(r,c) << endl;
}
}
将下图矩阵作为样例:
[
0
7
0
0
0
5
0
1
0
0
0
0
15
0
0
0
0
0
3
0
]
\begin{bmatrix} 0 &7 &0 &0 &0 \\ 5 &0 &1 &0 &0 \\ 0 &0 &15 &0 &0 \\ 0 &0 &0 &3 &0 \end{bmatrix}
⎣⎢⎢⎡050070000115000030000⎦⎥⎥⎤
运行程序,结果如下:

往期博客
- 【数据结构基础】数据结构基础概念
- 【数据结构基础】线性数据结构——线性表概念 及 数组的封装
- 【数据结构基础】线性数据结构——三种链表的总结及封装
- 【数据结构基础】线性数据结构——栈和队列的总结及封装(C和java)
- 【算法与数据结构基础】模式匹配问题与KMP算法
- 【数据结构与算法基础】二叉树与其遍历序列的互化 附代码实现(C和java)
- 【数据结构与算法拓展】 单调队列原理及代码实现
- 【数据结构基础】图的存储结构
- 【数据结构与算法基础】并查集原理、封装实现及例题解析(C和java)
- 【数据结构与算法拓展】二叉堆原理、实现与例题(C和java)
- 【数据结构与算法基础】哈夫曼树与哈夫曼编码(C++)
- 【数据结构与算法基础】最短路径问题
- 【数据结构与算法基础】堆排序原理及实现
- 【数据结构与算法基础】最小生成树算法原理及实现
- 【数据结构基础】图的遍历方法与应用
参考资料:
- 《数据结构》(刘大有,杨博等编著)
- 《算法导论》(托马斯·科尔曼等编著)
- 《图解数据结构——使用Java》(胡昭民著)
- OI WiKi
本文介绍了矩阵在数据结构中的存储方式,包括数组、三元组表和十字链表。数组是最朴素的表示方式,适用于稠密矩阵;三元组表用于稀疏矩阵,通过线性表存储非零元素,节省空间;十字链表则结合链表优势,支持快速访问行和列。文章通过实例详细讲解了这些结构,并提供了相应的代码实现。

877

被折叠的 条评论
为什么被折叠?



