1k likes | 1.22k Views
第 6 章 树. 数据结构( C++ 描述 ). 目录. 6.1 树的基本概念. 6.2 二叉树. 6.3 遍历二叉树. 6.4 线索二叉树. 6 . 5树和森林. 6.6 哈夫曼树. 退出. 6.1 树的基本概念. 6.1.1 树的定义. 1 .树的定义. 树是由 n ( n≥0 )个结点组成的有限集合。若 n=0 ,称为空树;若 n>0 ,则: ( 1 )有一个特定的称为根( root )的结点。它只有直接后继,但没有直接前驱;
E N D
第6章 树 数据结构(C++描述)
目录 6.1 树的基本概念 6.2 二叉树 6.3遍历二叉树 6.4线索二叉树 6.5树和森林 6.6 哈夫曼树 退出
6.1 树的基本概念 6.1.1 树的定义 1.树的定义 树是由n(n≥0)个结点组成的有限集合。若n=0,称为空树;若n>0,则: (1)有一个特定的称为根(root)的结点。它只有直接后继,但没有直接前驱; (2)除根结点以外的其它结点可以划分为m(m≥0)个互不相交的有限集合T0,T1,…,Tm-1,每个集合Ti(i=0,1,…,m-1)又是一棵树,称为根的子树,每棵子树的根结点有且仅有一个直接前驱,但可以有0个或多个直接后继。 由此可知,树的定义是一个递归的定义,即树的定义中又用到了树的概念。 树的结构参见图6-1。
2.树的逻辑结构描述 一棵树的逻辑结构可以用二元组描述为: tree =(k,R) k={ki∣1≤i≤n;n≥0,kielemtype} R={r} 其中,n为树中结点个数,若 n=0,则为一棵空树, n> 0时称为一棵非空树,而关系 r 应满足下列条件: (1)有且仅有一个结点没有前驱,称该结点为树根; (2)除根结点以外,其余每个结点有且仅有一个直接前驱; (3)树中每个结点可以有多个直接后继(孩子结点)。
例如,对图6-1(c )的树结构,可以二元组表示为: K={A,B,C,D,E,F,G,H,I,J,K,L,M} R={r} r={(A,B),(A,C),(A,D),(B,E),(B,F),(C,G),(D,H),(D,I),(E,J),(E,K),(E,L),(H,M)} 3.树的基本运算 树的基本运算可以定义如下几种: (1) inittree(&T) 初始化树T。 (2) root(T) 求树T的根结点。
(3) parent(T,x) 求树T中,值为x的结点的双亲。 (4) child(T,x,i) 求树T中,值为x的结点的第i个孩子。 (5) addchild(y,i,x) 把值为x的结点作为值为y的结点的第i个孩子插入到树中。 (6) delchild(x,i) 删除值为x的结点的第i个孩子。 (7) traverse(T) 遍历或访问树T。
6.1.2 基本术语 1.结点 指树中的一个数据元素,一般用一个字母表示。 2.度 一个结点包含子树的数目,称为该结点的度。 3.树叶(叶子) 度为0的结点,称为叶子结点或树叶,也叫终端结点。 4.孩子结点 若结点X有子树,则子树的根结点为X的孩子结点,也称为孩子,儿子,子女等。如图6-1(c)中A的孩子为B,C,D。
5.双亲结点 若结点X有子女Y,则X为Y的双亲结点。 6.祖先结点 从根结点到该结点所经过分枝上的所有结点为该结点的祖先,如图6-1(c)中M的祖先有A,D ,H 。 7.子孙结点 某一结点的子女及子女的子女都为该结点子孙。 8.兄弟结点 具有同一个双亲的结点,称为兄弟结点。
9.分枝结点 除叶子结点外的所有结点,为分枝结点,也叫非终端结点。 10.层数 根结点的层数为1,其它结点的层数为从根结点到该结点所经过的分支数目再加1。 11. 树的高度(深度) 树中结点所处的最大层数称为树的高度,如空树的高度为0,只有一个根结点的树高度为1。 12.树的度 树中结点度的最大值称为树的度。
13. 有序树 若一棵树中所有子树从左到右的排序是有顺序的,不能颠倒次序。称该树为有序树。 14. 无序树 若一棵树中所有子树的次序无关紧要,则称为无序树。 15.森林(树林) 若干棵互不相交的树组成的集合为森林。一棵树可以看成是一个特殊的森林。
6.1.3 树的表示 1.树形结构表示法 具体参 见图6-1 。
2. 凹入法表示法 具体参见图6-3 。
3. 嵌套集合表示法 具体参 见图6-4 。
4.广义表表示法 对图6-1(c)的树结构,广义表表示法可表示为: (A(B(E(J,K,L),F),C(G),D(H(M),I))) 6.1.4 树的性质 性质1树中的结点数等于所有结点的度加1。 证明:根据树的定义,在一棵树中,除根结点以外,每个结点有且仅有一个直接前驱,也就是说,每个结点与指向它的一个分支一一对应,所以,除根结点以外的结点数等于所有结点的分支数(即度数),而根结点无直接前驱,因此,树中的结点数等于所有结点的度数加1。
性质 2 度为k的树中第i层上最多有ki-1个结点(i≥1)。 下面用数学归纳法证明: 对于i=1,显然成立,假设对于i-1层,上述条件成立,即第i-1层最多有ki-2个结点, 对于第i层,结点数最多为第i-1层结点数的k倍(因为度为k),故第i层的结点数为ki-2*k= ki-1。 性质3深度为h的 k叉树最多有 个结点。 证明:由性质2可知,若每一层的结点数最多,则整个k叉树结点数最多,共有 =k0+k1+…+kh-1= 。 当一棵K叉树上的结点数达到 时,称为满K叉树。
性质4具有n个结点的k叉树的最小深度为logk(n(k-1)+1)。 (请注意,x表示取不小于x的最小整数,或叫做对x上取整。) 证明:设具有n个结点的k叉树的深度为h,即在该树的前面h-1层都是满的,即每一层的结点数等于ki-1个(1≤i≤h-1),第h层(即最后一层)的结点数可能满,也可能不满,这时,该树具有最小的深度。由性质3知道,结点数n应满足下面条件: <n≤ ,通过转换为:kh-1<n(k-1)+1≤kh,再取以k为底的对数后,可以得到: h-1<logk(n(k-1)+1)≤h,即有:logk(n(k-1)+1)≤h<logk(n(k-1)+1)+1,而h只能取整数,所以,该k叉树的最小深度为h=logk(n(k-1)+1)。
6.2 二叉树 6.2.1 二叉树的定义 1.二叉树的定义 和树结构定义类似,二叉树的定义也可以递归形式给出: 二叉树是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵不相交的左子树和右子树组成。 二叉树的特点是每个结点最多有两个孩子,或者说,在二叉树中,不存在度大于2的结点,并且二叉树是有序树(树为无序树),其子树的顺序不能颠倒,因此,二叉树有五种不同的形态,参见图6-5 。
2.二叉树的基本运算 (1)inittree(&T) 二叉树的初始化。 (2)root(T) 求二叉树的根结点。 (3)parent(T,x) 求二叉树T中值为x的结点的双亲。 (4)lchild(T,x) 求二叉树T中值为x的结点的左孩子。
(5) rchild(T,x) 求二叉树T中值为x的结点的右孩子。 (6) lbrother(T,x) 求二叉树T中值为x的结点的左兄弟。 (7) rbrother(T,x) 求二叉树T中值为x的结点的右兄弟。 (8) traverse(T) 遍历二叉树T。
(9) createtree(&T) 建立一棵二叉树T。 (10) addlchild(&T,x,y) 在二叉树T中,将值为y的结点作为值为x的结点的左孩子插入。 (11) addrchild(&T,x,y) 在二叉树T中,将值为y的结点作为值为x的结点的右孩子插入。 (12) dellchild(&T,x) 在二叉树T中,删除值为x 的结点的左孩子。 (13) delrchild(&t,x) 在二叉树T中,删除值为x 的结点的右孩子。
6.2.2 二叉树的性质 性质1若二叉树的层数从1开始,则二叉树的第k层结点数,最多为2k-1个(k>0)。 可以用数学归纳法证明之。 性质2深度(高度)为k的二叉树最大结点数为2k-1(k>0)。 证明: 深度为k的二叉树,若要求结点数最多,则必须每一层的结点数都为最多,由性质1可知,最大结点数应为每一层最大结点数之和。既为 20+21+…+2k-1=2k-1。
性质3 对任意一棵二叉树,如果叶子结点个数为n0,度为2的结点个数为n2,则有n0=n2+1。 证明:设二叉树中度为1的结点个数为n1,根据二叉树的定义可知,该二叉树的结点数n=n0+n1+n2。又因为在二叉树中,度为0的结点没有孩子,度为1的结点有1 个孩子,度为2的结点有2个结孩子,故该二叉树的孩子结点数为 n0*0+n1*1+n2*2 ,而一棵二叉树中,除根结点外所有都为孩子 结点,故该二叉树的结点数应为孩子结点数加1即:n=n0*0+n1*1+n2*2+1因此,有 n=n0+n1+n2=n0*0+n2*1+n2*2+1,最后得到n0=n2+1。 为继续给出二叉树的其它性质,先定义两种特殊的二叉树。
满二叉树 深度为k具有2k-1个结点的二叉树,称为满二叉树。 从上面满二叉树定义可知,必须是二叉树的每一层上的结点数都达到最大,否则就不是满二叉树。 完全二叉树 如果一棵具有n个结点的深度为k的二叉树,它的每一个结点都与深度为k的满二叉树中编号为1~ n的结点一一对应,则称这棵二叉树为完全二叉树。 从完全二叉树定义可知,结点的排列顺序遵循从上到下、从左到右的规律。所谓从上到下,表示本层结点数达到最大后,才能放入下一层。从左到右,表示同一层结点必须按从左到右排列,若左边空一个位置时不能将结点放入右边。
从满二叉树及完全二叉树定义还可以知道,满二叉树一定是一棵完全二叉树,反之完全二叉树不一定是一棵满二叉树。满二叉树的叶子结点全部在最底层,而完全二叉树的叶子结点可以分布在最下面两层。深度为4的满二叉树和完全二叉树如图6-6所示。从满二叉树及完全二叉树定义还可以知道,满二叉树一定是一棵完全二叉树,反之完全二叉树不一定是一棵满二叉树。满二叉树的叶子结点全部在最底层,而完全二叉树的叶子结点可以分布在最下面两层。深度为4的满二叉树和完全二叉树如图6-6所示。
性质4 具有n个结点的完全二叉树高度为log2(n)+1 或 log2(n+1) 。 (注意x表示取不大于x的最大整数,也叫做对x下取整,x表示取不小于x的最小整数,也叫做对x上取整。) 证明:设该完全二叉树高度为k,则该二叉树的前面k-1层为满二叉树,共有2k-1-1个结点,而该二叉具有k层,第k层至少至少有1个结点,最多有2k-1个结点。因此有下面的不等式成立:(2k-1 –1) +1 ≤n ≤(2k-1-1)+2k-1,即有 2k-1≤n≤2k-1。由式子后半部分可知, n≤2k-1…① 由式子前半部分可知 2k-1≤n…② 由①有 n+1≤2k,同时取对数得: log2(n+1)≤k 故 k≥log2(n+1),即 k=log2(n+1)。即得到第二个结论。 由②有2k-1≤n,同时取对数得:k≤log2n+1即 k=log2 n+1,即第一个结论成立,证毕。
性质5如果将一棵有n个结点的完全二叉树从上到下,从左到右对结点编号1,2,…,n,然后按此编号将该二叉树中各结点顺序地存放于一个一维数组中,并简称编号为j的结点为 j(1≤j≤n),则有如下结论成立: (1)若 j=1,则结点j为根结点,无双亲,否则j的双亲为 j/2; (2)若2j≤n,则结点j的左子女为2j ,否则无左子女。即满足2j>n的结点为叶子结点; (3)若2j+1≤n,则结点j的右子女为2j+1,否则无右子女; (4)若结点j序号为奇数且不等于1,则它的左兄弟为j-1; (5)若结点j序号为偶数且不等于n,它的右兄弟为j+1; (6) 结点j所在层数(层次)为log2j+1;
6.2.3 二叉树的存贮结构 1.顺序存贮结构 将一棵二叉树按完全二叉树顺序存放到一个一维数组中,若该二叉树为非完全二叉树,则必须将相应位置空出来,使存放的结果符合完全二叉树形状。如图6-7给出了顺序存贮形式。
对于一棵二叉树,若采用顺序存贮时,当它为完全二叉树时,比较方便,若为非完全二叉树,将会浪费大量存贮存贮单元。最坏的非完全二叉树是全部只有右分支,设高度为K,则需占用2K-1个存贮单元,而实际只有k个元素,实际只需k个存储单元。因此,对于非完全二叉树,宜采用下面的链式存储结构。对于一棵二叉树,若采用顺序存贮时,当它为完全二叉树时,比较方便,若为非完全二叉树,将会浪费大量存贮存贮单元。最坏的非完全二叉树是全部只有右分支,设高度为K,则需占用2K-1个存贮单元,而实际只有k个元素,实际只需k个存储单元。因此,对于非完全二叉树,宜采用下面的链式存储结构。 2.二叉链表存贮结构 (1)二叉链表表示 将一个结点分成三部分,一部分存放结点本身信息,另外两部分为指针,分别存放左、右孩子的地址。 二叉链表中一个结点可描述为:
对于一棵二叉树,若采用二叉链表存贮时,当二叉树为非完全二叉树时,比较方便,若为完全二叉树时,将会占用较多存贮单元(存放地址的指针)。若一棵二叉树有n个结点,采用二叉链表作存贮结构时,共有2n个指针域,其中只有n-1个指针指向左右孩子,其余n+1个指针为空,没有发挥作用,被白白浪费掉了,(当然后面介绍的线索可利用它)。对于一棵二叉树,若采用二叉链表存贮时,当二叉树为非完全二叉树时,比较方便,若为完全二叉树时,将会占用较多存贮单元(存放地址的指针)。若一棵二叉树有n个结点,采用二叉链表作存贮结构时,共有2n个指针域,其中只有n-1个指针指向左右孩子,其余n+1个指针为空,没有发挥作用,被白白浪费掉了,(当然后面介绍的线索可利用它)。 (2)二叉链表的数据类型 二叉链表的数据类型描述如下: struct bitree { elemtype data ; //结点数据类型 bitree *lchild, *rchild;} //定义左、右孩子为指针型
(3)二叉链表的建立 为了后面遍历二叉树方便,先介绍建立二叉链表的算法(假设elemtype 为char型)。 假设二叉链表的数据类型描述如刚才所述,为建立二叉链表,用一个一维表数组来模拟队列,存放输入的结点,但是,输入结点时,必须按完全二叉树形式,才能使结点间满足性质5,若为非完全二叉树,则必须给定一些假想结点(虚结点),使之符合完全二叉树形式。为此,我们在输入结点值时,存在的结点则输入它对应的字符,不存在的结点(虚结点),输入逗号,最后以一个特殊符号 “#”作为输入的结束,表示建二叉链表已完成。建成的二叉链表可以由根指针root唯一确定。
算法描述如下: #include<iostream.h> typedef char elemtype; struct bitree { elemtype data; bitree *lchild,*rchild;}; bitree *create() { bitree *q[100]; //定义q数组作为队列存放二叉链表中结点,100为最大容量 bitree *s; //二叉链表中的结点 bitree *root ; //二叉链表的根指针 int front=1,rear=0; //定义队列的头、尾指针
char ch; //结点的data域值 root=NULL; cin>>ch; while(ch!='#') //输入值为#号,算法结束 { s=NULL; if(ch!=',') //输入数据不为逗号,表示不为虚结点,否则为虚结点 { s=new bitree; s->data=ch; s->lchild=NULL; s->rchild=NULL; } rear++; q[rear]=s; //新结点或虚结点进队
if(rear==1) root=s; else { if((s!=NULL)&&(q[front]!=NULL)) { if(rear%2==0) q[front]->lchild=s; //rear为偶数,s为双亲左孩子 else q[front]->rchild=s;} //rear为奇数,s为双亲右孩子 if(rear%2==1) front++; } //出队 cin>>ch;} return root; }
对图6-9(a)所示的二叉树,要用算法建成图6-10所示的二叉树链表,从键盘输入的数据应为:AB,,C,,,,D#↙ 其中#为输入结束,↙为回车符。
6.3遍历二叉树 所谓遍历二叉树,就是遵从某种次序,访问二叉树中的所有结点,使得每个结点仅被访问一次。 这里提到的“访问”是指对结点施行某种操作,操作可以是输出结点信息,修改结点的数据值等,但要求这种访问不破坏它原来的数据结构。在本书中,我们规定访问是输出结点信息data,且以二叉链表作为二叉树的存贮结构。
由于二叉树是一种非线性结构,每个结点可能有一个以上的直接后继,因此,必须规定遍历的规则,并按此规则遍历二叉树,最后得到二叉树所有结点的一个线性序列。 令L,R,D分别代表二叉树的左子树、右子树、根结点,则遍历二叉树有6种规则:DLR、DRL、LDR、LRD、RDL、RKD。若规定二叉树中必须先左后右(左右顺序不能颠倒),则只有DLR、LDR、LRD三种遍历规则。DLR称为前根遍历(或前序遍历、先序遍历、先根遍历),LDR称为中根遍历(或中序遍历),LRD称为后根遍历(或后序遍历)。
6.3.1前根遍历 所谓前根遍历,就是根结点最先遍历,其次左子树,最后右子树。 1.递归遍历 前根遍历二叉树的递归遍历算法描述为: 若二叉树为空,则算法结束;否则 (1)输出根结点; (2)前根遍历左子树; (3)前根遍历右子树;
算法如下: void preorder(bitree *root) { bitree *p; p=root; if(p!=NULL) {cout<<p->data<<“ ”; preorder(p->lchild); preorder (p->rchild);} }
2.非递归遍历 利用一个一维数组作为栈,来存储二叉链表中结点,算法思想为: 从二叉树根结点开始,沿左子树一直走到末端(左孩子为空)为止,在走的过程中,访问所遇结点,并依次把所遇结点进栈,当左子树为空时,从栈顶退出某结点,并将指针指向该结点的右孩子。如此重复,直到栈为空或指针为空止。
算法如下: void preorder1(bitree *root) { bitree *p,*s[100]; int top=0; //top为栈顶指针 p=root; while((p!=NULL)||(top>0)) { while(p!=NULL) {cout<<p->data<<" "; s[++top]=p; p=p->lchild; } p=s[top--]; p=p->rchild; }}
6.3.2中根遍历 所谓中根遍历,就是根在中间,先左子树,然后根结点,最后右子树。 1.递归遍历 中根遍历二叉树的递归遍历算法描述为: 若二叉树为空,则算法结束;否则 ⑴中根遍历左子树; ⑵输出根结点; ⑶中根遍历右子树。
算法如下: void inorder(biteee *root) { bitree *p; p=root; if (p!=NULL) { inorder(p->lchild); cout<<p->data<<“ ”; inorder(p->rchild); } }
2.非递归遍历 同样利用一个一维数组作栈,来存贮二叉链表中结点,算法思想为: 从二叉树根结点开始,沿左子树一直走到末端(左孩子为空)为止,在走的过程中,把依次遇到的结点进栈,待左子树为空时,从栈中退出结点并访问,然后再转向它的右子树。如此重复,直到栈空或指针为空止。
算法如下: void inorder1( bitree *root) { bitree *p,*s[100]; //s为一个栈,top为栈顶指针 int top=0; p=root; while((p!=NULL)||(top>0)) { while(p!=NULL) {s[++top]=p;p=p->lchild;} {p=s[top--]; cout<<p->data<<" "; p=p->rchild; } }}
6.3.3 后根遍历 所谓后根遍历,就是根在最后,即先左子树,然后右子树,最后根结点。 1.递归遍历 后根遍历二叉树的递归遍历算法描述为: 若二叉树为空,则算法结束;否则 (1)后根遍历左子树: (2)后根遍历历子树; (3)访问根结点。