Top Banner
第第第 第第第第第第
124

第五章 数组和广义表

Jan 16, 2016

Download

Documents

Julio

第五章 数组和广义表. 本章主要内容 :. 数组的类型定义. 数组的顺序表示和实现. 稀疏矩阵的压缩存储. 广义表的类型定义. 广义表的表示方法. 广义表操作的递归函数. 本章重点 : 数组的类型定义 稀疏矩阵的压缩存储 广义表的类型定义 本章难点 : 稀疏矩阵的压缩存储 广义表的表示和实现. 第五章 数组和广义表. 数组和广义表可以看成线性表在下述含义上的扩展:表中数据元素本身也是一个数据结构。 几乎所有的程序设计语言都把数组类型设为固定类型。 本章以抽象数据类型的形式讨论数组的定义和实现。. 5.1 数组的类型定义. ADT Array { - PowerPoint PPT Presentation
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: 第五章 数组和广义表

第五章 数组和广义表

Page 2: 第五章 数组和广义表

数组的类型定义

稀疏矩阵的压缩存储

数组的顺序表示和实现

广义表的类型定义广义表的表示方法广义表操作的递归函数

本章主要内容 :

Page 3: 第五章 数组和广义表

本章重点 :• 数组的类型定义• 稀疏矩阵的压缩存储• 广义表的类型定义

本章难点 :• 稀疏矩阵的压缩存储• 广义表的表示和实现

Page 4: 第五章 数组和广义表

第五章 数组和广义表• 数组和广义表可以看成线性表在下

述含义上的扩展:表中数据元素本身也是一个数据结构。

• 几乎所有的程序设计语言都把数组类型设为固定类型。

• 本章以抽象数据类型的形式讨论数组的定义和实现。

Page 5: 第五章 数组和广义表

5.1 数组的类型定义ADT Array {

数据对象: D = {aj1,j2, ...,,ji,jn| ji =0,...,bi -1, i=1,2,..,n }

数据关系: R = {R1, R2, ..., Rn}

Ri = {<aj1,... ji,... jn , aj1, ...ji +1, ...jn > | 0 jk bk -1,

1 k n 且 k i, 0 ji bi -2, i=2,...,

n }

} ADT Array

基本操作 :

Page 6: 第五章 数组和广义表

二维数组的定义 :

数据对象 : D = {aij | 0≤i≤b1-1, 0 ≤j≤b2-1}

数据关系 : R = { ROW, COL }

ROW = {<ai,j,ai+1,j 1 >| 0≤i≤b1-2, 0≤j≤b2-1}

COL = {<ai,j,ai,j+1>| 0≤i≤b1-1, 0≤ j≤b2-2}

Page 7: 第五章 数组和广义表

• 我们可以把二维数组看成这样一个定长线性表:它的每个数据元素也是一个定长线性表。

• 可以把二维数组看成一个一维数组,其中每个数据元素也是一维数组。

Page 8: 第五章 数组和广义表

• 如下所示二维数组,以 m 行 n 列的矩阵表示:• A= a00 a01 a02 … a0,n-1

• a10 a11 a12 … a1,n-1

• . . . .

• . . . .

• am-1,0 ……. am-1,n-1

• 它可以看成一个线性表:• A= ( A0,A1,A2,…Ap) (p=m-1 或 n-1)

• Aj 可以看成列向量形式 = ( a0j,a1j,…am-1j)

• 或者 Ai 是行向量形式列表 = ( ai0,ai1,..ai,n-1)

Page 9: 第五章 数组和广义表

基本操作:数组一旦被定义,它的维数和上、下界就不会再改变,因此,除了结构的初始化和销毁之外,数组只有存取元素和修改元素的操作。

Page 10: 第五章 数组和广义表

基本操作:InitArray(&A, n, bound1, ..., boundn)

DestroyArray(&A)

Value(A, &e, index1, ..., indexn)

Assign(&A, e, index1, ..., indexn)

Page 11: 第五章 数组和广义表

InitArray(&A, n, bound1, ..., boundn)

操作结果:若维数 n 和各维长度合法, 则构造相应的数组 A ,并 返回 OK 。

Page 12: 第五章 数组和广义表

DestroyArray(&A)

操作结果:销毁数组 A 。

Page 13: 第五章 数组和广义表

Value(A, &e, index1, ..., indexn)

初始条件: A 是 n 维数组, e 为元素变量, 随后是 n 个下标值。 操作结果:若各下标不超界,则 e 赋值为 所指定的 A 的元素值,并返 回 OK 。

Page 14: 第五章 数组和广义表

Assign(&A, e, index1, ..., indexn)

初始条件: A 是 n 维数组, e 为元素变量, 随后是 n 个下标值。 操作结果:若下标不超界,则将 e 的值赋 给所指定的 A 的元素,并返回 OK 。

Page 15: 第五章 数组和广义表

5.2 数组的顺序表示和实现 类型特点 :

1) 只有引用型操作,没有加工型操作;2) 数组是多维的结构,而存储空间是 一个一维的结构。

有两种顺序映象的方式 :

1) 以行序为主序 ( 低下标优先 ) ;2) 以列序为主序 ( 高下标优先 ) 。

Page 16: 第五章 数组和广义表

 b1*b2 数组 按行序为主序存放

0

1

m-1

m*n-1

m

amn

…….. am1

am0

……….

a1n

…….. a11

a10

a0n

…….

a01

a00

a00 a01 …….. a0n

a10 a11 …….. a1n

am0 am1 …….. amn

………………….

Loc(aij)=Loc(a00)+[i*b2+j]*l

Page 17: 第五章 数组和广义表

  b1*b2 数组按列序为主序存放

0

1

m-1

m*n-1

m

amn

…….. a1n

a0n

……….

am1

…….. a11

a01

am0

…….

a10

a00

a00 a01 …….. a0n

a10 a11 …….. a1n

am0 am1 …….. amn

………………….

Loc(aij)=Loc(a00)+[j*b1+i]*l

Page 18: 第五章 数组和广义表

称为基地址或基址。

以“行序为主序”的存储映象

二维数组 A 中任一元素 ai,j 的存储位置 LOC(i,j) = LOC(0,0) + (b2×i + j)× L

Page 19: 第五章 数组和广义表

推广到一般情况,可得到 n 维数组数据元素存储位置的映象关系

称为 n 维数组的映象函数。数组元素的存储位置是其下标的线性函数。

LOC(j1, j2, ..., jn ) = LOC(0,0,...,0) + ∑ ci ji = LOC(0,0,...,0) +(b2*b3*b4*…*bn*j1+b3*b4*b5*…*bn*j2+…+bn*jn-1+jn)L

i =1

n

Page 20: 第五章 数组和广义表

• 例:以行序为主序存储的整数数组A(9*3*5*8),第一个元素的地址是100,每个整数占4个字节,问a(3125)的地址是多少?

• LOC(a[3125])=100+(3*5*8*3+5*8*1+8*2+5)*4=1784

Page 21: 第五章 数组和广义表

用高级语言编程序的时候,通常用二维数组来存放矩阵的元。但是,有时一些阶数很高的矩阵,里面存了很多值相同的元,或者零元,所以要对矩阵进行压缩存储。

5.3 稀疏矩阵的压缩存储在这里我们考虑的不是矩阵本身,而是如何存储矩阵的元,从而使矩阵的各项运算能够进行。

Page 22: 第五章 数组和广义表

假设 m 行 n 列的矩阵含 t 个非零元素,则称

为稀疏因子。通常认为 0.05 的矩阵为稀疏矩阵。

nmt

5.3 稀疏矩阵的压缩存储何谓稀疏矩阵?

Page 23: 第五章 数组和广义表

就是为多个值相同的元分配一个存储空间;对零元不分配空间。

5.3 稀疏矩阵的压缩存储何谓压缩存储?

Page 24: 第五章 数组和广义表

1) 特殊矩阵 非零元在矩阵中的分布有一定规则 例如 : 三角矩阵 对角矩阵

2) 随机稀疏矩阵 非零元在矩阵中随机出现

有两类稀疏矩阵:

Page 25: 第五章 数组和广义表

特殊矩阵的压缩存储以 n 阶对称矩阵为例 : 在一个 n 阶方阵 A 中,若元素满足下述性质: aij=aji ( 1≦i,j≦n ) 则称 A 为 n 阶对称矩阵。如图 5.1 1 5 1 3 7 a11

5 0 8 0 0 a21 a 22

1 8 9 2 6 a31 a32 a33

3 0 2 5 1 ………………..

7 0 6 1 3 an1 an2 an3 …a nn

图 5.1 对称矩阵

Page 26: 第五章 数组和广义表

n 阶对称矩阵中的元素关于主对角线对称,故只要存储矩阵中上三角或下三角中的元素,让每两个对称的元素共享一个存储空间,这样,能节约近一半的存储空间。

不失一般性,我们按“行优先顺序”存储主对角线(包括对角线)以下的元素。

Page 27: 第五章 数组和广义表

i(i-1)/2+j-1 当 i≧j

j(j-1)/2+i-1 当 i<jk=

在这个下三角矩阵中,第 i 行恰有 i 个元素,元素总数为 n(n+1)/2 ,这样就可将 n2 个数据元素压缩存储在 n(n+1)/2 个存储单元中。

假设以一维数组 va 作为 n 阶对称矩阵 A 的压缩存储单元, k 为一维数组 va 的下标序号,aij 为 n 阶对称矩阵 A 中 i 行 j 列的数据元素( 其中 1≦i,j≦n ),其数学映射关系为:

Page 28: 第五章 数组和广义表

– 三角矩阵

a11 0 0 …….. 0

a21 a22 0 …….. 0

an1 an2 an3…….. ann

…………………. 0

Loc(aij)=Loc(a11)+[( +(j-1)]*l

i(i-1)2

a11 a21 a22 a31 a32 an1 ann …... …...k=0 1 2 3 4 n(n-1)/2 n(n+1)/2-1

按行序为主序:

Page 29: 第五章 数组和广义表

– 对角矩阵

a11 a12 0 …………… . 0

a21 a22 a23 0 …………… 0

0 0 … an-1,n-2 an-1,n-1 an-1,n

0 0 … …an,n-1 ann.

0 a32 a33 a34 0 ……… 0

……………………………

Loc(aij)=Loc(a11)+2(i-1)+(j-1)

a11 a12 a21 a22 a23 ann-1 ann …... …...k=0 1 2 3 4 n(n-1)/2 n(n+1)/2-1

按行序为主序:

Page 30: 第五章 数组和广义表

• 特殊矩阵中非零元的分布都有一个明显的规律,所以可以压缩存储到一维数组中,并找出在一维数组中的对应关系。

Page 31: 第五章 数组和广义表

随机稀疏矩阵的压缩存储方法 :

一、三元组顺序表

二、行逻辑联接的顺序表

三、 十字链表

Page 32: 第五章 数组和广义表

#define MAXSIZE 12500 typedef struct { int i, j; // 该非零元的行下标和列下标 ElemType e; // 该非零元的值 } Triple; // 三元组类型

一、三元组顺序表

typedef struct{ Triple data[MAXSIZE + 1]; int mu, nu, tu; } TSMatrix; // 稀疏矩阵类型

Page 33: 第五章 数组和广义表

0280036

00070

500140

1 2 14

1 5 -5

2 2 -7

3 1 36

3 4 28

Page 34: 第五章 数组和广义表

例 1 :写出下图 5.3 所示稀疏矩阵的压缩存储形式。 1 2 3 4 5 6 1 2 3 4 5 6 图 5.3

解:用三元组线性表线性表表示: {{1,2,12},{1,3,9},{3,1,-3},{3,5,14}, {4,3,24},{5,2,18},{6,1,15},{6,4,-7}}

0 12 9 0 0 00 0 0 0 0 0

-3 0 0 0 14 00 0 24 0 0 00 18 0 0 0 0

15 0 0 -7 0 0

Page 35: 第五章 数组和广义表

例 2 :下面的三元组表表示一个稀疏矩阵,试还原出它的稀疏矩阵。

1 2 2

2 1 12

3 1 3

4 4 4

5 3 6

6 1 16

i j d11

22

33

4 md 4 md

5 nd5 nd

6 td6 td

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 2 0 012 0 0 0 3 0 0 0 0 0 0 4 0 0 6 016 0 0 0

6

4

6

Page 36: 第五章 数组和广义表

如何求转置矩阵?

0280036

00070

500140

005

2800

000

0714

3600

Page 37: 第五章 数组和广义表

用常规的二维数组表示时的算法

其时间复杂度为 : O(mu×nu)

for (col=1; col<=nu; ++col)

for (row=1; row<=mu; ++row)

T[col][row] = M[row][col];

Page 38: 第五章 数组和广义表

用“三元组”表示时如何实现?

•先分析转置的过程 :能否将行、列直接互换??

Page 39: 第五章 数组和广义表

用“三元组”表示时如何实现?

1 2 14

1 5 -5

2 2 -73 1 363 4 28

5 1 -5

4 3 28

2 2 -7

2 1 14

1 3 36

Page 40: 第五章 数组和广义表

•有两种解决方法:•1 、按照矩阵 M的列序来转置。

Page 41: 第五章 数组和广义表

用“三元组”表示时如何实现?

1 2 14

1 5 -5

2 2 -73 1 363 4 28

2 1 14

5 1 -5

2 2 -7

1 3 36

4 3 28

Page 42: 第五章 数组和广义表

2 、快速转置即按 a 中三元组次序转置,转置结果放入 b 中恰当位置此法关键是要预先确定 M 中每一列第一个非零元在 b 中位置,为确定这些位置,转置前应先求得 M 的每一列中非零元个数

实现:设两个数组num[col] :表示矩阵 M 中第 col 列中非零元个数cpot[col] :指示 M 中第 col 列第一个非零元在 b 中位置显然有:

cpot[1]=1;cpot[col]=cpot[col-1]+num[col-1];(2col a.nu)

Page 43: 第五章 数组和广义表

实现:设两个数组num[col] :表示矩阵 M 中第 col 列中非零元个数cpot[col] :指示 M 中第 col 列第一个非零元在 b 中位置显然有:

cpot[1]=1;cpot[col]=cpot[col-1]+num[col-1];

1 3 5 7 8 8 9

col

num[col]

cpot[col]

1

2

2

2

3

2

4

1

5

0

6

1

7

0

7600070015

00000180

00002400

01400003

0000000

00009120

M

Page 44: 第五章 数组和广义表

1 2 151 5 -52 2 -73 1 363 4 28

col 1 2 3 4 5Num[pos] 1 2 0 1 1Cpot[col] 1 2 4 4 5

cpot[1] = 1; for (col=2; col<=M.nu; ++col) cpot[col] = cpot[col-1] + num[col-1];

Page 45: 第五章 数组和广义表

6 7 8

1 2 12

1 3 9

3 1 -3

3 6 14 4 3 24

5 2 18

6 1 15

6 4 -7

i j v

0 1 2 3 4 5 6 7 8

i j v

0 1 2 3 4 5 6 7 8

T

col

num[col]

cpot[col]

1

1

2

2

3

2

3

5

2

4

7

1

5

8

0

6

8

1

7

9

0

7 6 8

1 3 -3

1 6 15

2 1 12

2 5 18 3 1 9

3 4 24

4 6 -7

6 3 14

ppp

pppp

p

4 62 9

753

M

Page 46: 第五章 数组和广义表

Status FastTransposeSMatrix(TSMatrix M, TSMatrix &T){

T.mu = M.nu; T.nu = M.mu; T.tu = M.tu; if (T.tu) { for (col=1; col<=M.nu; ++col) num[col] = 0; for (t=1; t<=M.tu; ++t) ++num[M.data[t].j]; cpot[1] = 1; for (col=2; col<=M.nu; ++col) cpot[col] = cpot[col-1] + num[col-1]; for (p=1; p<=M.tu; ++p) { } } // if return OK;} // FastTransposeSMatrix

转置矩阵元素

Page 47: 第五章 数组和广义表

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]

Page 48: 第五章 数组和广义表

分析算法 FastTransposeSMatrix 的时间复杂度:

时间复杂度为 : O(M.nu+M.tu)

for (col=1; col<=M.nu; ++col) … …

for (t=1; t<=M.tu; ++t) … …

for (col=2; col<=M.nu; ++col) … …

for (p=1; p<=M.tu; ++p) … …

for (col=1; col<=M.nu; ++col) … …

for (t=1; t<=M.tu; ++t) … …

for (col=2; col<=M.nu; ++col) … …

for (p=1; p<=M.tu; ++p) … …

Page 49: 第五章 数组和广义表

三元组顺序表又称有序的双下标法,它的特点是,非零元在表中按行序有序存储,因此便于进行依行顺序处理的矩阵运算。然而,若需随机存取某一行中的非零元,则需从头开始进行查找。

二、行逻辑联接的顺序表

Page 50: 第五章 数组和广义表

#define MAXMN 500 typedef struct { Triple data[MAXSIZE + 1]; int rpos[MAXMN + 1]; int mu, nu, tu; } RLSMatrix; // 行逻辑链接顺序表类型

修改前述的稀疏矩阵的结构定义,增加一个数据成员 rpos ,存放各行第一个非零元在三元组表中的位置.其值在稀疏矩阵的初始化函数中确定。

Page 51: 第五章 数组和广义表

例如:给定一组下标,求矩阵的元素值ElemType value(RLSMatrix M, int r, int c) {

p = M.rpos[r];

while (M.data[p].i==r &&M.data[p].j < c)

p++;

if (M.data[p].i==r && M.data[p].j==c)

return M.data[p].e;

else return 0;

} // value

Page 52: 第五章 数组和广义表

矩阵乘法的精典算法 : for (i=1; i<=m1; ++i) for (j=1; j<=n2; ++j) { Q[i][j] = 0; for (k=1; k<=n1; ++k) Q[i][j] += M[i][k] * N[k][j]; }

其时间复杂度为 : O(m1×n2×n1)

Page 53: 第五章 数组和广义表

Q 初始化; if Q 是非零矩阵 { // 逐行求积 for (arow=1; arow<=M.mu; ++arow) { // 处理 M 的每一行 ctemp[] = 0; // 累加器清零 计算 Q 中第 arow 行的积并存入 ctemp[] 中; 将 ctemp[] 中非零元压缩存储到 Q.data ; } // for arow } // if

两个稀疏矩阵相乘( QMN ) 的过程可大致描述如下:

Page 54: 第五章 数组和广义表

Status MultSMatrix (RLSMatrix M, RLSMatrix N, RLSMatrix &Q) {

if (M.nu != N.mu) return ERROR; Q.mu = M.mu; Q.nu = N.nu; Q.tu = 0; if (M.tu*N.tu != 0) { // Q 是非零矩阵 for (arow=1; arow<=M.mu; ++arow) { // 处理 M 的每一行 } // for arow } // if return OK; } // MultSMatrix

Page 55: 第五章 数组和广义表

ctemp[] = 0; // 当前行各元素累加器清零 Q.rpos[arow] = Q.tu+1; for (p=M.rpos[arow]; p<M.rpos[arow+1];++p) { // 对当前行中每一个非零元 brow=M.data[p].j; if (brow < N.nu ) t = N.rpos[brow+1]; else { t = N.tu+1 } for (q=N.rpos[brow]; q< t; ++q) { ccol = N.data[q].j; // 乘积元素在 Q 中列号 ctemp[ccol] += M.data[p].e * N.data[q].e; } // for q } // 求得 Q 中第 crow( =arow) 行的非零元 for (ccol=1; ccol<=Q.nu; ++ccol) if (ctemp[ccol]) { if (++Q.tu > MAXSIZE) return ERROR; Q.data[Q.tu] = {arow, ccol, ctemp[ccol]}; } // if

处理

的每一

M

Page 56: 第五章 数组和广义表

分析上述算法的时间复杂度累加器 ctemp 初始化的时间复杂度为 (M.muN.nu) ,求 Q 的所有非零元的时间复杂度为 (M.tuN.tu/N.mu) ,进行压缩存储的时间复杂度为 (M.muN.nu) ,总的时间复杂度就是 (M.muN.nu+M.tuN.tu/N.mu) 。

若 M 是 m 行 n 列的稀疏矩阵, N 是 n 行 p 列的稀疏矩阵,则 M 中非零元的个数 M.tu = Mmn , N 中非零元的个数 N.tu = Nnp ,相乘算法的时间复杂度就是 (mp(1+nMN)) ,当 M<0.05 和 N<0.05 及 n <1000 时,相乘算法的时间复杂度就相当于 (mp) 。

Page 57: 第五章 数组和广义表

3. 十字链表设行指针数组和列指针数组,分别指向每行、列第一个非零元结点定义

tpedef struct node{ int row,col,val; struct node *down, *right;}JD;

row col valdown right

^

^ ^

^

^ ^ ^

Page 58: 第五章 数组和广义表

34008

000

450

003

A

1 1 3

4 1 8

2 2 5 2 3 4

^

^ ^

^

^ ^ ^

Page 59: 第五章 数组和广义表

十字链表M.chead

M.rhead

3 0 0 50 -1 0 02 0 0 0

1 1 3 1 4 5

2 2 -1

3 1 2

^

^^

^ ^

^ ^

Page 60: 第五章 数组和广义表

5.4 广义表的类型定义ADT Glist {

数据对象: D = {ei | i=1,2,..,n; n≥0;

ei AtomSet ∈ 或 ei GList,∈ AtomSet 为某个数据对象 }

数据关系:

LR = {<ei-1, ei >| ei-1 ,ei D, 2≤i≤n}∈

} ADT Glist

基本操作 :

Page 61: 第五章 数组和广义表

广义表是递归定义的线性结构, LS = ( 1, 2, , n )

其中: i 或为原子 或为广义表例如 : A = ( )

F = (d, (e))

D = ((a,(b,c)), F)

C = (A, D, F)

B = (a, B) = (a, (a, (a, , ) ) )

Page 62: 第五章 数组和广义表

广义表是一个多层次的线性结构

例如:D=(E, F)

其中 : E=(a, (b, c))

F=(d, (e))

D

E Fa ( ) d ( )

b c e

Page 63: 第五章 数组和广义表

广义表 LS = ( 1, 2, …, n ) 的结构特点 :

1) 广义表中的数据元素有相对次序;2) 广义表的长度定义为最外层包含元素个数;3) 广义表的深度定义为所含括弧的重数; 注意:“原子”的深度为 0

 “空表”的深度为 1

4) 广义表可以共享;5) 广义表可以是一个递归的表。 递归表的深度是无穷值,长度是有限值。

Page 64: 第五章 数组和广义表

6) 任何一个非空广义表 LS = ( 1, 2, …, n) 均可分解为 表头 Head(LS) = 1 和 表尾 Tail(LS) = ( 2, …, n) 两部分。

例如 : D = ( E, F ) = ((a, (b, c)) , F )

Head( D ) = E Tail( D ) = ( F )

Head( E ) = a Tail( E ) = ( ( b, c) )

Head( (( b, c)) ) = ( b, c) Tail( (( b, c)) ) = ( )

Head( ( b, c) ) = b Tail( ( b, c) ) = ( c )

Head( ( c ) ) = c Tail( ( c ) ) = ( )

Page 65: 第五章 数组和广义表

结构的创建和销毁 InitGList(&L); DestroyGList(&L); CreateGList(&L, S); CopyGList(&T, L);

基本操

状态函数 GListLength(L); GListDepth(L); GListEmpty(L); GetHead(L); GetTail(L);

插入和删除操作 InsertFirst_GL(&L, e); DeleteFirst_GL(&L, &e);

遍历 Traverse_GL(L, Visit());

Page 66: 第五章 数组和广义表

5.5 广义表的表示方法由于广义表的数据元素可以具有不同的结构(可以为原子或广义表),因此难以用顺序存储结构。通常采用链式存储结构,有两种结点。

表结点 :

原子结点:

tag=1 hp tp

tag=0 data

Page 67: 第五章 数组和广义表

5.5 广义表的表示方法

任何一个非空列表都可以分解成表头和表尾,而一对确定的表头和表尾可以唯一确定一个列表。可以采用头、尾指针的链表结构。

Page 68: 第五章 数组和广义表

1) 表头、表尾分析法:

构造存储结构的两种分析方法 :

若表头为原子,则为

空表 ls=NIL

非空表 lstag=1

指向表头的指针

指向表尾的指针

tag=0 data

否则,依次类推。

Page 69: 第五章 数组和广义表

例如:L=(a, (x, y), ((x)) )

a ((x, y), ((x)) )

(x, y) ( ((x)) )

x (y) ((x)) ( )

y ( ) (x) ( )

x ( )

Page 70: 第五章 数组和广义表

L = ( a, ( x, y ), ( ( x ) ) )a ( x, y ) ( )L = ( )( )x

Page 71: 第五章 数组和广义表

2) 子表分析法:可以看成由 n 个子表构成(长度为 n )。表头指针指向对应的子表,表尾指针指向下一个元素。

若子表为原子,则为

空表 ls=NIL非空表

1

指向子表 1 的指针

tag=0 data

否则,依次类推。

1

指向子表 2 的指针

1

指向子表 n 的指针

ls …

Page 72: 第五章 数组和广义表

例如 :

a (x, y) ((x))

LS=( a, (x,y), ((x)) )

ls

Page 73: 第五章 数组和广义表

例如 :

(A) ((B),C) D

LS=((A),((B),C),D)

ls

Page 74: 第五章 数组和广义表

5.6 广义表操作的递归函数递归函数 一个含直接或间接调用本函数语句的函数被称之为递归函数,它必须满足以下两个条件:

1) 在每一次调用自己时,必须是 (在某 种意义上 )更接近于解 ;2) 必须有一个终止处理或计算的准则。

Page 75: 第五章 数组和广义表

例如 : 梵塔的递归函数void hanoi (int n, char x, char y, char z) { if (n==1) move(x, 1, z); else { hanoi(n-1, x, z, y); move(x, n, z); hanoi(n-1, y, x, z); } }

Page 76: 第五章 数组和广义表

二叉树的遍历 void PreOrderTraverse( BiTree T,void (Visit)(BiTree P))

{ if (T) { Visit(T->data); (PreOrderTraverse(T->lchild, Visit);

(PreOrderTraverse(T->rchild, Visit);

} } // PreOrderTraverse

Page 77: 第五章 数组和广义表

一、分治法 (Divide and Conquer)

(又称分割求解法 )

如何设计递归函数?

二、后置递归法 (Postponing the work)

三、回溯法 (Backtracking)

Page 78: 第五章 数组和广义表

对于一个输入规模为 n 的函数或问题,用某种方法把输入分割成 k(1<k≤n) 个子集,从而产生 l 个子问题,分别求解这 l 个问题,得出 l 个问题的子解,再用某种方法把它们组合成原来问题的解。若子问题还相当大,则可以反复使用分治法,直至最后所分得的子问题足够小,以至可以直接求解为止。

分治法的设计思想为 :

Page 79: 第五章 数组和广义表

在利用分治法求解时,所得子问题的类型常常和原问题相同,因而很自然地导致递归求解。

Page 80: 第五章 数组和广义表

例如 : 梵塔问题 : Hanoi(n, x, y, z)

可递归求解 Hanoi(n-1, x, z, y)

将 n 个盘分成两个子集 (1至 n-1

和 n ) ,从而产生下列三个子问题:

1) 将 1至 n-1号盘从 x 轴移动至 y 轴;

3) 将 1至 n-1号盘从 y轴移动至 z轴;

2) 将 n号盘从 x 轴移动至 z 轴;

可递归求解 Hanoi(n-1, x, z, y)

Page 81: 第五章 数组和广义表

又如 : 遍历二叉树 : Traverse(BT)

可递归求解 Traverse(LBT)

将 n 个结点分成三个子集 (根结点、左子树 和右子树 ) ,从而产生下列三个子问题:1) 访问根结点;

3) 遍历右子树 ;

2) 遍历左子树;

可递归求解 Traverse(RBT)

Page 82: 第五章 数组和广义表

广义表从结构上可以分解成广义表 = 表头 + 表尾

或者广义表 = 子表 1 + 子表 2 + ··· + 子表 n

因此常利用分治法求解之。算法设计中的关键问题是,如何将 l 个子问题的解组合成原问题的解。

Page 83: 第五章 数组和广义表

广义表的头尾链表存储表示:typedef enum {ATOM, LIST} ElemTag; // ATOM==0: 原子 , LIST==1: 子表typedef struct GLNode { ElemTag tag; // 标志域 union{ AtomType atom; // 原子结点的数据域 struct {struct GLNode *hp, *tp;} ptr; };} *GList

tag=1 hp tp

ptr

表结点

Page 84: 第五章 数组和广义表

例一 求广义表的深度

例二 复制广义表

例三 创建广义表的存储结构

Page 85: 第五章 数组和广义表

广义表的深度 =Max { 子表的深度 } +1

例一 求广义表的深度

可以直接求解的两种简单情况为: 空表的深度 = 1

原子的深度 = 0

将广义表分解成 n 个子表,分别( 递归 ) 求得每个子表的深度,

Page 86: 第五章 数组和广义表

int GlistDepth(Glist L) {

// 返回指针 L 所指的广义表的深度

for (max=0, pp=L; pp; pp=pp->ptr.tp){ dep = GlistDepth(pp->ptr.hp); if (dep > max) max = dep; } return max + 1; } // GlistDepth

if (!L) return 1;

if (L->tag == ATOM) return 0;

Page 87: 第五章 数组和广义表

1 1 1 L …

for (max=0, pp=L; pp; pp=pp->ptr.tp){ dep = GlistDepth(pp->ptr.hp); if (dep > max) max = dep; }

例如 :pp

pp->ptr.hp

pp pp

pp->ptr.hp pp->ptr.hp

Page 88: 第五章 数组和广义表

例二 复制广义表

新的广义表由新的表头和表尾构成。

可以直接求解的两种简单情况为: 空表复制求得的新表自然也是空表 ;

原子结点可以直接复制求得。

将广义表分解成表头和表尾两部分,分别 ( 递归 ) 复制求得新的表头和表尾,

Page 89: 第五章 数组和广义表

若 ls= NIL 则 newls = NIL

否则 构造结点 newls,

由 表头 ls->ptr.hp 复制得 newhp

由 表尾 ls->ptr.tp 复制得 newtp

并使 newls->ptr.hp = newhp,

newls->ptr.tp = newtp

复制求广义表的算法描述如下 :

Page 90: 第五章 数组和广义表

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

分别复制表头和表尾

Page 91: 第五章 数组和广义表

CopyGList(T->ptr.hp, L->ptr.hp);

// 复制求得表头 T->ptr.hp 的一个副本 L->ptr.hp

CopyGList(T->ptr.tp, L->ptr.tp); // 复制求得表尾 T->ptr.tp 的一个副本 L->ptr.tp

语句 CopyGList(T->ptr.hp, L->ptr.hp);

等价于 CopyGList(newhp, L->ptr.tp);

T->ptr.hp = newhp;

Page 92: 第五章 数组和广义表

例三 创建广义表的存储结构

对应广义表的不同定义方法相应地有不同的创建存储结构的算法。

Page 93: 第五章 数组和广义表

假设以字符串 S = (1, 2, , n )

的形式定义广义表 L ,建立相应的存储结构。 由于 S 中的每个子串 i 定义 L 的一个子表,从而产生 n 个子问题,即分别由这 n 个子串 ( 递归 )建立 n 个子表,再组合成一个广义表。 可以直接求解的两种简单情况为:由串 ( ) 建立的广义表是空表;由单字符建立的子表只是一个原子结点。

Page 94: 第五章 数组和广义表

如何由子表组合成一个广义表?

首先分析广义表和子表在存储结构中的关系。

先看第一个子表和广义表的关系 :

1 L

指向广义表的头指针

指向第一个子表的头指针

Page 95: 第五章 数组和广义表

再看相邻两个子表之间的关系 :

1 1

指向第 i+1 个子表的头指针

指向第 i 个子表的头指针

可见,两者之间通过表结点相链接。

Page 96: 第五章 数组和广义表

若 S = ( ) 则 L = NIL ;否则,构造第一个表结点 *L , 并从串 S 中分解出第一个子串 1 ,对应创建第一个子广义表 L->ptr.hp ; 若剩余串非空,则构造第二个表结点 L->ptr.tp ,并从串 S 中分解出第二个子串 2 ,对应创建第二个子广义表 ……; 依次类推,直至剩余串为空串止。

Page 97: 第五章 数组和广义表

void CreateGList(Glist &L, String S) {

if ( 空串 ) L = NULL; // 创建空表 else {

L=(Glist) malloc(sizeof(GLNode));

L->tag=List; p=L;

sub=SubString(S,2,StrLength(S)-1);

//脱去串 S 的外层括弧

} // else

}

由 sub 中所含 n 个子串建立 n 个子表 ;

Page 98: 第五章 数组和广义表

do {

sever(sub, hsub); // 分离出子表串 hsub=i

if (!StrEmpty(sub) {

p->ptr.tp=(Glist)malloc(sizeof(GLNode));

// 建下一个子表的表结点 *(p->ptr.tp)

p=p->ptr.tp;

}

} while (!StrEmpty(sub));

p->ptr.tp = NULL; // 表尾为空表

创建由串 hsub 定义的广义表 p->ptr.hp;

Page 99: 第五章 数组和广义表

if (StrLength(hsub)==1) {

p->ptr.hp=(GList)malloc(sizeof(GLNode));

p->ptr.hp->tag=ATOM;

p->ptr.hp->atom=hsub; // 创建单原子结点}

else CreateGList(p->ptr.hp, hsub);

// 递归建广义表

Page 100: 第五章 数组和广义表

假如某个问题的求解过程可以分成

若干步进行,并且当前这一步的解可

以直接求得,则先求出当前这一步的

解,对于余下的问题,若问题的性质

和原问题类似,则又可递归求解。

后置递归的设计思想为 :

Page 101: 第五章 数组和广义表

递归的终结状态是,当前的问题可以直接求解,对原问题而言,则是已走到了求解的最后一步。

链表是可以如此求解的一个典型例子。

例如:编写“删除单链表中所有值为 x 的数据元素”的算法。

Page 102: 第五章 数组和广义表

1) 单链表是一种顺序结构,必须从第一个结点起,逐个检查每个结点的数据元素;

分析 :

2) 从另一角度看,链表又是一个递归结构,若 L 是线性链表 (a1, a2, , a

n) 的头指针,则 L->next 是线性链表 (a2, , an) 的头指针。

Page 103: 第五章 数组和广义表

a1 a2 a3 an … L例如 :

a1 a2 a3 an L

a1 a2 a3 an L

已知下列链表

1) “a1=x” ,则 L 仍为删除 x 后的链表头指针

2) “a1≠x” ,则余下问题是考虑以 L->next 为头指针的链表

… a1

L->next

L->next=p->next

p=L->next

Page 104: 第五章 数组和广义表

void delete(LinkList &L, ElemType x) { // 删除以 L为头指针的带头结点的单链表中 // 所有值为 x的数据元素 if (L->next) { if (L->next->data==x) { p=L->next; L->next=p->next; free(p); delete(L, x); } else delete(L->next, x); }} // delete

Page 105: 第五章 数组和广义表

删除广义表中所有元素为 x的原子结点分析 : 比较广义表和线性表的结构特点:

相似处:都是链表结构。

不同处: 1) 广义表的数据元素可能还是个 广义表; 2)删除时,不仅要删除原子结点, 还需要删除相应的表结点。

Page 106: 第五章 数组和广义表

void Delete_GL(Glist&L, AtomType x) { //删除广义表 L 中所有值为 x 的原子结点 if (L) { head = L->ptr.hp; // 考察第一个子表 if ((head->tag == Atom) && (head->atom == x)) { } // 删除原子项 x 的情况 else { }// 第一项没有被删除的情况 }} // Delete_GL

… …

… …

Page 107: 第五章 数组和广义表

p=L; L = L->ptr.tp; // 修改指针free(head); // 释放原子结点free(p); // 释放表结点Delete_GL(L, x); // 递归处理剩余表项

1 L

0 x

1

pL

head

Page 108: 第五章 数组和广义表

if (head->tag == LIST) // 该项为广义表 Delete_GL(head, x);

Delete_GL(L->ptr.tp, x);

// 递归处理剩余表项

1 L

0 a

1

1 head

L->ptr.tp

Page 109: 第五章 数组和广义表

回溯法是一种“穷举”方法。其基本思想为:

假设问题的解为 n 元组 (x1, x2, …,

xn) ,其中 xi 取值于集合 Si 。 n 元组的子组 (x1, x2, …, xi) (i<n) 称为部分解,应满足一定的约束条件。 对于已求得的部分解 (x1, x2, …, xi) ,若在添加 xi+1Si+1 之后仍然满足约束条件,则得到一个新的部分解 (x1, x2, …, xi+1) ,

之后继续添加 xi+2Si+2 并检查之。

Page 110: 第五章 数组和广义表

若对于所有取值于集合 Si+1的 xi+1都不能得到新的满足约束条件的部分解(x1,x2,

,xi+1 ),则从当前子组中删去 xi, 回溯到前一个部分解(x1,x2, ,xi-1 ),重新添加那些值集 Si 中尚未考察过的 xi,看是否满足约束条件。如此反复进行,直至求得满足约束条件的问题的解,或者证明问题无解。

Page 111: 第五章 数组和广义表

例一、皇后问题求解

设四皇后问题的解为 (x1, x2, x3, x4),

其中: xi (i=1,2,3,4) Si={1, 2, 3, 4}

约束条件为:其中任意两个 xi 和 xj 不能位于棋盘的同行、同列及同对角线。 按回溯法的定义,皇后问题求解过程为:解的初始值为空;首先添加 x1=1, 之后添加满足条件的 x2=3 ,由于对所有的 x3{1,2, 3, 4} 都不能找到满足约束条件的部分解 (x1, x2, x3), 则回溯到部分解 (x1), 重新添加满足约束条件的 x2=4, 依次类推。

Page 112: 第五章 数组和广义表

void Trial(int i, int n) { // 进入本函数时,在 n×n棋盘前 i-1 行已放置了互不攻 // 击的 i-1 个棋子。现从第 i 行起继续为后续棋子选择 // 满足约束条件的位置。当求得 (i>n) 的一个合法布局 // 时,输出之。 if (i>n) 输出棋盘的当前布局 ; else for (j=1; j<=n; ++j) { 在第 i 行第 j 列放置一个棋子 ; if ( 当前布局合法 ) Trial(i+1, n); 移去第 i 行第 j 列的棋子 ; }} // trial

Page 113: 第五章 数组和广义表

回溯法求解的算法一般形式:void B(int i, int n) {

// 假设已求得满足约束条件的部分解 (x1,..., xi-1) ,本函 // 数从 xi 起继续搜索,直到求得整个解 (x1, x2, … xn) 。 if (i>n)

else while ( ! Empty(Si)) {

从 Si 中取 xi 的一个值 viSi;

if (x1, x2, …, xi) 满足约束条件 B( i+1, n); // 继续求下一个部分解 从 Si 中删除值 vi;

}} // B

Page 114: 第五章 数组和广义表

综合几点:1. 对于含有递归特性的问题,最好设计递归形式的算法。但也不要单纯追求形式,应在算法设计的分析过程中“就事论事”。例如,在利用分割求解设计算法时,子问题和原问题的性质相同;或者,问题的当前一步解决之后,余下的问题和原问题性质相同,则自然导致递归求解。

Page 115: 第五章 数组和广义表

2. 实现递归函数,目前必须利用“栈”。一个递归函数必定能改写为利用栈实现的非递归函数;反之,一个用栈实现的非递归函数可以改写为递归函数。需要注意的是递归函数递归层次的深度决定所需存储量的大小。

Page 116: 第五章 数组和广义表

3. 分析递归算法的工具是递归树,从递归树上可以得到递归函数的各种相关信息。例如:递归树的深度即为递归函数的递归深度;递归树上的结点数目恰为函数中的主要操作重复进行的次数;若递归树蜕化为单支树或者递归树中含有很多相同的结点,则表明该递归函数不适用。

Page 117: 第五章 数组和广义表

例如 : n=3 的梵塔算法中主要操作 mov

e 的执行次数可以利用下列递归树进行分析 :

move(3, a, b, c)

move(2, a, c, b) move(2, b, a, c)

move(1, a, b, c)

move(1, c, a, b)

move(1, b, c, a)

move(1, a, b, c)

上图递归树的中序序列即为圆盘的移动操作序列。

Page 118: 第五章 数组和广义表

又如 : 求 n! 的递归函数的递归树已退化为一个单枝树 ; 而计算斐波那契递归函数的递归树中有很多重复出现的结点。

n

n-1

1

0

。。。

F5

F4F3

F3 F2

F2 F1

F1 F0

F1 F0

。。。

Page 119: 第五章 数组和广义表

4. 递归函数中的尾递归容易消除。例如:先序遍历二叉树可以改写为: void PreOrderTraverse( BiTree T) {

While (T) {

Visit(T->data);

PreOrderTraverse(T->lchild);

T = T->rchild;

}

} // PreOrderTraverse

Page 120: 第五章 数组和广义表

void delete(LinkList &L, ElemType x) { // L为无头结点的单链表的头指针 if (L) { if (L->data=x) { p=L; L=L->next; free(p); delete(L, x); } else delete(L->next, x); }}

又如 :

Page 121: 第五章 数组和广义表

void delete(LinkList &L, ElemType x) { // L为带头结点的单链表的头指针 p=L->next; pre=L; while (p) { if (p->data=x) { pre->next=p->next; free(p); p=pre->next; } else { pre=p; p=p->next; } }}

可改写

Page 122: 第五章 数组和广义表

5. 可以用递归方程来表述递归函数的 时间性能。

例如 : 假设解 n 个圆盘的梵塔的执行 时间为 T(n)

则递归方程为: T(n) = 2T(n-

1) + C , 初始条件为: T(0) = 0

Page 123: 第五章 数组和广义表

1. 了解数组的两种存储表示方法,并掌握数组在以行为主的存储结构中的地址计算方法。2. 掌握对特殊矩阵进行压缩存储时的下标变换公式。3. 了解稀疏矩阵的两类压缩存储方法的特点和适用范围,领会以三元组表示稀疏矩阵时进行矩阵运算采用的处理方法。

Page 124: 第五章 数组和广义表

4. 掌握广义表的结构特点及其存储表示方法,读者可根据自己的习惯熟练掌握任意一种结构的链表,学会对非空广义表进行分解的两种分析方法:即可将一个非空广义表分解为表头和表尾两部分或者分解为 n个子表。 5. 学习利用分治法的算法设计思想编制递归算法的方法。