数据结构—稀疏矩阵和广义表 Ⅹ

数据结构-数组和广义表(第六章)的整理笔记,若有错误,欢迎指正。
特殊矩阵的压缩存储
共用体类型



  • 当一个阶数较大的矩阵中的非零元素个数s相对于矩阵元素的总个数t非常小时,即s ≪ \ll t时,称该矩阵为稀疏矩阵(sparse matrix)。
  • 稀疏矩阵和特殊矩阵相比有一个明显的差异:特殊矩阵中特殊元素的分布具有某种规律,而稀疏矩阵中特殊元素(非零元素)的分布没有规律,即具有随机性。稀疏矩阵抽象数据类型与d(d=2)维数组抽象数据类型的描述相似。

稀疏矩阵的三元组表示

  • 稀疏矩阵的压缩存储方法是只存储非零元素。由于稀疏矩阵中非零元素的分布没有任何规律,所以在存储非零元素时必须同时存储该非零元素对应的行下标、列下标和元素值。这样稀疏矩阵中的每一个非零元素由一个三元组 ( i , j , a i , j ) (i,j,a_{i,j}) (i,j,ai,j)唯一确定,稀疏矩阵中的所有非零元素构成三元组线性表。

例: 假设有一个6×7阶稀疏矩阵A:
A 6 × 7 = [ 0 0 1 0 0 0 0 0 2 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 5 0 0 0 0 0 0 0 6 0 0 0 0 0 0 0 7 4 ] A_{6×7}=\begin{bmatrix} 0 & 0 & 1 & 0 & 0 & 0 & 0\\ 0 & 2 & 0 & 0 & 0 & 0 & 0\\ 3 & 0 & 0 & 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 5 & 0 & 0 & 0\\ 0 & 0 & 0 & 0 & 6 & 0 & 0\\ 0 & 0 & 0 & 0 & 0 & 7 & 4\\ \end{bmatrix} A6×7=003000020000100000000500000060000007000004

则稀疏矩阵A对应的三元组可表示为:

以行为主序: i i i j j j a i , j a_{i,j} ai,j 以列为主序: i i i j j j a i , j a_{i,j} ai,j
(0,2,1) 0 2 1 (2,0,3) 2 0 3
(1,1,2) 1 1 2 (1,1,2) 1 1 2
(2,0,3) 2 0 3 (0,2,1) 0 2 1
(3,3,5) 3 3 5 (3,3,5) 3 3 5
(4,4,6) 4 4 6 (4,4,6) 4 4 6
(5,5,7) 5 5 7 (5,5,7) 5 5 7
(5,6,4) 5 6 4 (5,6,4) 5 6 4
  • 若把稀疏矩阵的三元组线性表按顺序存储结构存储,则称为稀疏矩阵的三元组顺序表,简称为三元组表(list of 3-tuples)。三元组顺序表的数据类型声明如下:
#include
#define M <稀疏矩阵行数>
#define N <稀疏矩阵列数>
#define MaxSize <稀疏矩阵中非零元素最多的个数>
typedef int ElemType;
typedef struct
{
	int r; //行号
	int c; //列号
	ElemType d; //元素值 
}TupNode; //三元组类型

typedef struct
{
	int rows; //行号
	int cols; //列号
	int nums; //非零元素个数
	TupNode data[MaxSize];
}TSMatrix; //三元组顺序表的类型
  • 其中,data域中表示的非零元素通常以行序为主序排列,即为一种下标按行有序的存储结构。

从一个二维稀疏矩阵创建其三元组表示

  • 采用以行序为主序的方式扫描二维稀疏矩阵A,将其中非零的元素依次插人到三元组顺序表中。
void CreateMat(TSMatrix& t, ElemType A[M][N])
{
	t.rows = M;
	t.cols = N;
	t.nums = 0;
	for (int i = 0; i < M; i++)
	{
		for (int j = 0; j < N; j++)
			if (A[i][j] != 0) //只存储非零元素
			{
				t.data[t.nums].r = i;
				t.data[t.nums].c = j;
				t.data[t.nums].d = A[i][j];
				t.nums++;
			}
	}
}

三元组元素的赋值

  • 该运算就是对于稀疏矩阵A执行A[i][j]=x(x通常是一个非零值)。先在三元组顺序表t中找到适当的位置k,如果该位置对应一个非零元素,将其d数据域修改为x;否则需要插入一个非零元素,将k~t.nums-1的元素均后移一个位置,再将非零元素x插入到
    t.data[k]处。
bool Value(TSMatrix& t, ElemType x, int i, int j)
{
	int k, k1;
	if (i >= t.rows || j >= t.cols) return false; //i、j参数超界,返回假
	while (k < t.nums && t.data[k].r < i) k++; //查找第i行的第一个非0元素
	while (k < t.nums && t.data[k].r == i && t.data[k].c < j) k++; //在第i行的非0元素中查找第j列
	if (t.data[k].r == i && t.data[k].c == j) t.data[k].d = x; //若存在这样的非0元素,修改非0元素的值
	else //若不存在这样的非0元素,若干元素均后移一个位置
	{
		for (k1 = t.nums - 1; k1 >= k; k1--)
		{
			t.data[k1 + 1].r = t.data[k1].r;
			t.data[k1 + 1].c = t.data[k1].c;
			t.data[k1 + 1].d= t.data[k1].d;
		}
		t.data[k].r = i; //插入非零元素x
		t.data[k].c = j;
		t.data[k].d = x;
		t.nums++; //非0元素个数增1
	}
	return true; //操作成功后返回真
}

将指定位置的元素值赋给变量

  • 该运算就是对于稀疏矩阵A执行x=A[i][j],即提取A中指定下标的元素值。先在三元组顺序表中查找指定的位置,若找到了,说明是一个非零元素,将其值赋给x;否则说明是零元素,置x=0。
bool Assign(TSMatrix& t, ElemType& x, int i, int j)
{
	int k = 0;
	if (i >= t.cols && j >= t.rows) return false;
	while (k < t.nums && t.data[k].r < i) k++;
	while (k < t.nums && t.data[k].r == i && t.data[k].c < j) k++;
	if (t.data[k].r == i && t.data[k].c == j) x = t.data[k].d;
	else x=0;
	return true;
}

输出三元组

  • 该运算从头到尾扫描三元组顺序表t,依次输出元素值。
void DispMat(TSMatrix t)
{
	if (t.nums <= 0) return; //没有非零元素时直接返回
	printf("\t%d\t%d\t%d\n", t.rows, t.cols, t.nums);
	printf("\t-------------------------\n");
	for (int k = 0; k < t.nums; k++) //输出所有非0元素
		printf("\t%d\t%d\t%d\n", t.data[k].r, t.data[k].c, t.data[k].d);
}

稀疏矩阵转置后的三元组

  • 该运算对于一个m×n的稀疏矩阵 A m × n A_{m×n} Am×n,求其转置矩阵 A n × m A_{n×m} An×m,即 b i , j = a j , i b_{i,j}=a_{j,i} bi,j=aj,i,其中 0 ≤ i ≤ m − 1 , 0 ≤ j ≤ n − 1 0≤i≤m-1,0≤j≤n-1 0im10jn1。采用的算法思路是A对应的三元组顺序表为t,其转置矩阵B对应的三元组顺序表为tb。按v=0,1,…,t.cols在t中找列号为v的元素,每找到一个这样的元素,将行、列交换后添加到tb中。
void TranTup(TSMatrix& t, TSMatrix& tb)
{
	int k1 = 0; //k1记录tb中的元素个数
	tb.rows = t.rows;
	tb.cols = t.cols;
	tb.nums = t.nums;
	if (t.nums <= 0) return; //没有非零元素时直接返回
	for (int v = 0; v < t.cols; v++) //当存在非零元素时执行转置,按v=0、1、...、t.cols循环
	{
		for (int k = 0; k < t.nums; k++) //k用于扫描t.data的所有元素
			if (t.data[k].c == v) //找到一个列号为v的元素
			{
				tb.data[k1].r = t.data[k].c; //将行、列交换后添加到tb中
				tb.data[k1].c = t.data[k].r;
				tb.data[k1].d = t.data[k].d;
				k1++; //tb中元素的个数增1
			}
	}
}
  • 该算法中含有两重for循环,其时间复杂度为O(t.cols×t.nums)。最坏的情况是当稀疏矩阵中的非零元素个数t.nums和m×n同数量级时,时间复杂度为 O ( m × n 2 ) O(m×n^2) O(m×n2),所以这不是一种高效的算法。

完整代码

#include
#define M 6
#define N 7
#define MaxSize 42
int A[M][N] = { {0,0,1,0,0,0,0},{0,2,0,0,0,0,0},{3,0,0,0,0,0,0,},
						 {0,0,0,5,0,0,0},{0,0,0,0,6,0,0},{0,0,0,0,0,7,4} };

typedef int ElemType;
typedef struct
{
	int r; //行号
	int c; //列号
	ElemType d; //元素值 
}TupNode; //三元组类型

typedef struct
{
	int rows; //行号
	int cols; //列号
	int nums; //非零元素个数
	TupNode data[MaxSize];
}TSMatrix; //三元组顺序表的类型

//--------三元组的操作---------
void CreateMat(TSMatrix& t, ElemType A[M][N]) //从一个二维稀疏矩阵创建其三元组表示
{
	t.rows = M;
	t.cols = N;
	t.nums = 0;
	for (int i = 0; i < M; i++)
	{
		for (int j = 0; j < N; j++)
			if (A[i][j] != 0) //只存储非零元素
			{
				t.data[t.nums].r = i;
				t.data[t.nums].c = j;
				t.data[t.nums].d = A[i][j];
				t.nums++;
			}
	}
} 

bool Value(TSMatrix& t, ElemType x, int i, int j) //三元组元素的赋值
{
	int k=0, k1;
	if (i >= t.rows || j >= t.cols) return false; //i、j参数超界,返回假
	while (k < t.nums && t.data[k].r < i) k++; //查找第i行的第一个非0元素
	while (k < t.nums && t.data[k].r == i && t.data[k].c < j) k++; //在第i行的非0元素中查找第j列
	if (t.data[k].r == i && t.data[k].c == j) t.data[k].d = x; //若存在这样的非0元素,修改非0元素的值
	else //若不存在这样的非0元素,若干元素均后移一个位置
	{
		for (k1 = t.nums - 1; k1 >= k; k1--)
		{
			t.data[k1 + 1].r = t.data[k1].r;
			t.data[k1 + 1].c = t.data[k1].c;
			t.data[k1 + 1].d= t.data[k1].d;
		}
		t.data[k].r = i; //插入非零元素x
		t.data[k].c = j;
		t.data[k].d = x;
		t.nums++; //非0元素个数增1
	}
	return true; //操作成功后返回真
}

bool Assign(TSMatrix& t, ElemType& x, int i, int j) //将指定位置的元素值赋给变量
{
	int k = 0;
	if (i >= t.cols && j >= t.rows) return false;
	while (k < t.nums && t.data[k].r < i) k++;
	while (k < t.nums && t.data[k].r == i && t.data[k].c < j) k++;
	if (t.data[k].r == i && t.data[k].c == j) x = t.data[k].d;
	else x=0;
	return true;
}

void DispTup(TSMatrix t) //输出三元组
{
	if (t.nums <= 0) return; //没有非零元素时直接返回
	printf("\t%d\t%d\t%d\n", t.rows, t.cols, t.nums);
	printf("\t------------------\n");
	for (int k = 0; k < t.nums; k++) //输出所有非0元素
		printf("\t%d\t%d\t%d\n", t.data[k].r, t.data[k].c, t.data[k].d);
}

void TranTup(TSMatrix& t, TSMatrix& tb) //稀疏矩阵转置后的三元组
{
	int k1 = 0; //k1记录tb中的元素个数
	tb.rows = t.rows;
	tb.cols = t.cols;
	tb.nums = t.nums;
	if (t.nums <= 0) return; //没有非零元素时直接返回
	for (int v = 0; v < t.cols; v++) //当存在非零元素时执行转置,按v=0、1、...、t.cols循环
	{
		for (int k = 0; k < t.nums; k++) //k用于扫描t.data的所有元素
			if (t.data[k].c == v) //找到一个列号为v的元素
			{
				tb.data[k1].r = t.data[k].c; //将行、列交换后添加到tb中
				tb.data[k1].c = t.data[k].r;
				tb.data[k1].d = t.data[k].d;
				k1++; //tb中元素的个数增1
			}
	}
}

//-----------矩阵的操作---------
void DispMat(ElemType A[M][N]) //输出矩阵
{
	for (int i = 0; i < M; i++)
	{
		for (int j = 0; j < N; j++)
			printf("\t%d ", A[i][j]);
		printf("\n");
	}
}

void DispTranMat(ElemType B[N][M]) //输出矩阵的转置
{
	for (int i = 0; i < N; i++)
	{
		for (int j = 0; j < M; j++)
			printf("\t%d ", B[i][j]);
		printf("\n");
	}
}

void TranMat(ElemType B[N][M]) //矩阵转置
{
	for (int i = 0; i < N; i++)
		for (int j = 0; j < M; j++)
			B[i][j] = A[j][i];
}

void ValueMat(ElemType A[M][N], ElemType x, int i, int j)  //修改矩阵对应位置的值
{
	for (int m = 0; m < N; m++)
		for (int n = 0; n < M; n++)
			if (m == i && n == j) A[m][n] = x;
}

int main()
{
	TSMatrix t, tb;
	ElemType B[N][M], x;
	int i, j;
	CreateMat(t,A);
	printf("输出稀疏矩阵A:\n");
	DispMat(A);
	printf("\n输出稀疏矩阵A的三元组:\n");
	DispTup(t);
	TranMat(B);
	printf("\n输出稀疏矩阵A的转置矩阵B:\n");
	DispTranMat(B);
	TranTup(t, tb);
	printf("\n输出稀疏矩阵A的转置矩阵B的三元组:\n");
	DispTup(tb);
	x = 8, i = 3, j = 2;
	printf("\n三元组元素的赋值(A[%d][%d])=%d:\n", i, j, x);
	Value(t, x, i, j);
	DispTup(t);
	i = 4, j = 4;
	Assign(t, x, i, j);
	printf("\n矩阵中A[%d][%d]的值为:%d", i, j, x);
	return 0;
}

程序分析

数据结构—稀疏矩阵和广义表 Ⅹ_第1张图片

  • 从以上可以看出,稀疏矩阵采用三元组顺序表存储后,当非零元素个数较少时会在一定程度上节省存储空间。如果用一个二维数组直接存储稀疏矩阵,此时具有随机存取特性,但采用三元组顺序表存储后会丧失随机存取特性。

稀疏矩阵的十字链表表示

  • 十字链表(orthogonal list)是稀疏矩阵的一种链式存储结构(相应的,前面的三元组顺序表是稀疏矩阵的一种顺序存储结构)。

例: 假设有一个3×4阶稀疏矩阵B:
B 3 × 4 = [ 1 0 0 2 0 0 3 0 0 0 0 4 ] B_{3×4}=\begin{bmatrix} 1 & 0 & 0 & 2\\ 0 & 0 & 3 & 0\\ 0 & 0 & 0 & 4\\ \end{bmatrix} B3×4=100000030204

  • 创建稀疏矩阵B的十字链表的步骤如下:
  1. 对于稀疏矩阵中每个非零元素创建一个结点存放它,包含元素的行号、列号和元素值。这里有4个非零元素,创建4个数据结点。
  2. 将同一行的所有结点构成一个带头结点的循环单链表,行号为i的单链表的头结点为hr[i]。这里有3行,对应有3个循环单链表,头结点分别为hr[0]~hr[2]。hr[i](0≤i≤2)头结点的行指针指向行号为i的单链表的首结点。
  3. 将同一列的所有结点构成一个带头结点的循环单链表,列号为j的单链表的头结点为hd[j]。这里有4列,对应有4个循环单链表,头结点分别为hd[0]~hd[3]。hd[j](0≤j≤3)头结点的列指针指向列号为j的单链表的首结点。
  • 由此创建了3+4=7个循环单链表,头结点的个数也为7个。实际上,可以将hr[i]和hd[j]合起来变为h[i],即h[i]同时包含有行指针和列指针。h[i](0≤i≤2)头结点的行指针指向行号为i的单链表的首结点,h[i](0≤i≤3)头结点的列指针指向列号为i的单链表的首结点,这样头结点的个数为MAX{3,4}=4个。
  1. 再将所有头结点h[i](0≤i≤3)连起来构成一个带头结点的循环单链表,这样需要增加一个总头结点hm,总头结点中存放稀疏矩阵的行数和列数等信息。

稀疏矩阵B的十字链表:
数据结构—稀疏矩阵和广义表 Ⅹ_第2张图片

  • 每个非零元素就好比在一个十字路口,由此称为十字链表。
  • 在稀疏矩阵的十字链表中包含两种类型的结点,一种是存放非零元素的数据结点,另一种是头结点。
    数据结构—稀疏矩阵和广义表 Ⅹ_第3张图片
  • 为了方便算法设计,将两种类型的结点统一起来,设计稀疏矩阵的十字链表的结点类型MaxNode如下:
#include
#define M<稀疏矩阵行数>
#define N<稀疏矩阵列数>
#define Max (M>N?M:N) //三目运算符,取矩阵行列的较大者
typedef int ElemType;
typedef struct MatNode
{
	int row; //行号或者行数
	int col; //列号或者列数
	struct MatNode* right, * down; //行、列指针
	union //共用体
	{
		ElemType value; //非零元素值
		struct MatNode* link; //指向下一个头结点
	}tag;
};
  • 从中可以看出,在十字链表中行、列头结点是共享的,而且采用头结点数组存储,通过头结点h[i]的h[i]->right指针可以逐行搜索行下标为i的所有非零元素,h[i]->down指针可以逐列搜索列下标为i的所有非零元素。每一个非零元素同时包含在两个链表中,方便算法中行方向和列方向的搜索,因而大大降低了算法的时间复杂度。
  • 对于一个m×n的稀疏矩阵,总的头结点个数为MAX{m,n}+1。


广义表

广义表的定义

  • 广义表(generalized table)是线性表的推广,是有限个元素的序列,其逻辑结构采用括号表示法为:GL=(a₁,a₂,…,aᵢ,…,aₙ)。其中,n表示广义表的长度,即广义表中所含元素的个数,n≥0。若n=0,称为空表。aᵢ为广义表的第i个元素,如果aᵢ属于原子类型(原子类型是不可分解的),称为广义表GL的原子(atom);如果aᵢ又是一个广义表,称为广义表GL的子表(subgeneralized table)。
  • 广义表具有以下特性:
  1. 广义表中的数据是有相对次序的
  2. 广义表的长度定义为最外层包含元素的个数
  3. 广义表的深度定义为所含括弧的重数,其中原子的深度为0,空表的深度为1
  4. 广义表可以共享,一个广义表可以被其他广义表共享,这种共享广义表称为再入表
  5. 广义表可以是一个递归的表,一个广义表可以是自己的子表,这种广义表称为递归表

例: A = ( ( ( a , b , ( ) , c ) , d ) , e , ( ( f ) , g ) ) A=(((a,b,(),c),d),e,((f),g)) A=(((a,b,(),c),d),e,((f),g))
数据结构—稀疏矩阵和广义表 Ⅹ_第4张图片

  • 表头是: ( ( a , b , ( ) , c ) , d ) ((a,b,(),c),d) ((a,b,(),c),d),表尾是 ( ( f ) , g ) ((f),g) ((f),g)
  • 长度(红框的部分)是:3,深度(括弧的重数,蓝色点的地方)是:4
    以上广义表可以看成 A = ( B , C , D ) A=(B,C,D) A=(B,C,D),A中含有3个元素:B、C、D;
  • 其中,B又是一个子表,可以看成 B = ( E , F ) B=(E,F) B=(E,F),E又是一个子表,有三个原子(a,b,c)以及一个空表();F只含有单个原子d
  • C是含有单个原子e
  • D又是一个子表,可以看成 D = ( G , H ) D=(G,H) D=(G,H),G是一个只有单个原子f的表,H只含单个原子g。

广义表的存储结构

  • 广义表是一种递归的数据结构,因此很难为每个广义表分配固定大小的存储空间,所以其存储结构只好采用链式存储结构。
    从上图可以看到,广义表有两类结点,一类为圆圈结点,在这里对应子表;另一类为方形结点,在这里对应原子。
    为了使子表和原子两类结点既能在形式上保持一致,又能进行区别,可采用以下结构形式:

  • 其中,tag域为标志字段,用于区分两类结点,即由tag决定是使用结点的sublist还是data域:

  1. 若tag=0,表示该结点为原子结点,则第2个域为data,存放相应原子元素的信息;
  2. 若tag=1,表示该结点为表/子表结点,则第2个域为sublist,存放相应表/子表中第一个元素对应结点的地址。
  • link域存放同一层的下一个元素对应结点(兄弟结点)的地址,当没有兄弟结点时,其link域为NULL。

广义表的结点类型GLNode声明如下:

typedef struct lnode
{
	int tag; //结点类型标识
	union
	{
		ElemType data; //存放原子值
		struct lnode* sublist; //指向子表的指针
	}val;
	struct lnode* link; //指向下一个元素
}GLNode; //广义表的结点类型

你可能感兴趣的:(计算机技术,数据结构,稀疏矩阵,数组,c语言,visual,studio)