5.5 广义表的表示方法
头、尾指针的链表结构
表结点:
Tag=1 |
Hp |
Tp |
原子结点:
Tag=0 |
Data |
构造存储结构的两种分析方法:
1) 空表 ls=NIL
非空表
若表头为原子,则为
0 |
Data |
否则,依次类推。
2) 空表ls=NIL
非空表LS=(a1,a2,…,an)
若子表为原子,则为
0 |
Data |
否则,依次类推
5.6 广义表操作的递归函数
递归函数:一个含直接或间接调用本函数语句的函数被称之为递归函数,它必须满足以下两个条件:
1)在每一次调用自己时,必须是(在某种意义上)更接近于解;
2)必须有一个终止处理或计算的准则。
例如:梵塔的递归函数
30_001 |
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); } } |
例如:二叉树的遍历
30_002 |
void PreOrderTraverse(BiTree T, void(Visit)(BiTree P)) { if(T) { Visit(T->data); PreOrderTraverse(T->lchild, Visit); PreOrderTraverse(T->rchild, Visit); } }//PreOrderTraverse |
这一类问题可以借助算法设计的一种方法—分治法(分割求解)(Divide and Conquer)来求解。
分治法的设计思想为:
对于一个输入规模为n的函数或问题,用某种方法把输入分割成k(1<k<=n)个子集,从而产生l个子问题,分别求解这l个问题,得出l个问题的子解,在用某种方法把它们组合成原来的问题的解,若子问题还相当大,则可以反复使用分治法,直至最后所分得的子问题足够小,以至可以直接求解为止。
在利用分治法求解时,所得子问题的类型常常和原问题相同,因而很自然地导致递归求解。
例如:梵塔问题:Hanoi(n,x,y,z)可以分解成三个子问题
1) Hanoi(n-1, x, y, z) –递归
2) Move(n, ‘X’->’Z’)
3) Hanoi(n-1,y,x,z) –递归
n=1时可以直接求解
二叉树的遍历Traverse(BT)分成三个子问题
1) Visite(RootNod)
2) Traverse(LBT) –递归
3) Traverse(RBT)—递归
对空树不需要遍历
广义表从结构上可以分解成广义表=表头+表尾或者广义表=子表1+子表2+……+子表n
依次常用分治法求解。
广义表的头尾链表存储表示:
30_003 |
typedef enum{ATOM, LIST} ElemTag; //ATOM==0:原子,LIST==1:子表 typedef struct GLNode { ElemTag tag; union { AtomType atom; struct{struct GLNode *hp, *tp}ptr; }; } *Glist; |
例一求广义表的深度
广义表的深度=Max{z子表的深度}+1
空表的深度= 1 原子的深度=0
30_004 |
int GlistDepth(Glist L) { if(! L) { return 1; } if(L->tag == ATOM) { return 0; } for(max = 0, pp = L; pp; pp = pp->ptr.tp) { dep = GlistDepth(pp->ptr.hp); if(dep > max) { max = dep; } } return max + 1; }//GlistDepth |
例二复制广义表
若ls=NIL则newls=NIL
否则由表头ls^.hp复制得newhp 由表尾ls^.tp复制得newtp 构造结点newls,并使newls^.hp=newhp,newls^.tp=newtp
31_001 |
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 { CopyGlist(T->ptr.hp, L->ptr.hp); CopyGlist(T->ptr.tp, L->ptr.tp); }//else }//else return OK; } |
例三创建广义表的存储结构
根据LS=’(a1,a2,…,an)’ 建广义表ls
若LS=’()’则ls=NIL
否则构造表结点ls^
分解出第一个子串a1,对应建广义表的表头ls^.hp
若剩余串非空,则构造表尾结点ls^.tp分解出第二个子串a2,对应广义表,依次类推,直至剩余串为空串为止。
31_002 |
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); //脱外层括弧 do { //重复建n个子表 sever(sub, hsub); //分离出子表hsub = a1 if(StrLength(hsub) == 1) { p->ptr.hp = (GList) malloc(sizeof(GNolde)); p->ptr.hp->tag = ATOM; p->ptr.hp->atom = hsub; //创建单原子 } else { CreateGList(p->ptr.hp, hsub); //递归建子表 } if(! StrEmpty(sub) { p->ptr.tp = (Glist) malloc(sizeof(GLNode)); p = p->ptr.tp; //建表尾结点*p } }while(! StrEmpty(sub)); p->ptr.tp = NULL; }//else } |
删除单链表中所有值为x的数据元素
分析:
1) 单链表是一种顺序结构,必须从第一个结点起,逐个检查每个结点的数据元素;
2) 从另一角度看,链表又是一个递归结构,若L是线性表(a1,a2,…,an)的头指针,则L->next是线性链表(a2,…,an)的头指针。
31_003 |
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); } } } |
删除广义表中所有元素为x的原子结点
分析:广义表和线性表比较:
相似处:都是顺序结构
不同处:广义表的数据元素可能还是广义表;删除时,不仅要删除原子结点,还需要删除相应的表结点
综合几点:
1. 对于含有递归特性的问题,最好设计递归形式的算法。但也不要单纯追求形式,应在算法设计的分析过程中“就事论事”。例如,在利用分割求解设计算法时,子问题和原问题的性质相同;或者,问题的当前一步解决之后,余下的问题性质相同,则自然导致递归求解。
2. 实现递归函数,目前必须利用“栈”。一个递归函数必定能改写为利用栈实现的非递归函数函数;反之,一个用栈实现的非递归函数可以改写为递归函数。需要注意的是递归层次的深度决定所需存储量的大小。
3. 分析递归算法的工具是递归树,从递归树上可以得到递归函数的各种相关信息。例如:递归树的深度即为递归函数的递归深度;递归树上的结点数目恰为函数中的主要操作重复进行的次数;若递归树蜕化为单枝树或者递归树中含有很多相同的结点,则表明该递归函数不适用。
4. 递归函数中的尾递归容易消除。
32_001 |
void PreOrderTraverse(BiTree T) { While(T) { Visit(T->data); PreOrderTraverse(T->lchild); T = T->rchild; } }//PreOrder |
5. 可以用递归方程来表述递归函数的时间性能。
例如:假设解n个圆盘的梵塔的执行时间为T(n),则递归方程为:T(n)=2T(n-1)+C,初始条件为:T(0)=0
第5章广义表
补:
4.掌握广义表的结构特点及其存储表示方法,读者可以根据自己的学习习惯熟练掌握任意一种结构的链表,学会对非空广义表进行分解的两种分析方法:即可将一个非空广义表分解为表头和表尾两部分或者分解为n个子表。
5.学习利用分治法的算法设计思想编制递归算法的方法。