420 likes | 565 Views
第六章 树和二叉树. 树是计算机算法最重要的非线性结构。 树中每个数据元素至多有一个直接前驱,但可以有多个直接后继。 树是一种以分支关系定义的层次结构。. 树的基本概念. 一、树( Tree )的定义 a. 树是 n(≥0) 结点组成的有限集合。 {N. 沃恩 } ( 树是 n(n≥1) 个结点组成的有限集合。 {D.E.Knuth}) 在任意一棵非空树中: ⑴有且仅有一个没有前驱的结点 ---- 根 (root) 。 ⑵当 n>1 时,其余结点有且仅有一个直接前驱。 ⑶所有结点都可以有0个或多个后继。
E N D
第六章 树和二叉树 树是计算机算法最重要的非线性结构。 树中每个数据元素至多有一个直接前驱,但可以有多个直接后继。 树是一种以分支关系定义的层次结构。
树的基本概念 一、树(Tree)的定义 a.树是n(≥0)结点组成的有限集合。{N.沃恩} (树是n(n≥1)个结点组成的有限集合。{D.E.Knuth}) 在任意一棵非空树中: ⑴有且仅有一个没有前驱的结点----根(root)。 ⑵当n>1时,其余结点有且仅有一个直接前驱。 ⑶所有结点都可以有0个或多个后继。 b. 树是n(n≥0)个结点组成的有限集合。 在任意一棵非空树中: ⑴有一个特定的称为根(root)的结点。 ⑵当n>1时,其余结点分为m(m≥0)个互不相交的子集T1,T2,…,Tm。 每个集合本身又是一棵树,并且称为根的子树(subtree) 树的固有特性---递归性。即非空树是由若干棵子树组成,而子树又可以由若干棵更小的子树组成。
T T A T=Null B C D A ⑶ E F G H I J ⑵ ⑴ K L M 树的基本概念 (3)中有13个结点,其中A是树根,其余结点分成三个互不相交的子集。 T1={B,E,F,K,L} T2={C,G} T3={D,H,I,J,K} T1,T2,T3都是根A的子树,它们本身又是一棵树。 T1:根B {E,K,L} {F} 根E {k} {L} T2:根C {G} T3:根D {H,M} {I} {J} 根H {M}
树的基本操作 二、树的基本操作 1、InitTree(&T) 初始化 2、DestroyTree(&T) 撤消树 3、creatTree(&T,F) 按F的定义生成树 4、ClearTree(&T) 清除 5、TreeEmpty(T) 判树空 6、TreeDepth(T) 求树的深度 7、Root(T) 返回根结点 8、Parent(T,x) 返回结点 x 的双亲 9、Child(T,x,i) 返回结点 x 的第i 个孩子 10、InsertChild(&T,&p,i,x) 把 x 插入到 P的第i棵子树处 11、DeleteChild(&T,&p,i) 删除结点P的第i棵子树 12、traverse(T) 遍历
树的基本术语 树的结点:包含一个数据元素及若干指向子树的分支。 ●结点的度: 结点拥有子树的数目 ●叶结点 : 度为零的结点 ●分枝结点: 度非零的结点 ●树的度 : 树中各结点度的最大值 ●孩子 : 树中某个结点的子树的根 ●双亲 : 结点的直接前驱 ●兄弟 : 同一双亲的孩子互称兄弟 ●祖先 : 从根结点到某结点j 路径上的所有结点(不包括指定结点)。 ●子孙 : 某结点的子树中的任一结点称为该结点的子孙 ●结点层次: 从根结点到某结点 j 路径上结点的数目(包括结点j) ●树的深度: 树中结点的最大层次 ●有向树 :结点间的连线是有向的。我们所讲的树都是有向的。 ●有序树 : 若树中结点的各子树从左到右是有次序的,称该树为有序树,否则为无序树 ●森林 : 由 m 棵互不相交的树构成 F=(T1,T2,.......Tm) 一棵树去掉根结点后就表成了森林。 对森林加上一个结点,并从该结点引边到各树的根结点就变成了森林。
T A 1、结点和结点间的连线表示法 2、文式图表示法----集合嵌套。 B C D E F G H I J K L M 树的逻辑表示
3、广义表表示法----递归表示。 4、凹式表示法----层次表示。 (A(B(E(J,K,L),F),C(G),D(H(M),I))) 树的逻辑表示
Btree Btree A A Btree Btree A A §6.2 二叉树 一、二叉树的定义 二叉树是n(n≥0)个结点的有限集合。此集合或为空, 或由一个根结点加上至多两个互不相交的左右两个子树组成。 结点的度最大为2。 子树有左右之分,不能颠倒。 二叉树可以有五种基本形态: Btree=Null
二叉树的基本运算 • initBiTree(&T) 初始化 • Root(T) 返回根结点 • Parent(T,x) 返回结点 x 的双亲 • LeftChild(T,x) 返回结点 x 的左孩子 • RighChild(BT,x) 返回结点 x 的右孩子 • creatbiTree(&T) 生成一棵二叉树树 • InsertLChild(&T,y,x) 把 x作为y的左子树插入 • InsertRChild(&T,y,x) 把 x作为y的右子树插入 • DelLChild(&T,y) 删除结点y的左子树 • DelRChild(&T,y) 删除结点y的右子树 • Traverse(T) 遍历二叉树 • clearBiTrr(&T) 把二叉树置成空树
二叉树的性质 【性质1】二叉树的第i层结点数最多为2i-1个(i>0)。 【性质2】深度为K的二叉树至多有2k-1个结点(K>0)。 【性质3】对任何一棵二叉树,设n0,n1,n2分别是度为0,1,2的结点数,则有:n0=n2+1 证明: ∵ n= n0+ n1 + n2 (n为结点总数) b= n1 +2 n2 (b 为分支总数) b=n-1 (除根结点外,任一结点都有分支连入父结点) ∴ n=b+1= n1 +2 n2 +1= n0+ n1 + n2 整理得: n0 = n2 +1
二叉树的性质 满二叉树:深度为 k 且有2k -1 个结点的二叉树。 特点: 1、每一层上的结点数都达到最大。 2、每个结点都有高度相同的子树。 3、叶子结点只可能在最后一层。 可从根结点开始,自上而下,自左至右对结点进行连续编号。 完全二叉树:深度为K的n个结点的二叉树,当且仅当每个结点都与深度为K的满二叉树中编号为1至n的结点一一对应。 特点: 1、叶子结点只可能在层次最大的两层上出现。 2、对任一结点,若其右分支下的子孙的最大层次为L,则其左分支下的最大层次为L或L+1。
1 1 2 3 2 3 4 5 6 7 4 5 完全二叉树 满二叉树 A B C 一般二叉树 F E D 二叉树的性质
二叉树的性质 【性质4】具有n个结点的完全二叉树高度为log2n+1 或 log2n+1。 证明:根据完全二叉树的性质有: (2k-1 –1) +1 ≤n ≤2k-1 即 2k-1≤n≤2k-1 2k-1 ≤ n <2k 有:k= log2n+1 2k-1<n+1≤2k 有:k= log2n+1 【性质5】具有n个结点的完全二叉树具有如下特征: ① i=1 根结点,无双亲 i>1 其双亲结点为 (PARENT(i)= i/2 ② 2i>n 结点i无左孩,否则 lchild(i)=2i ③ 2i+1>n 结点i无右孩,否则 rchild(i)=2i+1
3 5 5 1 2 4 3 2 1 2 4 3 1 1 2 3 4 0 0 5 1 0 2 0 0 0 3 1 2 3 4 5 0 0 二叉树的存储结构 1、顺序存储结构 深度为K的二叉树分配2k -1个元素存储单元。 从根结点开始,按完全二叉树双亲与孩子的关系将结点值存入相应单元。 typedef struct { TElemType *SBT; int k; }SqRiTree; 编号为i的接点放到下标为i-1的单元。
lchild data rchild 二叉链表 三叉链表 lchild data parent rchild ^ d ^ ^ c ^ a ^ ^ b a a b c ^ b ^ c ^ d ^ c ^ 二叉树的存储结构 2、链式存储结构 typedef struct BiTNode{ TElemType data; struct BiTNode *lchild,*rchild; }BiTNode,*BiTree 特点:n个结点的二叉树一定有n+1个空指针域。 度为k、n个结点的树有n(k-1)+1个空指针域。
二叉树的遍历 一、遍历二叉树 指以一定的次序访问二叉树的每个结点,且每个结点仅访问一次。所谓访问结点:可以理解为对结点进行各种处理的抽象。 如输出结点信息,修改结点的数据值等。 假定:二叉树以二叉链表作为存储结构,访问是输出结点值data,遍历二叉树的过程实际上就是按某种规律把二叉树的各结点排成序列。 任何一棵二叉树或子树由三部分组成: 根(N) 左子树(L) 右子树(R) 访问规律有六种:NLR、LNR、LRN、NRL、RDL、RLD。 若限定对子树的访问为先左后右,则只考虑: NLR----前序遍历 先根遍历 先序遍历 LNR----中序遍历 中根遍历 LRN----后序遍历 后根遍历 二叉树的结构:具有递归性。二叉树的遍历是一个递归过程。
- + / a * e f b - c d 二叉树先序遍历 先序遍历算法: 访问根结点 先序遍历左子树 先序遍历右子树 void preorder(BiTree T) { BiTree *p; p=T; if(p!=NULL) { visit(p->data); preorder(p->lchild); preorder (p->rchild); } } - + a * b - c d / e f
L 访根(+) L 访根(a) - R 访根(*) L 访根(b) + / R 访根(-) L 访根(c) a * e f R 访根(d) 返回 b - 返回 返回 c d R 访根(/) L 访根(e) R 访根(f) 返回 返回 访根(-)
二叉树先序遍历 非递归先根遍历 根结点处理的先后次序与其右子树的处理次序满足栈操作特点,可用栈来保存处理过的结点的右指针。每当一个结点被处理时,就把该结点的非空右子树根结点指针入栈。当从根结点开始,沿左子树一直走到末端(左孩子为空)时,若栈不为空,则从栈顶退栈获得指向其右孩子指针。如此重复,直到栈和指针均为空为止. void preorder(BiTree bt) { initstack(s); p=bt; while((p!=NULL)||(!Empty(s))) { while(p!=NULL) { visit(p->data ) ; if(p->rchild) push (s,p->rchild); p=p->lchild; } if(!Empty(s)) pop(s,p); }
- + / a * e f Top Top Top Top Top Top Top b - c d Top
- + / a * e f b - c d 二叉树中序遍历 中序遍历算法: 中序遍历左子树 访问根结点 中序遍历右子树 void inorder(BiTree T) { BiTree *p; p=T; if(p!=NULL) { inorder(p->lchild); visit(p->data); inorder (p->rchild); } } a + b * c - d - e / f
二叉树中序遍历 非递归中序遍历 根结点的扫描顺序与处理的先后次序满足栈操作特点,可用栈来保存扫描的结点指针。当扫描指针为空时,弹出栈顶指针并处理该结点,然后扫描指针指向其右孩子结点。如此重复,直到栈和指针均为空为止. void inorder(BiTree bt) { initstack(s); p=bt; while (p!=NULL || !empty(s)) { while (p) { push (s,p); p=p->lchild; } if (!empty(s)) { pop(s,p); visit(p->data); p=p->rchild; } } }
- + / a * e f b - c d p - + a null null * b null null / - c null null d null null e null null f null null
- + / a * e f b - c d 二叉树后序遍历 后序遍历算法: 后序遍历左子树 后序遍历右子树 访问根结点 void poseorder(BiTree T) { BiTree *p; p=T; if(p!=NULL) { postorder(p->lchild); postorder (p->rchild); visit(p->data); } } a b c d - * + e f / -
二叉链表的建立 二叉树存储结构的建立可按二叉树的遍历方式来实现。 一般以先序遍历方式输入二叉树的结点序列。在输入二叉树结点序列时,可以输入一个约定的结点数据表示该结点无左孩子或无右孩子。若结点数据为字符,可以用空格来表示。将约定的结点数据称为虚结点数据。 Status CreateBinTree(BiTree &T) {ch=getch(); if(ch==' ') T=NULL; else { if(!(T=(BiTNode *)malloc(sizeof(BiTNode)))exit(0); T->data=ch; CreateBinTree((BiTree *)&(T->Lchild)); CreateBinTree((BiTree *)&(T->Rchild)); } return(OK); }
线索二叉树 遍历二叉树实质上是将一个非线性结构线性化为一个线性序列。 在二叉树的二叉链表存储结构中,只能找到结点的左右孩子信息,其直接前驱和直接后继的信息只能在遍历过程中动态得到,效率低。 通过一次遍历,保存其结点前驱和后继信息,实现方法: 1、每个结点增加两个指针域fwd和bwd,分别指向其前驱和后继。 2、利用空指针域来存放结点的直接前驱和直接后继信息。 规定:在空Lchild中放前驱信息,在空Rchild中放其后继信息。 增加两个特征位来区分指针域是指向前驱或后继还是孩子。 Ltag=0 Lchild指向左孩 =1 Lchild指向前驱 Lchild=NULL结点无前驱 Rtag=0 Rchild指向右孩 =1 Rchild指向后继 Rchild=NULL结点无后继 存放结点的前驱指针和后继指针称为“线索”。加上线索的二叉链表叫线索链表。加上线索的二叉树叫线索二叉树。对二叉树以某种次序遍历使其为线索二叉树的过程叫线索化。
- + / a e f * b - c d 线索二叉树 在中序线索树中找后继结点: 若结点的右链是线索,其后继结点为右链所指结点。 若结点的右链是指针,其后继结点为中序遍历其右子树时要访问的第一个结点---右子树最左下的结点。 在中序线索树中找前驱结点: 若结点的左链是线索,其前驱为其左链所指结点。 若结点的左链是指针,其前序是中序遍历其左子树时最后访问的结点---左子树中最右下的结点。 二叉线索树存储结构描述: typedef enum{ Link, Thread} PointerTag; typedef struct BiThrNode{ TelemType data; struct BiThrNode *lchild, *rchild; PointerTag ltag, rtag; }BiThrNode, *BiThrTree;
线索二叉树的建立 假设线索二叉树有一个头结点,头结点的Lchild指向二叉树的根结点,rchild作为线索指向中序遍历时访问的最后一个结点,同时令中序遍历的第一个访问结点的Lchild和最后一个结点的rchild均指向头结点。这样可从第一个结点起沿后继进行遍历,从最后一个结点起沿前驱进行遍历。 Status InOrderThreading(BiThrTree &Thrt,BiThrTree T) { if(!(Thrt=(BiThrTree)malloc(sizeof(BiThrNode))) exit(OVERFLOW); Thrt->Ltag=Link;Thrt->Rtag=Thread; Thrt->rchild=thrt; if(T==NULL) Thrt->lchild=Thrt; else{ Thrt->lchild=T;pre:=Thrt; Inthread(T); //中序线索化 Pre->rchild=Thrt;pre->rtag=Thread; Thrt->rchild=pre; } }
线索二叉树的建立 中序线索化: void InThreading(BiThrTree p) { if(p) { InThreading(p->lchild); if(!p->lchild) { p->Ltag=Thread; p->lchild=pre; } else if(!pre->rchild) { pre->Rtag=Tread; pre->rchild=p; } pre=p; InThreading(p->rchild); } }
A B C D E F G H 树的存储结构 一、双亲表示法 以一组连续空间存储树的结点,每个结点附设一个指示器指示其双亲结点在链表中的位置。 #define MAX_SIZE 100 typedef struct PTNode{ TElemType data; int parent; } PTNode; typedef struct{ PTNode nodes[MAX_SIZE]; int n; } 特点:求双亲容易,求孩子困难。 0 1 2 3 4 5 6 7 8
A B C D E F G H data L1 L2 ............. Ln 0 1 2 3 4 5 6 7 A B E G C D ^ B F ^ C ^ D H ^ data d L1 L2 ............. Ld E ^ F ^ G ^ H ^ 树的存储结构 二、孩子表示法:多重链表表示法 每个结点设多个指针域指示多个孩子的存储位置。三种格式: 1、定长结点多重链表 取树的度作为结点指针域的个数。 缺点:空间浪费大。n(k-1)+1个空指针域 2、不定长结点多重链表。-实现困难 缺点:运算复杂,插入和删除困难。 3、多单链表表示法 每个结点设一个链表,把各孩子结点连接起来形成一个单链表。n个头指针组成一个顺序表示的线性表。
A B C D E F G H A ^ ^ F ^ ^ H ^ ^ G B E D ^ ^ C firstchilddatanextsibling 树的存储结构 三、孩子兄弟表示法:树的二叉树表示法或二叉链表表示法。 采用二叉链表作为树的存储结构。结点的两个指针域分别指向该结点的第一个孩子和下一个兄弟。 结点结构: typedef struct CSNode{ ElemType data; struct CSNode *firstchild, *nextsibling; }CSNode, *CSTree; 孩子兄弟表示法给出了将树转换为二叉树和将二叉树转换为树的方法。
A B C D E F G H 树的遍历 三种遍历方法: 先根遍历:先访问根结点,然后依次先根遍历根的每棵子树。 后根遍历:先依次后根遍历根的每棵子树,然后访问根结点。(相当于转换成二叉树后的中序遍历) 层次遍历:先访问根结点,然后从左到由访问第二层结点,再照此访问第三层结点。 先根:ABEFCDGH 后根:EFBCGHDA 层次:ABCDEFGH
n ∑ wk Lk WPL= k=1 哈夫曼树及其应用 • 一、基本术语 • 路径: 从一结点到另一结点上的分支构成这两个结点的路径。 • 路径长度: 路径上的分支数目。 • 树的路径长度: 从根到所有结点的路径长度之和。 • 结点的带权路径长度: 从该结点到树根之间的路径长度与 结点上权值的乘积。 • 树的带权路径长度: 树中所有叶子结点的带权路径长度之和。 n 为叶结点数 wk为叶结点k的权值 Lk 为叶结点k的路径长度
2 7 c a 4 d 5 2 4 7 5 2 4 b c d a b c d 5 7 a b 哈夫曼树 定义: 设有n 个权值 {w1,w2,......wn},试构造具有 n 个叶结点的二叉树,每个叶结点权值为 wi,则其中带权路径长度WPL最小的二叉树称为哈夫曼树(最优二叉树)。 WPL=2*(7+5+2+4)=36 WPL=3*(7+5)+2*4+2=46 WPL=3*(2+4)+2*5+7=35 特点:权值越大的叶子离根越近。 若叶结点上的权值均相同,则完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。
18 7 5 2 4 7 5 6 7 11 7 11 6 2 5 4 5 6 2 4 2 4 哈夫曼树构造 (1) 根据给定的n个权值 {w1,w2,......wn}, 生成 n 棵二叉树的集合F= {T1,T2,.......Tm};其中每棵二叉树Ti只有一个带权为Wi的根结点,左右子树为空。 (2) 在 F 中选择两棵根结点值最小的树 Ti ,Tj作为左右子树,构成一棵新二叉树Tk , Tk根结点值为Ti ,Tj根结点权值之和; (3) 在 F 中删除Ti ,Tj ,并把 Tk 加到 F中; (4) 重复 (2) (3),直到 F中只含一棵树。 例:w={7,5,2,4}
哈夫曼编码 数据的压缩过程称为编码,解压过程称为解码。 编码:将文件中的字符转换为唯一的一个二进制串。 解码:将一个二进制串转换为对应的字符。 定长编码:设编码字符个数为n,码长为k,则k= log2n+1。 不等长编码:使出现频率最多的字符采用尽可能短的编码。 前缀编码:对不等长编码,要求任一字符的编码都不是另一个字符的编码的前缀。 采用二叉树设计前缀编码:用二叉树的叶结点表示待编码的字符,并约定左分支表示字符‘0’,右分支表示字符‘1’,则从根结点到叶子结点的路径上分支字符组成的字符串作为该叶子结点的编码。由此得到的编码必为二进制的前缀编码。 方法:以n种字符出现的频率作权,设计一棵哈夫曼树,由此得到字符的二进制前缀编码为总长最短的二进制前缀编码,这种编码即为哈夫曼编码。
哈夫曼编码实现 哈夫曼树的存储结构:静态链表(连续存储空间)。 哈夫曼树共有2n-1个结点,其中有n个叶结点,n-1个非叶结点。 typedef struc { unsigned int weight; unsigned int parent,lchild,rchild; }HTNode,*HuffmanTree; typedef char **HuffmanCode; 算法的C语言实现: 1、初始化:将树T的所有结点的三个指针均置为空(-1),权值置为0。 2、输入:读入n个叶结点的权值存放于T的前n个分量中。 3、合并:进行n-1次合并,将产生的新结点i依次放入T的第i个分量中(n≤i≤m-1)。合并分两步进行: (1)在当前森林T[0..i-1]的所有结点中,选取权最小和次小的两个根结点T[p1]和T[p2]作为合并对象。(0≤p1,p2≤i-1) (2)将根为T[p1]和T[p2]的两棵树作为左右子树合并为一棵新的树。新树的根为T[i],权值为T[p1]和T[p2]的权值之和。并且T[p1]和T[p2]的parent为i,T[i]的lchild和rchild分别为p1和p2。
哈夫曼编码实现 Void HuffmanCoding(HuffmanTree &HT,HuffmanCode &Hc,int *w,int n) { if(n<=1) return; m=2*n-1 HT=(HuffmanTree)malloc(m*sizeof(HuTNode)); for(p=HT,i=0;i<n;i++,p++) *p={w[i],-1,-1,-1}; for(i=n;i<m;i++,p++) *p={0,-1,-1,-1}; for(i=n;i<m;i++){ //生成哈夫曼树 select(HT,i-1,s1,s2); HT[s1].parent=i; HT[s2].parent=i; HT[i].lchild=s1; HT[i].rchild=s2; HT[i].weight= HT[s1].weight+ HT[s2].weight; } Hc=(Huffmancode*)malloc(n*sizeof(char *)); cd=(char *)malloc(n*sizeof(char)); cd[n-1]=‘\0’; for(i=0;i<n;i++) //哈夫曼编码 { start=n-1; for(c=i, f=HT[i].parent; f!=-1; c=f, f=HT[f].parent) if(HT[f].lchild==c) cd[--strat]=‘0’; else cd[--start]=‘1’; Hc[i]=(char *)malloc((n-start)*sizeof(char)) strcpy(HC[i],&cd[start]); } free(cd); } 编码过程:从叶结点开始向根结点逆向处理。
0 1 0 1 0 1 23 29 0 0 1 1 11 14 0 0 1 1 3 5 7 8 例:编码字符8个 概率{0.05,0.29,0.07,0.08,0.14,0.23,0.03,0.11} w={5,29,7,8,14,23,3,11} 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 5 0 01 1 29 1 0 7 1 1 1 0 8 1 1 1 1 14 1 1 0 23 0 1 3 0 01 0 11 0 0 0
哈夫曼译码的实现 哈夫曼编码是前缀编码,即任何一个字符的编码不是另一个字符的编码的前缀。 译码方法:根据读入的字符编码顺序与哈夫曼树的分支进行匹配,直到达到某个叶子结点,即识别出一个字符。
作业 思考题: 6.2 6.3 6.5 6.8 6.13 6.14 6.15 6.16 6.17 6.26 作业题: 6.27 6.28 6.41 6.42 6.43 6.44 6.47 6.54 6.55 上机实验 p149 5.2 哈夫曼编码/译码