Ch5 数组和广义表 | 数据结构

302 阅读4分钟

第 5 章 数组和广义表


5.1 数组与线性表及数组的运算

5.1.1 数组

Pasted image 20221017161257.png

任何数组 AA 都可以看作一个线性表

二维数组 m×nm\times n 时,aia_{i}是数组中第 ii 列所有元素,表中每一个元素是一个一维数组;

三维数组时,表中每一个元素是一个二维数组;

nn 维数组时,表中每一个元素是一个 (n1)(n-1) 维数组。

5.1.2 数组与线性表之间的关系

线性表的扩展,其数据元素本身也是线性表

数组的特点

  • 数组中各元素都具有统一的类型
  • 可以认为,d 维数组的非边界元素具有 d 个直接前趋和 d 个直接后继
  • 数组维数确定后,数据元素个数和元素之间的关系不再发生改变, 适合于顺序存储
  • 每组有定义的下标都存在一个与其相对应的值

在数组上的操作

  • 给定一组下标,取得相应的数据元素值
  • 给定一组下标,修改相应的数据元素值

数组的基本操作定义

  1. 构造n维数组
InitArray(&A, n, bound1, …, boundn)
  1. 销毁数组A
DestroyArray(&A)
  1. 取得指定下标的数组元素值
Value(A, &e, index1, …, indexn )
  1. 为指定下标的数组元素重新赋值
Assign(&A, e, index1, …, indexn)

5.2 数组的顺序存储结构

5.2.1 一维数组

ElemType a[n];

Pasted image 20221017165930.png

5.2.2 二维数组

ElemType a[m][n];

Pasted image 20221017165948.png

5.2.3 n 维数组

ElemType a[b1][b2]...[bn];

Pasted image 20221017170006.png Pasted image 20221017170016.png

5.3 矩阵的压缩存储

目的是节省空间。

5.3.1 对称矩阵

Pasted image 20221017170110.png

特点:在 n×nn\times n 的矩阵 AA 中,满足如下性质

aij=aji(1i,jn)a_{ij}=a_{ji}\quad(1\leq i,j\leq n)

存储方法:只存储下三角或者上三角(包括主对角线)的数据元素,共占用 n(n+1)2\cfrac{n(n+1)}{2} 个元素空间

在一维数组中存储,下标与元素的关系为:

k={i(i1)2+j1ij下三角j(j1)2+i1ij上三角k=\begin{cases} \cfrac{i(i-1)}{2}+j-1 \quad i\geq j \quad 下三角 \\ \cfrac{j(j-1)}{2}+i-1 \quad i\leq j \quad 上三角 \end{cases}

5.3.2 三角矩阵

Pasted image 20221017170126.png

特点:对角线以下(或者以上)的数据元素(不包括对角线)全部为常数 C 。

存储方法:重复元素 C 共享一个元素存储空间(a[0]),共占用 n(n+1)2+1\cfrac{n(n+1)}{2}+1 个元素空间

在一维数组中存储,下标与元素的关系为

Pasted image 20221017170213.png

5.3.3 带状矩阵

特点:在 的方阵中,非零元素集中在主对角线及其两侧共 L (奇数)条对角线的带状区域内 —— L 对角矩阵

存储方法:只存储带状区域内的元素

  1. 以对角线的顺序存储

Pasted image 20221017170229.png

  1. 只存储带状区域内的元素。从上一行的主对角线元素 ai1i1a_{i-1\,i-1} 到本行的主对角 线元素 aiia_{ii} 这一段最多有 LL 个元素,共 (n1)L+1(n-1)L+1 个元素

Pasted image 20221017170242.png

5.3.4 随机稀疏矩阵

特点:大多数元素为零(稀疏因子 δ=ns0.05\delta = \cfrac{n}{s} \leq 0.05

常用存储方法:三元组表、十字链表

5.3.4.1 三元组顺序表

类型定义

#define MAXSIZE 1000
//设定非零元素最大值
typedef struct {
	int i, j ;
	ElemType e;
} Triple;
typedef struct {
	Triple data[MAXSIZE + 1];
	//三元组表,data[0]未用
	int mu, nu, tu;
	//行数、列数、非零元个数
} TSMatrix;

求转置矩阵

方法一:

Pasted image 20221017170536.png

算法实现 O(n×t)O(n\times t)

void TransMatrix(TSMatrix &b, TSMatrix a)
{
	b.mu = a.nu; b.nu = a.mu; b.tu = a.tu;
	if (b.tu) {
		q = 1; //指示b中存放数据的位置,初值为1
		for (col = 1; col <= a.nu; col++) //尝试找到列为1到nu的元素
		for (p = 1; p <= a.tu; p++) //对a的每个三元组检查
			if (a.data[p].j == col) { //找列号为col的三元组
				b.data[q].i = a.data[p].j;
				b.data[q].j = a.data[p].i;
				b.data[q].e = a.data[p].e;
				q++; //修正q值
			}
	}
} //TransMatrix

方法二:快速转置法 O(n+t)O(n+t)

Pasted image 20221017171004.png

算法实现:

Statue FastTransMatrix(TSMatrix &b, TSMatrix a)
{
	b.mu = a.nu; b.nu = a.mu; b.tu = a.tu;
	if ( b.tu ) {
		for(col = 1; col <= a.nu; ++col) //num清零
			num[col] = 0;
			for (t = 1; t <= a.tu; ++t) //对a.tu个非零元素按列号计数
				++num[a.data[t].j];
			cpot[1] = 1; //生成cpot,计算每列第1元素转置后的位置
		for (col = 2; col <= a.nu; ++col)
			cpot[col] = cpot[col - 1] + num[col - 1];
		for (p = 1; p <= a.tu; ++p) { //对a.tu个非零元素转置
			col = a.data[p].j; q = cpot[col];
			b.data[q].i = a.data[p].j;
			b.data[q].j = a.data[p].i;
			b.data[q].e = a.data[p].e;
			++cpot[col];//偏置下一个原来列元素的位置
		}
	}
	return OK;
}//FastTransMatrix
5.3.4.2 行逻辑链接的顺序表

特点:便于随机存储任意一行的非零元素。

类型定义

typedef struct {
	Triple data[MAXSIZE + 1];
	int rpos[MAXRC + 1];
	int mu, nu, tu;
} RLSMatrix;

Pasted image 20221017185943.png

这种方式可以便于某些运算,如:稀疏矩阵相乘等。

5.3.4.3 十字(正交)链表

特点:在行、列两个方向上,将非零元素链接在一起。克服三元组表在矩阵的非零元素位置或个数经常变动时的使用不便。

类型定义

typedef struct OLNode {
	int i, j;
	ElemType e;
	struct OLNode *right, *down;
} OLNode, *OLink;
typedef struct {
	OLink *rhead, *chead;
	int mu, nu, tu;
} CrossList;

Pasted image 20221017190230.png

算法示例:从终端接收信息建立稀疏矩阵的十字链表

算法思想:

  • 赋值矩阵的行数mu、列数nu和非零元素个数tu;
  • 申请行、列头指针向量,将各行、列链表置为空链表;
  • 读入一个非零元素的行号i、列号j、值e
    • 建立该元素的结点,赋值其三元组
    • 寻找该结点在行表中的插入位置并插入
    • 寻找该结点在列表中的插入位置并插入
    • 存在下一个非零元素,转3;否则,结束。

5.4 广义表(列表)的定义和表示方法

概念:广义表是由零个或多个原子或者子表组成的有限序列,可以记作 LS=(d1,d2,,dn)LS=(d_1,d_2,\cdots,d_n)

  • 原子:逻辑上不能再分解的元素
  • 子表:作为广义表中元素的广义表。

广义表中的元素全部为原子时即为线性表。

线性表是广义表的特例,广义表是线性表的推广。

广义表的图形表示方法

Pasted image 20221017190507.png

规定所有表都有名字时,广义表的表示方法

习惯上用大写字母表示广义表的名称,用小写字母表示原子。

例:

  • A(a,b)
  • B(A(a,b), c)
  • L(A(a,b), B(A(a,b),c))
  • D(a, D(a, D(…)))

广义表的表示方法和相关术语

Pasted image 20221017190736.png

  • 表的长度:表中的元素(第一层)个数。
  • 表的深度:表中元素的最深嵌套层数。
  • 表头:表中的第一个元素。
  • 表尾:除第一个元素外,剩余元素构成的广义表。

任何一个非空广义表的表尾必定仍为广义表。

L(A(a,b), B(A(a,b),c))

  • 获取表头的操作:GetHead(L) = A(a,b)
  • 获取表尾的操作:GetTail(L)=(B(A(a,b),c))

广义表结构的分类

  • 纯表:与树型结构对应的广义表。
  • 再入表:允许结点共享的广义表。
  • 递归表:允许递归的广义表。

递归表 \supset 再入表纯表 \supset 线性表

广义表的应用

  • 程序的语句结构
  • m元多项式的表示

Pasted image 20221017190933.png

抽象数据类型定义与基本操作(课本p107)

5.5 广义表的存储结构

5.5.1 头尾链表形式

类型定义

typedef enum (ATOM, LIST) ElemTag;
// ATOM == 0; LIST = 1
typedef struct GLNode {
	ElemTag tag;
	union {
		AtomType atom;
		struct {
			struct GLNode *hp, *tp;
			// hp: head pointer
			// tp: tail pointer
		} ptr;
	}
} *GList1;

示例

Pasted image 20221017154540.png

5.5.2 扩展的线性链表形式

类型定义

typedef enum {ATOM, LIST} ElemTag;
// ATOM == 0; LIST == 1
typedef struct GLNode {
	ElemTag tag;
	union {
		AtomType atom;
		struct {
			struct GLNode *hp;
		}
	}
	struct GLNode *tp;
} *GList2;

示例

Pasted image 20221017154816.png

5.6 广义表的递归算法

广义表的特点:定义是递归的

示例约定:非递归表且无共享子表

示例 1 - 计算广义表的深度

方法一:分析表中各元素(子表)

  • 求深度的递归函数

    • 基本项:
      • DEPTH(LS)=1whenLSisemptyDEPTH(LS) = 1\quad when\,LS\,is\,empty
      • DEPTH(LS)=0whenLSisanatomDEPTH(LS) = 0\quad when\,LS\,is\,an\,atom
    • 归纳项:
      • DEPTH(LS)=1+max1in{DEPTH(ai)}n1DEPTH(LS) = 1 + \max\limits_{1\leq i\leq n}\{DEPTH(ai)\}\quad n\geq 1
  • 算法描述

int GListDepth(GList1 L)
{
	//采用头尾链表存储结构,求广义表L的深度
	if (!L) return 1; //空表
	if (L->tag == ATOM) return 0; //单原子
	for (max = , pp = L; pp; pp = pp->ptr.tp) {
		dep = GListDepth(pp->ptr.hp);
		if (dep > max) max = dep;
	}
	return max + 1; //非空表的深度是各元素深度的最大值加1
}

方法二:分析表头和表尾

  • 求深度的递归函数
depth(LS)={1,LS为空表0,LS为单原子max{depth(head(LS)+1,depth(tail(LS)}depth(LS)= \begin{cases} 1\quad ,\quad 当LS为空表 \\ 0\quad ,\quad 当LS为单原子 \\ \max\{depth(head(LS)+1,\,depth(tail(LS)\} \end{cases}
  • 算法描述
int GListDepth(GList1 L)
{
	if (!L) return 1; //空表
	if (L->tag == ATOM) return 0; //单原子
	dep1 = GListDepth(L->ptr.hp) + 1;
	dep2 = GListDepth(L->ptr.tp);
	return dep1 > dep2 ? dep1 : dep2;
}

示例 2 - 复制广义表

  • 复制操作的递归定义

    • 基本项:InitGList(NEWLS)InitGList(NEWLS)
      • 置空广义:当 LSLS 为空表时
      • 复制单原子结点:当 LSLS 为原子时
    • 归纳项:
      • 建表结点
      • 复制表头
      • 复制表尾
  • 算法描述

Status CopyGList(GList1 &T, GList1 L)
{
	if (!L) T = NULL; //复制空表
	else {
		T = (GList1)malloc(sizeof(GLNode));
		if (!T) exit(OVERFLOW);
		T->tag = L->tag;
		if (L->tag == ATOM) T->atom = L->atom; //复制单原子
		else {
			CopyGList(T->ptr.hp, L->ptr.hp);
			CopyGList(T->ptr.tp, L->ptr.tp);
		}
	}
	return OK;
}//CopyGList

本章学习要点

  • 掌握数组类型的特点以及在高级编程语言中的两种存储表示和实现方法,并熟练掌握低下标优先时指定下标的元素在存储结构中的地址计算方法。

  • 掌握矩阵压缩存储的常用方法。

  • 掌握广义表的结构特点和表长、表深、表头、表尾 的定义。

  • 了解广义表的存储表示方法,学会对非空广义表进行分解的两种分析方法:即可将一个非空广义表分解为表头和表尾两部分或者分解为n个子表。巩固递归算法的设计思想。