920 likes | 1.03k Views
数据结构. 2014 年 3 月. 第六章 树和二叉树. 6.1 树的概念和基本术语 6.2 二叉树 6.3 遍历二叉树和线索二叉树 6.4 树与森林 6.6 霍夫曼树. 6.1 树的概念和基本术语. 一、树的定义 树是由 n (n 0) 个结点的有限集合。如果 n = 0 ,称为空树;如果 n > 0 ,则 有且仅有一个特定的称之为根 (Root) 的结点,它只有直接后继,但没有直接前驱;
E N D
数据结构 2014年3月
第六章 树和二叉树 6.1树的概念和基本术语 6.2二叉树 6.3遍历二叉树和线索二叉树 6.4树与森林 6.6霍夫曼树
6.1树的概念和基本术语 • 一、树的定义 • 树是由 n (n 0) 个结点的有限集合。如果 n = 0,称为空树;如果 n > 0,则 • 有且仅有一个特定的称之为根(Root)的结点,它只有直接后继,但没有直接前驱; • 当n > 1,除根以外的其它结点划分为 m (m >0) 个互不相交的有限集 T1, T2 ,…, Tm,其中每个集合本身又是一棵树,并且称为根的子树(SubTree)。
A B C D E F G H I J K L M 例如 A 只有根结点的树 有13个结点的树 其中:A是根;其余结点分成三个互不相交的子集, T1={B,E,F,K,L}; T2={C,G}; T3={D,H,I,J,M}, T1,T2,T3都是根A的子树,且本身也是一棵树
1 3 2 1 4 5 7 2 4 5 3 7 1 2 4 5 3 7 二、树的表示 • 1、树型表示法 • 2、集合表示法 • 3、括号表示法 (1(2(4,5),3(7))) • 4、凹入表示法
1层 A 2层 B C D height = 4 3层 E F G H I J 4层 K L M 三、树的基本术语 结点 结点的度 叶结点 分支结点 树的深度 树的度 森林 子女 双亲 兄弟 祖先 子孙 结点层次
1 3 2 4 5 7 6 8 9 10 11 12 树的基本术语 树的结点:包含一个数据元素及若干指向子树的分支;孩子结点:结点的子树的根称为该结点的孩子;双亲结点:B结点是A结点的孩子,则A结点是B结点的双亲;兄弟结点:同一双亲的孩子结点;堂兄结点:同一层上结点;结点层:根结点的层定义为1;根的孩子为第二层结点,依此类推;树的高度:树中最大的结点层结点的度:结点子树的个数树的度: 树中最大的结点度。叶子结点:也叫终端结点,是度为0的结点;分枝结点:度不为0的结点;森林;互不相交的树集合;有序树:子树有序的树,(子树不能互换)如:家族树;无序树:不考虑子树的顺序;
1 3 2 4 5 7 6 8 9 10 11 12 四、树的基本操作 1)InitTree(&T); 2)DestroyTree(&T); 3)CreateTree(&T, definition); 4)ClearTree(&T); 5)TreeEmpty(T); 6)TreeDepth(T); 7) Root(T); 8) Value(T, &cur_e); 9) Assign(T, cur_e, value); 10)Paret(T, cur_e); 11)LeftChild(T, cur_e); 12)RightSibling(T, cur_e); 13)InsertChild(&T, &p, i, c); 14)DeleteChild(&T,&p, i); 15)TraverseTree(T, Visit( ));
L R L R 6.2二叉树 (Binary Tree) 一、定义 一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。 特点 每个结点至多只有两棵子树(二叉树中 不存在度大于2的结点) 五种形态
c e a d b f + / - - * 应用举例 例 可以用二叉树表示表达式 具有三个结点的树和二叉树的形态各有几种? a+b*(c-d)-e/f
1层 A 2层 B D height = 4 3层 H I E F 4层 L J K 二、性质 性质1在二叉树的第 i 层上至多有 2i -1个结点。(i 1) [证明用归纳法] 证明:当i=1时,只有根结点,2 i-1=2 0=1。 假设对所有j,i>j1,命题成立,即第j层上至多有2 j-1个结点。 由归纳假设第i-1层上至多有 2i -2个结点。 由于二叉树的每个结点的度至多为2,故在第i层上的最大结点数为第i-1层上的最大结点数的2倍,即2* 2i -2= 2 i-1。
1层 A 2层 B D = =20 + 21 + … + 2 k-1 = 2 k-1 height = 4 3层 H I E F 4层 L J K • 性质2深度为 k 的二叉树至多有 2 k-1个结点(k 1)。 • 证明:由性质1可见,深度为k的二叉树的最大结点数为
1层 A 2层 B D height = 4 3层 H I E F 4层 L J K 性质3对任何一棵二叉树T, 如果其叶结点数为 n0, 度为2的结点数为 n2,则n0=n2+1. 证明:若度为1的结点有 n1个,总结点个数为 n,总边数为 e,则根据二叉树的定义, n = n0 + n1 + n2 e = 2n2 + n1 = n - 1 因此,有 2n2 + n1 = n0 + n1 + n2- 1 n2 = n0- 1 n0 = n2 + 1
1 3 2 4 5 7 6 8 9 10 11 12 13 14 15 两种特殊形态的二叉树 定义1满二叉树 (Full Binary Tree) 一棵深度为k且有2 k-1个结点的二叉树称为满二叉树。 满二叉树
1 1 3 3 2 2 4 5 4 5 6 6 7 1 3 2 4 5 7 6 8 9 10 11 12 定义2完全二叉树 (Complete Binary Tree) 若设二叉树的高度为h,则共有h层。除第 h 层外,其它各层 (0 h-1) 的结点数都达到最大个数,第 h 层从右向左连续缺若干结点,这就是完全二叉树。 完全二叉树 非完全二叉树
1 3 2 4 5 7 6 8 9 10 11 12 性质4具有 n (n 0) 个结点的完全二叉树的深度为log2(n) +1 证明: 设完全二叉树的深度为 h,则根据性质2和完全二叉树的定义有 2h-1- 1 < n 2h- 1或 2h-1 n < 2h 取对数 h-1 < log2n h,又h是整数, 因此有 h = log2(n) +1
0 1 3 1 2 2 4 5 7 3 4 5 6 6 8 9 10 11 12 8 9 7 • 性质5如将一棵有n个结点的完全二叉树自顶向下,同一层自左向右连续给结点编号0, 1, 2, …, n-1,则有以下关系: • 若i = 0, 则 i 无双亲 若i > 0, 则 i 的双亲为(i -1)/2 • 若2*i+1 < n, 则 i 的左子女为 2*i+1,若2*i+2 < n, 则 i 的右子女为2*i+2 • 若结点编号i为偶数,且i!=0,则左兄弟结点i-1. • 若结点编号i为奇数,且i!=n-1,则右兄弟结点为i+1. • 结点i 所在层次为log2(i+1) 性质5:在完全二叉树中编号(1开始)为i的结点, 1)若有左孩子,则左孩编号为2i; 2)若有右孩子,则有孩子结点编号为2i+1; 3)若有双亲,则双亲结点编号为i/2;
顺序表示 1 1 2 3 2 3 4 5 6 7 4 5 6 8 9 7 8 9 10 10 9 三、二叉树的存储结构 1 2 3 4 5 6 7 8 910 1 2 3 4 0 5 6 7 8 0 0 910 完全二叉树 一般二叉树 的顺序表示 的顺序表示
lChild data rChild parent data data rChild lChild rChild lChild • 链表表示 含两个指针域的结点结构 lChild data parent rChild 含三个指针域的结点结构 二叉链表 三叉链表
二叉树链表表示的示例 root root root A A A B B B D D C D C C E F E F E F 二叉树 二叉链表 三叉链表
四、二叉链表的定义 typedef struct BiTNode { //结点定义 TElemType data; struct BiTNode* lchild, * rchild; } BiTNode,*BiTree; BiTree指针数据类型作为二叉树的数据类型。
6.3 二叉树的遍历 一、定义 1、遍历:按某种搜索路径访问二叉树的每个结点,而且每个结点仅被访问一次。 2、访问:含义很广,可以是对结点的各种处理,如修改结点数据、输出结点数据。 遍历是各种数据结构最基本的操作,许多其他的操作可以在遍历基础上实现。对于线性结构由于每个结点只有一个直接后继,遍历是很容易的事 二叉树是非线性结构,每个结点可能有两个后继,如何访问二叉树的每个结点,而且每个结点仅被访问一次?
D E C G B A F 二、二叉树的遍历方法 二叉树由根、左子树、右子树三部分组成 二叉树的遍历可以分解为:访问根,遍历左子树和遍历右子树 令:L:遍历左子树D:访问根结点R:遍历右子树 有六种遍历方法: DLR,LDR,LRD, DRL,RDL,RLD 约定先左后右,有三种遍历方法: DLR、LDR、LRD ,分别称为先序遍历、中序遍历、后序遍历
D E C G B A F 1、先序遍历(DLR) 若二叉树非空 (1)访问根结点; (2)先序遍历左子树; (3)先序遍历右子树; 例:先序遍历右图所示的二叉树 (1)访问根结点A (2)先序遍历左子树:即按DLR的顺序遍历左子树 (3)先序遍历右子树:即按DLR的顺序遍历右子树 先序遍历序列:A,B,D,E,G,C,F
D E C G B A F 2、中序遍历(LDR) 若二叉树非空(1)中序遍历左子树(2)访问根结点 (3)中序遍历右子树 例:中序遍历右图所示的二叉树 (1)中序遍历左子树:即按LDR的顺序遍历左子树 (2)访问根结点A (3)中序遍历右子树:即按LDR的顺序遍历右子树 中序遍历序列: D,B,G,E,A,C,F
D E C G B A F 3、后序遍历(LRD) 若二叉树非空(1)后序遍历左子树(2)后序遍历右子树 (3)访问根结点 例:后序遍历右图所示的二叉树 (1)后序遍历左子树:即按LRD的顺序遍历左子树 (2)后序遍历右子树:即按LRD的顺序遍历右子树 (3)访问根结点A 后序遍历序列: D,G,E,B,F,C,A
- + / * a e f b - c d 例:先中序遍历序遍历、中序遍历、后序遍历下图所示的二叉树 先序遍历序列:-,+,a,*,b,-,c,d,/,e,f 中序遍历序列:a,+,b,*,c,-,d,-,e,/,f 后序遍历序列:a,b,c,d,-,*,+,e,f,/,-
二. 遍历的递归算法 上面介绍了三种遍历方法,显然是用递归的方式给出的三种遍历方法,以先序为例: 先序遍历(DLR)的定义: 先序遍历递归定义 递归项 若二叉树非空 (1)访问根结点; (2)先序遍历左子树 (3)先序遍历右子树; 该定义隐含着若二叉树为空,结束 这实际上是先序遍历的递归定义,我们知道递归定义包括两个部分: 1)基本项(也叫终止项); 2)递归项
上面先序遍历的定义等价于: 若二叉树为空,结束 ——基本项(也叫终止项) 若二叉树非空 ——递归项 (1)访问根结点; (2)先序遍历左子树 (3)先序遍历右子树; 下面给出先序、中序、后序遍历递归算法,为了增加算法的可读性,这里对书上算法作了简化,没有考虑访问结点出错的情况(即我们假设调用函数visit( )访问结点总是成功的。
T A B∧ C∧ ∧E∧ ∧D ∧F∧ • 先序遍历递归算法void PreOrderTraverse(BiTree T, Status(*Visit)(TElemType e)) {//采用二叉链表存贮二叉树, visit( )是访问结点的函数。本算法先序 • //遍历以为根结点指针的二叉树,对每个数据元素调用函数Visit( ) if (T) { //若二叉树为空,结束返回// 若二叉树不为空,访问根结点;遍历左子树,遍历右子树 • Visit(T->data);PreOrderTraverse(T->lchild, Visit); PreOrderTraverse(T->rchild, Visit); }//PreOrderTraverse • 最简单的Visit函数是:Status PrintElement(TElemType e) • { //输出元素e的值printf(e); • return OK; • }
2 中序遍历递归算法 void InOrderTraverse(BiTree T, Status(*Visit)(TElemType e)) {//采用二叉链表存贮二叉树, visit( )是访问结点的函数。本算法中序 //遍历以为根结点指针的二叉树,对每个数据元素调用函数Visit( )if (T) { //若二叉树为空,结束返回// 若二叉树不为空,遍历左子树,访问根结点,遍历右子树InOrderTraverse(T->lchild, Visit); Visit(T->data); InOrderTraverse(T->rchild, Visit); }// InOrderTraverse 你能写出后序遍历递归算法了吧
3 后序遍历递归算法 void PostOrderTraverse(BiTree T, Status(* Visit)(TElemType e)) { //采用二叉链表存贮二叉树, visit( )是访问结点的函数。本算法中序 //遍历以为根结点指针的二叉树,对每个数据元素调用函数Visit( ) if (T) { //若二叉树为空,结束返回// 若二叉树不为空,遍历左子树,遍历右子树,访问根结点PostOrderTraverse(T->lchild, Visit); PostOrderTraverse(T->rchild, Visit); Visit(T->data); }//PostOrderTraverse
A B∧ C∧ ∧E∧ ∧D ∧F∧ 4. 层次遍历递归算法 void levelorder(Bitree bt) { Bitree queue[MAXNODE]; int front,rear; if(bt==NULL) return; front=-1; rear=0; queue[rear]=bt; while(front!=rear) { front++; cout<<queue[front]->data<<" "; if(queue[front]->lchild!=NULL) { rear++; queue[rear]=queue[front]->lchild; } if(queue[front]->rchild!=NULL) { rear++; queue[rear]=queue[front]->rchild; } } }
三、二叉树遍历应用 例1:为二叉树建立二叉链表(递归算法)以递归方式建立二叉树 输入:二叉树的先序序列 结果:二叉树的二叉链表 遍历操作访问二叉树的每个结点,而且每个结点仅被访问一次。是否可在利用遍历,建立二叉链表的所有结点并完成相应结点的链接? 基本思想:输入(在空子树处添加*的二叉树的)先序序列(设每个元素是一个字符//)按先序编历的顺序,建立二叉链表的所有结点并完成相应结点的链接
E E B D F B D F A A C C A * * * * * * * B∧ C∧ ∧E∧ ∧D ∧F∧ 先序序列:A B D F C E T (在空子树处添加*的二叉树的)先序序列: A B D * F * * C E * * * *
Status CreateBiTree(BiTree &T) { //输入(在空子树处添加*的二叉树的)先序序列(设每个元素是一个字 // 符)按先序编历的顺序,建立二叉链表,并将该二叉链表根结点指针 //赋给T scanf (&ch); if (ch= = ‘* ’) T=NULL; // 若ch= = ‘*’ 则T=NULL返回 else { // 若ch! = ‘*’ if (! (T=(BiTNode * )malloc(sizeof(BiTNode)))) exit(OVERFLOW); T->date = ch; // 建立(根)结点 CreateBiTree(T->lchild); //构造左子树链表,并将左子树根结点指针 //赋 给(根)结点 的左孩子域 CreateBiTree(T->rchild); //构造右子树链表,并将右子树根结点指针 //赋给(根)结点 的右孩子域 } return OK; }//CreateBiTree
2. 计算二叉树结点个数(递归算法) int Count(BiTtree T) { if (T==NULL) return 0; else return 1 + Count (T->lchild)+Count(T->rchild); }
3. 求二叉树中叶子结点的个数 int Countleaf(BiTree T ) //求二叉树中叶子结点的数目 { if ( T==NULL ) return 0; //空树没有叶子 if(T->lchild==NULL&&T->rchild==NULL) return 1; //叶子结点 return Countleaf(T->lchild)+Countleaf(T->rchild); //左子树的叶子数加上右子树的叶子数 }
4. 求二叉树高度(递归算法) int Height ( Bitree T ) //Height of a tree { if ( T == NULL ) return -1; else { int m = Height ( T->lchild ); int n = Height ( T->rchild ); return (m > n) ? m+1 : n+1; } }
5. 复制二叉树(递归算法) Bitree Copy( Bitree T ) // copy a tree { if ( T == NULL ) return NULL; Bitree Temp=new Bitnode; Temp->data=T->data; Temp->lchild = Copy( T->lchild ); Temp->rchild = Copy(T->rchild ); return Temp; }
6. 判断二叉树等价(递归算法) int Equal( BinTreeNode *a, BinTreeNode *b) { if ( a == NULL && b == NULL ) return 1; if ( a != NULL && b !=NULL && a->data==b->data && Equal( a->leftChild, b->leftChild) && Equal( a->rightChild, b->rightChild) ) return 1; return 0;//如果a和b的子树不等同,则函数返回0 }
7. 已知一棵二叉树的中根序列和先根序列分别为ABCDEFGHIJK和EBADCFHGIKJ,试画出这棵二叉树。7. 已知一棵二叉树的中根序列和先根序列分别为ABCDEFGHIJK和EBADCFHGIKJ,试画出这棵二叉树。 从先根序列EBADCFHGIKJ可确定根结点是E。因中根序列是先遍历左子树,再遍历根结点,最后遍历右子树,所以从中根序列ABCDEFGHIJK可确定对应的二叉树的左子树由A,B,C,D结点组成,二叉树的右子树由F,G,H,I,J,K结点组成,二叉树的组成示意图如图 (a)所示。
二叉树的左子树也是一棵二叉树,它的先根序列可从整个二叉树的先根序列EBADCFHGIKJ 中找到,为BADC,由此可确定该左子树的根结点是B。从中根序列中的ABCD可确定B结点的左子树由A结点组成,B结点的右子树由C,D结点组成,二叉树的组成示意图可进一步展开,如图 (b)所示。 该思路是个递归的思路,依此类推可得二叉树,如图 (c)所示。 中根序列为ABCDEFGHIJK和先根序列为EBADCFHGIKJ
- b - * c a - - - * c c * c * c * b b a a b a b a -*abc a*b-c ab*c-
a d a c b a a e d c 中序遍历二叉树(非递归算法)用栈实现 b a b退栈 访问 d退栈 访问 d入栈 a b入栈 e c c退栈 访问 a退栈 访问 e 退栈 访问 c e入栈 栈空
a c b e d void InOrder ( BinTree T ) { stack S; InitStack( &S ); //递归工作栈 BinTreeNode *p = T;//初始化 while ( p != NULL || !StackEmpty(&S) ) { if( p != NULL ) { Push(&S, p); p = p->leftChild; } if ( !StackEmpty(&S) ) { //栈非空 Pop(&S, p); //退栈 cout << p->data << endl; //访问根 p = p->rightChild; }//if }//while return ok; }
四、线索二叉树(Threaded Binary Tree) 1 线索二叉树遍历是二叉树最基本最常用的操作。1)对二叉树所有结点做某种处理可在遍历过程中实现;2)检索(查找)二叉树某个结点,可通过遍历实现; 与线性表相比,对二叉树的遍历存在如下问题: 1)遍历算法要复杂的多,费时的多; 2)为检索或查找二叉树中某结点在某种遍历下的后继,必须从根开始遍历,直到找到该结点及后继; 如果能将二叉树线性化,就可以减化遍历算法,提高遍历速度。 如何将二叉树线性化? 以中序遍历为例,我们看到通过遍历可以得到二叉树结点的中序序列。若能将中序序列中每个结点前趋、后继信息保存起来,以后再遍历二叉树时就可以根据所保存的结点前趋、后继信息对二叉树进行遍历。 加上结点前趋后继信息(线索)的二叉树称为线索二叉树
G C E A H D B F 孩子指针 前趋指针 后继指针 线索二叉树 中序遍历序列:D,B,H,E,A,F,C,G 在中序序列中,D的后继是B,H的前趋是B,后继是E… 加上结点前趋后继信息(结索)的二叉树称为线索二叉树
A T B E∧ ∧G∧ C ∧ F∧ ∧D ∧H ∧ 2.线索链表线索二叉树的存贮方法:1) 为每个结点增加二个指针域。缺点:要点用较多的内存空间。 每个n个结点的二叉链表,有n+1个空指针域,故可利用这些的空指针域存放结点的前趋和后继指针,以这种结点的构成二叉链表称为线索链表
data rchild lchild Ltag Rtag 结点结构 Ltag=0, lchild为左子女指针 Ltag=1, lchild为前驱线索 Rtag=0, rchild为右子女指针 Rtag=1, rchild为后继指针