Presentation is loading. Please wait.

Presentation is loading. Please wait.

Ch.6 树 2008.5.1.

Similar presentations


Presentation on theme: "Ch.6 树 2008.5.1."— Presentation transcript:

1 Ch.6 树

2 Ch.6 树 树形结构 特征 应用 家谱、行政架构等,计算机系统中的文件目录等 二叉树,树,森林等 结点间有分支,具有层次关系
每个结点最多只有一个直接前驱,但可有多个直间后继. 开始结点 —— 根 终端结点 —— 叶 其余结点 —— 内部结点 应用 家谱、行政架构等,计算机系统中的文件目录等

3 §6.1 树的概念 Def:树是n (n>=0) 个结点的有限集T,T为空时称为空树,否则它满足: 有且仅有一个特定的称为根的结点;
其余结点可分为m(m≥0)个互不相交的子集T1,T2,…Tm,其中每个子集本身又是一棵树,并称之为根的子树

4 §6.1 树的概念 递归定义 刻画了树的固有特性:非空树由若干棵子树构成,子树由较小子树构成. 表示

5 §6.1 树的概念 术语 结点的度:结点拥有的子树数目(树的度) 叶子:终端结点,度为0的结点 分支结点:非终端结点,度>0
内部结点:根之外的分支结点 根:开始结点 孩子、双亲:某结点的子树的根称为该结点的 孩子,该结点为孩子的双亲 直接前驱(双亲) 直接后继(孩子) 兄弟:同一双亲的孩子互为兄弟

6 §6.1 树的概念 术语 路径:道路(自上而下) 祖先和子孙:若k到ks有一路径,则k是ks的祖先,ks是k的子孙.
  若存在一结点序列k1,k2,…,kj,使得ki是ki+1的双亲(1≤i≤j-1),则称该序列是从k1到kj的一条路径或道路,路径长度为边数j-1. 祖先和子孙:若k到ks有一路径,则k是ks的祖先,ks是k的子孙. 结点A的祖先是从根到A的路径上所经过的所有结点. 结点A的子孙是以A为根的子树中的所有结点. 真祖先和真子孙不包含自身 层数:从根起算(为1或0),其余结点的层数是其双亲的层数+1

7 §6.1 树的概念 术语 高(深)度:树中结点的最大层数 堂兄弟:双亲在同一层的结点互为堂兄弟
有序树、无序树:若每结点的各子树看成是从左到右有次序(不能互换)的,则称为有序树,否则为无序树。 不同的有序树,一般讨论有序树 森林:m (m≥0) 棵互不相交的树的集合 树和森林非常接近:删去树根 => 森林           森林加上一根 => 树

8 }其余结点为内部结点. §6.1 树的概念 逻辑特征 §6.2 二叉树
父子关系(非线性关系):任一结点至多有一直接前驱(双亲)结点,但可有多个直接后继(子女)结点. 开始结点:根 终端结点:叶 祖先与子孙关系:是对父子关系的延拓,它定义了树中结点的纵向关系. 横向关系:有序树定义了同一组兄弟间的从左到右的长幼关系,可将其延拓到结点间的横向次序:k1和k2是兄弟,k1在左,则k1的任一子孙在k2的任一子孙的左边. }其余结点为内部结点. §6.2 二叉树 是一种特殊的树型结构,每个结点至多只有二棵子树,一般树可转换为二叉树,计算机用途甚广。

9 §6.2.1 二叉树的定义 Def: 二叉树是n(n≥0)个结点的有限集,它或者是空集,或由一个根结点及两棵互不相交的,分别称作根的左子树和右子树的二叉树组成。 形态

10 §6.2.1 二叉树的定义 与度为2的有序树区别:当某一结点只有一孩子时,有序树中它理所当然为长子,但二叉树中,一个结点只有一个孩子亦需分出其左右。

11 §6.2.2 二叉树的性质 性质1:二叉树的第i层上至多有2i-1个结点(i≥1,根为第1层) pf:归纳法
归纳假设:设所有的j(1≤j<i)命题成立。即:第j层结点数≤2j-1 归纳步骤:j=i时,第i-1层结点数≤2i-2(由归纳假设)   因为,每个结点至多有两个孩子.   所以,第i层上结点数≤2×2i-2=2i-1.

12 §6.2.2 二叉树的性质 性质2.深度为h的二叉树至多有2h-1个结点(h≥1).
Pf:深度一定时,仅当每层上结点达到最大时,该树结点最多. 利用性质1知,深度为h的二叉树至多有: 20+21+…+2h-1 = 2h-1

13 §6.2.2 二叉树的性质 性质3.在任一二叉树T中,设叶子数为n0,度为2的结点数为n2.则n0=n2+1.
Pf: 设n1为度为1的结点总数,则结点总数n等于0度,1度和2度 结点数之和   n = n0+n1+n2 // 二叉树   (6.1) 另一方面,除根外,其余结点均是其双亲的孩子,树中: 孩子结点总数 = n1+2n2  n-1 = n1+2n2 n = n1+2n (6.2) 由6.1和6.2: n0=n2+1

14 §6.2.2 二叉树的性质 满二叉树:深度为h的具有2h-1个结点的二叉树称为满二叉树.
完全二叉树:若一二叉树至多只有最下两层上结点的度数可小于2,且最下一层上的结点都集中在该层最左边的若干位置,则称为完全二叉树.    某结点无左孩子,则它必为叶子    满二叉树是完全二叉树,但反之未必成立.

15 §6.2.2 二叉树的性质 性质4.具有n个结点的完全二叉树高为⌊lgn⌋+1 或⌈lg(n+1)⌉ Pf:
∵ 树高为h,则前h-1层是高为h-1的满二叉树,结点总数 = 2h-1-1. ∴ 2h-1-1 < n ≤ 2h-1 (2h-1 < n+1 ≤ 2h) // 第h层上至少有一个结点 2h-1 ≤ n < 2h //整数 h-1 ≤ lgn < h (h-1 < lg(n+1) ≤ h) ∵ h-1和h是相邻的整数 ∴ h-1 = ⌊lgn⌋ (h =⌈lg(n+1)⌉ ) Ex.6.3,6.4,6.5,6.7

16 §6.2.3 二叉树的存储结构 顺序存储结构 如何将结点线性化,使得在线性序列中的相互位置能反映出结点间的逻辑关系?
若对完全二叉树自上而下,每层自左到右给所有n个结点编号,就能到一个足以反映整个二叉树结构的线性序列. ∵ 完全二叉树除最下层外,各层都充满了结点,每层结点数恰为上层的2倍. ∴ 从一结点编号可推出其双亲,左右孩子,兄弟结点和编号.

17 §6.2.3 二叉树的存储结构 性质5. 设完全二叉树中编号为i的结点简称ki(1≤i≤ n), 则有:
1)若i=1,则结点ki为根,无双亲;若i>1,则ki的双亲是k⌊i/2⌋ 2)若2i≤n,则ki的左孩子为k2i;否则ki无左孩子,即它必 为叶子。//因此,完全二叉树中编号i>⌊n/2⌋的结点必为叶子; 3)若2i+1≤n,则ki的右孩子为k2i+1 ,否则ki无右孩子; 4)若i为奇数且大于1,则ki的左兄弟为ki-1,否则ki无左兄弟; 5)若i为偶数且小于n,则ki的右兄弟为ki+1,否则ki无右兄弟。 证明从略。

18 §6.2.3 二叉树的存储结构 ∵ 上述关系中,编号足以反 映结点间的逻辑关系 ∴ 可将n个结点存储在向量 bt[0..n]中,其中:
bt[1..n] —— 存储编号为1至n的结点

19 §6.2.3 二叉树的存储结构 缺点 对一般的二叉树,须按完全二叉树的编号来存储,浪费空间,最坏情况是右单支树,k个结点需2k-1个结点空间。 结论 这种结构只适用于存储完全二叉树,且插入和删 除不便。

20 §6.2.3 二叉树的存储结构 链式存储结构 结点结构 类型定义 typedef struct node { DataType data;
struct node *lchild, *rchild; } BinTNode; // 结点类型 typedef BinTNode *BinTree; //二叉树类型

21 在二叉树中,所有类型为BinTNode的结点,加上一个指向根的BinTree型头指针root,就构成了二叉树的链式存储结构,称之为二叉链表
例子 特性 空树  root = NULL 叶子  左右指针为空 空指针数:n个结点,共有2n个指针域,但只有n-1个结点是别人的孩子,故空指针数为n+1

22 §6.3 遍历二叉树 概念 定义:沿某搜索路线,依次对树中每个结点均做一次且仅做一次访问。
§6.3 遍历二叉树 概念 定义:沿某搜索路线,依次对树中每个结点均做一次且仅做一次访问。 重要性:是其它运算的基础,很多树上操作均依赖于遍历操作,只是访问结点所做的操作不同。 如何遍历? 遍历线性结构很容易:从开始结点出发,依次访问当前结点的后继,直至终端结点为止。遍历路线只有一条(如单链表,从头指针开始)。 但二叉树中每个结点可能有两个后继,故遍历路线不唯一,须找到适用于每个结点的相同的遍历规则。

23 ∵ 在二叉树的递归定义中,非空树组成为:D、L、R
§6.3 遍历二叉树 ∵ 在二叉树的递归定义中,非空树组成为:D、L、R ∴ 在任一结点上,可按某种次序执行三个操作: 访问根结点(D) 遍历该结点的左子树(L) 遍历该结点的右子树(R) 显然有六种执行次序:

24 §6.3 遍历二叉树 遍历规则(从左到右) DLR,LDR,LRD的差别是访问根的先后次序不同 前序(先序、先根)遍历:DLR
§6.3 遍历二叉树 遍历规则(从左到右) DLR,LDR,LRD的差别是访问根的先后次序不同 前序(先序、先根)遍历:DLR 中序(中根)遍历:LDR 后序(后根)遍历:LRD

25 遍历算法 以中根为例,遍历二叉树定义为: if 二叉树非空 then { (1) 遍历左子树 // 即遍历二叉树
(2) 访问根 // 将(1)(2)和(2)(3)对调后为先根和后根遍历 (3) 遍历右子树 //即遍历二叉树 } // 否则为空操作 (递归结束条件) void Inorder(BinTree T) { // T为二叉树的头指针 if (T) { // T非空,T为空时为空操作 Inorder(T->lchild); // 递归遍历左子树 printf(“%c”, T->data);// 访问根结点,具体问题,此 // 操作不同 Inorder(T->rchild); // 递归遍历右子树 } 时间:O(n)

26 §6.3 遍历二叉树 遍历序列 包络线是递归遍历路线 向下:表示递归调用,更 深一层 向上:表示递归结束,返 回一层
§6.3 遍历二叉树 遍历序列 包络线是递归遍历路线 向下:表示递归调用,更 深一层 向上:表示递归结束,返 回一层 每个结点经过3次,第1次经过时访问所得结点序列为前序,第2次经过时访问所得结点序列为中序,第3次经过时访问所得结点序列为后序。 1个开始结点,1个终端结点,其余结点均有一个直接前驱和一个直接后继,为区别3种次序在前面冠以 叶子的相对次序相同

27 通用的遍历算法 因为访问结点的操作依赖于具体问题,故可将它作为一个函数指针参数放于遍历算法的参数表中,调用时,使其指向具体的访问结点的应用函数 void Inorder( BinTree T, void (*Visit)(DataType x) ) { if (T) { Inorder(T->lchild, Visit); Visit(T->data); // (*Visit)(T->data) Inorder(T->rchild, Visit); } 其中Visit是一函数指针,它指向形如void f(DataType x)的函数,故可将访问结点的操作写在函数f中,通过调用语句 Inorder(root, f); 将f的地址传给Visit。

28 §6.3 遍历二叉树 通用的遍历算法(续) Ex. 6.14,6.33,6.37,6.42,6.43,6.45,6.49
§6.3 遍历二叉树 通用的遍历算法(续) 例如,可将打印操作定义为函数 void print(DataType x) { print(“%c”, x); } 调用Inorder(root, print),即可完成前述算法。 函数名:函数代码的起址。 Ex. 6.14,6.33,6.37,6.42,6.43,6.45,6.49

29 §6.3 遍历二叉树 建立二叉链表 上述算法假定二叉链表已建立 建立二叉树对应的二叉链表方法很多 先序遍历构造法
§6.3 遍历二叉树 建立二叉链表 上述算法假定二叉链表已建立 建立二叉树对应的二叉链表方法很多 先序遍历构造法 输入先序序列,加入虚结点(输入时用空格符“ ”)以示空指针的位置,例如前述的二叉树,输入为: ABDФФФCEФФFФФ

30 §6.3 遍历二叉树 char ch; if ((ch = getchar()) == ‘\n’) return; // 回车结束输入
§6.3 遍历二叉树 void CreateBinTree(BinTree *T) { // 注意T为指针的指针 char ch; if ((ch = getchar()) == ‘\n’) return; // 回车结束输入 if (ch == ‘ ’) // 读入空格 *T = NULL; // 将相应的指针置空 else { // 读入的是结点数据 *T = (BinTNode*) malloc(sizeof(BinTNode)); (*T)->data = ch; // 生成新结点,相当于访问根节点 CreateBinTree(&(*T)->lchild); // 遍历左子树 CreateBinTree(&(*T)->rchild); // 遍历右子树 } 建树时调用 CreateBinTree(&root), 将root(BinTree类型)的地址复制给T,故修改*T就相当于修改了实参root本身。 时间:O(n)

31 §6.4 线索二叉树 基本概念 在一基本数据结构上常常需要扩充,增加辅助信息,其目的是: ①开发新操作;②加速已有操作。
§6.4 线索二叉树 基本概念 在一基本数据结构上常常需要扩充,增加辅助信息,其目的是: ①开发新操作;②加速已有操作。 线索 —— 利用空指针域(n+1个)存放指向结点在某种遍历次序下的前驱和后继指针 线索链表 —— 加上线索的二叉链表 线索二叉树 —— 相应的二叉树称为线索二叉树 目的:加速遍历操作 加速查找任一结点在某种遍历次序下的前驱和后继操作 如何区别结点的指针域 孩子指针:指向孩子? 线索指针:指向某种遍历次序下的前驱和后继的线索?

32 §6.4 线索二叉树 基本概念 结点结构 线索标志 中序线索树和中序线索链表 中序序列:CBDAFHGIE

33 §6.4 线索二叉树 基本概念 中序线索树中必有两个指针域为空 前序线索树中,有几个指针域为空?
§6.4 线索二叉树 基本概念 中序线索树中必有两个指针域为空 前序线索树中,有几个指针域为空? 前序序列的开始结点为根,故当它的左子树非空时,其指针域指向左指子树,此时前序序列开始结点左指针非空。 但是,前序序列的终端结点的右指针必为空。 仅当只有1个根结点或根的左子树为空时有两个空指针。

34 §6.4 线索二叉树 基本概念 后序线索树中,开始结点的左指针必为空,仅当只有1个根时或根的右子树为空时有两个空指针。 与前序线索树对称
§6.4 线索二叉树 基本概念 后序线索树中,开始结点的左指针必为空,仅当只有1个根时或根的右子树为空时有两个空指针。 与前序线索树对称 带头结点的线索链表 书上6.11图(b). 令空指针也指向此哨兵

35 线索化 设p和pre分别指向遍历过程中当前访问结点和其前驱,即*pre和*p是前驱和后继的关系,其中pre为全局量,在遍历过程中建立线索。
线索树中,为何判定结点是否为叶子? ltag = rtag = 1 (适用于三种线索树) 有时线索树只有左线索或右线索之一 线索化 将二叉树变为线索树的过程 按某种次序遍历,在遍历过程中用线索取代空指针。 typedef enum {Link, Thread} PointerTag; // 0为Link,1为Thread typedef struct node { DataType data; PointerTag ltag, rtag; struct node *lchild, *rchild; } BinThrNode, *BinThrTree; 设p和pre分别指向遍历过程中当前访问结点和其前驱,即*pre和*p是前驱和后继的关系,其中pre为全局量,在遍历过程中建立线索。 以中序为例,pre初始为NULL,因为中序前驱对开始结点是NULL。

36 void InorderThreading(BinThrTree p) { // pre为全局量,初值为NULL if (p) {
InorderThreading( p->lchild ); // 左子树线索化 p->ltag = (p->lchild) ? Link : Thread; // 左指针非空,置为 // Link,否则为线索。 p->rtag = (p->rchild) ? Link : Thread; if (pre) { // 若*pre存在 if (pre->rtag == Thread) // 当前结点*p的前驱右标志为线索 pre->rchild = p; // 令*pre的右线索指向中序后继*p if (p->ltag == Thread) // 当前结点的左线索已建立 p->lchild = pre; //令当前节点的左线索指向中序前驱 } pre = p; //使*pre为*p的前驱,循环不变量 InorderThreading( p->rchild ); // 右子树线索化 时间:和遍历相同O(n) 后序(前序)线索化类似于此。 访问根结点

37 §6.4 线索二叉树 操作(加速) (1) 找某结点*p(中序线索树中)中序前驱和中序后继结点 找*p的中序后继
§6.4 线索二叉树 操作(加速) (1) 找某结点*p(中序线索树中)中序前驱和中序后继结点 找*p的中序后继 i) 若*p的右子树空,则p->rchild为右线索,直接指向*p的中序后继 ii)若*p的右子树非空(rtag为0),则*p的中序后继必是其右子树中第一个中序遍历到的结点,其特征是:从*p的右孩子开始,沿其左链往下找,直至找到一个没有左孩子的结点为止,不妨称其为右子树中“最左下”的结点。 p 这里k≥ 1, Rk不一定是叶子,其右子树可为空,可非空。 算法 请自己给出找给定结点*p的中序后继算法。时间O(h),快于无线索的二叉树。

38 §6.4 线索二叉树 加速作用 在普通二叉树中,找*p中序后继: 对于ii)同样有效:O(h) 对于 i)就必须从根开始遍历。
§6.4 线索二叉树 加速作用 在普通二叉树中,找*p中序后继: 对于ii)同样有效:O(h) 对于 i)就必须从根开始遍历。 ∵有线索是直接从线索找到O(1)。但线索一般“向上”指向其祖先,而二叉树中无向上的链接,只能从根开始遍历得到,最坏情况O(n)。 找*p的中序前驱 ∵ 中序是对称序,其方法与①完全对称 i) 若*p的ltag = 1,则中序前驱为lchild ii)若*p的ltag = 0,则中序前驱是*p的左子树中 “最右下”的结点。

39 §6.4 线索二叉树 (2) 找*p的后序前驱和后序后继(在后序线索树中) 找*p的后序前驱(易)
§6.4 线索二叉树 (2) 找*p的后序前驱和后序后继(在后序线索树中) 找*p的后序前驱(易) i) 若*p的ltag = 1(左子树为空),则后序前驱为p->lchild ii)若*p的ltag = 0(左子树非空),则后序前驱为p的左孩子或右孩子 ∵根是在遍历左右子树L和R之后被访问 ∴*p的前驱必是L和R中最后一个遍历到的结点 if (p的右孩子非空) then return p->rchild; // 后序前驱为p的右孩子 else return p->lchild; //后序前驱为p的左孩子 找*p的后序后继(难)(见右图) i) 若*p为根,无后继,返回NULL ii) 若*p是其双亲右子,则*p的后序后继是双亲 iii)若*p是其双亲左子,但*p无右兄弟,则*p的 后序后继亦为双亲

40 §6.4 线索二叉树 (3) 找*p的前序前驱和前序后继 类似于后序的情况分析 讨论:找前序前驱难(涉及双亲) 找前序后继易
§6.4 线索二叉树 找*p的后序后继(续) iv)若*p是其双亲左子,但*p有右兄弟,则*p的 后序后继是其右兄弟子树中1st后序遍历到的结点,它是该子树中“最左下的叶结点” 结论:找后序前驱易 找后序后继难,因为: 只有*p的右子树为空(rtag = 1)时,p->rchild是线索,可直接找到; 否则,一般须涉及*p的双亲,故仅给出*p时,须从根遍历。 (3) 找*p的前序前驱和前序后继 类似于后序的情况分析 讨论:找前序前驱难(涉及双亲) 找前序后继易

41 §6.4 线索二叉树 (4) 遍历线索二叉树 遍历某次序的线索二叉树,只要从该次序下的开始结点出发,反复找其后继直至终端结点为止。 中序
§6.4 线索二叉树 (4) 遍历线索二叉树 遍历某次序的线索二叉树,只要从该次序下的开始结点出发,反复找其后继直至终端结点为止。 中序 找开始结点(最左下结点),找当前结点中序后继,直至终端结点(p->rchild = NULL) 头结点的右指针指向开始结点较方便 前序 找开始结点(根),找当前结点的前序后继,直至终端结点 (p->rchild = NULL) 头结点的右指针指向终端结点较方便 后序 找终端结点(根),找当前结点的后序前驱,直至开始结点 (p->lchild = NULL),得到的是后序序列的逆序列 头结点的右指针指向开始结点较方便 时间:仍为O(n),但因为非递归,略快于递归的方法 对遍历而言 前序线索树中,只需保留右线索树即可 中序线索树中,保留左、右线索之一均可 后序线索树中,只需保留左线索即可

42 §6.5 树和森林 树、森林与二叉树的转换 树中每个结点有多个孩子  二叉树只有两个孩子 树  二叉树
§6.5 树和森林 树、森林与二叉树的转换 树  二叉树 树中每个结点有多个孩子  二叉树只有两个孩子 长子及右邻兄弟  二叉树的左右孩子 节点X的长子是其左子,X的右兄弟是其右子 每个结点仅保留与长子的连线 所有兄弟结点间连线 A J I H G F E D C B 6 . 5 - 1 = >

43 §6.5 树和森林 树、森林与二叉树的转换 森林  二叉树 将各树转换为二叉树(根无右兄弟,所以无右子) 将各根作为兄弟互连

44 §6.5 树和森林 树、森林与二叉树的转换 二叉树  树或森林 设x是y的左孩子,则将x的右孩子,右孩子的右孩子,都与y相连
§6.5 树和森林 树、森林与二叉树的转换 二叉树  树或森林 设x是y的左孩子,则将x的右孩子,右孩子的右孩子,都与y相连 去掉所有双亲到右孩子的连线

45 §6.5 树和森林 树的存储结构 双亲链表表示法 ∵每结点双亲唯一,故存储结点时,增加一个parent域指向双亲,用向量表示较方便
J I H G F E D C B §6.5 树和森林 树的存储结构 双亲链表表示法 ∵每结点双亲唯一,故存储结点时,增加一个parent域指向双亲,用向量表示较方便 特点:向上链接,根的parent为-1 求指定结点双亲(O(1))及祖先(O(h))方便 求指定结点孩子及后代须遍历数组O(n) 类型说明(略) A B D C E F G H I J - 1 2 3 4 5 6 7 8 9 10 size data parent Fig 6.5 对应

46 树的存储结构 ∵ 树边n-1条 孩子链表表示法 若k叉树用k叉链表表示,会导致浪费空间 ∴ 空指针kn-(n-1)=n(k-1)+1
设度数域,结点不等长、运算不便 孩子链表:每结点设一孩子链表,将结点及相应孩子链表的头指针放在一向量中。 A J I H G F E D C B

47 树的存储结构 孩子链表表示法(续) 孩子兄弟链表表示法 树  二叉树时,结点关系由:最左孩子、右邻兄弟表示
特点:易实现找结点的孩子及子孙(向下查找易) 难实现找结点的双亲及祖先(向上查找难) 双亲孩子链表表示法 在孩子链表中,增加parent域 此方法结合了双亲链表和孩子链表的优点,向上向下查找均方便 类型说明:略 孩子兄弟链表表示法 树  二叉树时,结点关系由:最左孩子、右邻兄弟表示

48 §6.5 树和森林 树和森林的遍历 先序遍历树 先访问树的根;然后依次先序遍历根的每棵子树 后序遍历树 先依次后序遍历根的每棵子树;然后访问树的根 先序遍历森林 后序遍历森林(中序遍历) Ex.6.16,6.19,6.21,6.22,6.23,6.56

49 §6.6 Huffman树及其应用 §6.6.1 最优二叉树 概念 结点路径长度:根到该结点所经过的边数
树的路径长度:所有结点的路径长度之和 (结点数相同时,完全二叉树的路径长度最短) 结点的带权路径长度: 结点的权值Wi × 结点的路径路径长度li 树的带权路径长度(实际上是加权外部路径长度) 所有叶子的加权路径长度之和

50 §6.6 Huffman树及其应用 §6.6.1 最优二叉树 最优二叉树(Huffman树)
在权为w1,w2,…,wn的叶子所构成的所有的二叉树中,WPL最小的二叉树称为最优二叉树 例 n=4,{a:7, b:5, c:2, d:4} 若叶子权值相同,完全二叉树一定是最优二叉树,否则不一定

51 §6.6.1 最优二叉树 构造最优二叉树 显然,权越大应离根越近,Huffman首先提出了构造方法:
1)初始森林:根据给定的n个权{w1,w2,…,wn}构成一个包含n棵二叉树的森林F={T1,T2,…,Tn}. 其中Ti只有一个根(亦为叶子)结点,其权为wi; 2)合并:在F中选两棵根的权最小的二叉树(若不止两棵,则任选)作为左、右孩子,将其合并为一棵新树,新根权值为这两个结点的权值之和; 3)对新森林F重复2),直至只剩下一棵树为止。

52 §6.6 Huffman树及其应用 §6.6.1 最优二叉树 构造最优二叉树 算法特点 初始有n棵树,每棵树仅有一个孤立结点
每合并一次产生1新结点,且新结点度为2,故 最终的Huffman树有2n-1个结点: 实际上任何严格二叉树中,叶子树为n时,总结点数为2n-1。

53 §6.6 Huffman树及其应用 §6.6.1 最优二叉树 构造最优二叉树 存储结构 #define m 2*n-1 // 结点总数
#define n // 叶子数 #define m 2*n-1 // 结点总数 typedef struct { float weight; // 权,不妨设≥0 int lchild, rchild, parent; // 指针 } HTNode; typedef HTNode HuffmanTree[m]; parent域作用:找双亲结点 为-1时表示根,区分根与非根 构造算法

54 §6.6.1 最优二叉树 void CreateHuffmanTree (HuffmanTree T) { // 构造最优树,根为T[m-1] int i, p1, p2; InitHuffmanTree(T); // 初始化, 将2n-1个结点的3个指针置空(-1),权置为0 InputWeight(T); // 输入n个叶子(根)的权存于T[0..n-1]中,初 //始化森林F0中的根权 for (i = n; i < m; i++){ // 对F中的树合并n-1次,新根依次放入T[i]中,最终 //根为T[m-1] SelectMin(T, i-1, &p1, &p2); // 在当前森林T[0..i-1]的所有结点中,选权 //最小和次小的两个根结点T[p1]和T[p2]作为合并对象,0≤p1,p2≤i-1 T[p1].parent = T[p2].parent = i; // 合并产生的新根为T[i] T[i].lchild = p1; T[i].rchild = p2; T[i].weight = T[p1].weight + T[p2].weight } 算法中用到的三个函数略 例1:以7,5,1,4,8,10,20这7个权值构造huffman树

55 T[0..12] 初始森林:7,5,1,4,8,10,20 1st合并: 7,5,1,4,8,10,20,5 2nd合并:7,5,1,4,8,10,20,5,10 3rd合并: 7,5,1,4,8,10,20,5,10,15 4th合并: 7,5,1,4,8,10,20,5,10,15,20 5th合并: 7,5,1,4,8,10,20,5,10,15,20,35 6th合并: 7,5,1,4,8,10,20,5,10,15,20,35,55 55 20 35 10 5 1 4 15 7 8

56 §6.6.1 最优二叉树 权值:7,5,1,4,8,10,20 55 20 35 10 5 1 4 15 7 8 T[0..12]的最终状态
weight parent lchild rchild 权值:7,5,1,4,8,10,20 1 2 3 4 5 6 7 8 9 10 11 12 7 9 -1 5 8 1 4 10 20 11 2 3 15 12 35 6 55 55 20 35 10 5 1 4 15 7 8 T[0..12]的最终状态

57 §6.6.2 Huffman编码 概念 数据压缩 可将数据文件压缩20%~90%,压缩效率取决于文件特征 编解码 编码方案(对字符集编码)
例2: C = {a, b, c, d, e, f}, 设数据文件有10万字符,|C|=6 节约25%空间 a b c d e f 编码总长 频度(万) 4.5 1.3 1.2 1.6 0.9 0.5 定长 000 001 010 011 100 101 30万 变长 111 1101 1100 22.4万

58 §6.6.2 Huffman编码 定长编码:码长⌈lg|C|⌉ 变长编码:问题是解码可能有二义性
例如:设E,T,W编码为00, 01, 0001 解码时对0001串有两种方式:ET, W 问题的原因 —— E的编码是W的编码的前缀 前缀编码 要求字符集中,任一字符的编码皆不是其他字符编码的前缀。 显然,定长编码是前缀编码 最优的前缀码 (Huffman编码):压缩效果最佳的前缀

59 —— 使文件编码总长最短的前缀码是最优前缀码 对同一字符集表示的不同文件,编码方案不同,即根据文件特征 动态编码。
动态编码:对给定文件,先统计字符集C = {c1,c2,…,cn} 中各字符出现的频度fi, 据此设计编码,设ci的码长为li,则编码文件总长度: —— 使文件编码总长最短的前缀码是最优前缀码 对同一字符集表示的不同文件,编码方案不同,即根据文件特征 动态编码。 特点: 费时,效果最佳 静态编码:无需每次压缩前均统计Ci的频度,而是对定义在相同字符集上的大量文件进行统计,得出每个字符Ci 出现的概率pi,据此编码,则平均码长为: —— 平均码长最短的前缀码为最优前缀码 例:例2中a~f 出现的概率为:0.45, 0.13, …, 0.05, 平均码长为 2.24,优于定长编码 (平均码长为3)。 特点: 所有文件使用同一编码,省时 对不同的文件,效果不尽相同

60 §6.6.2 Huffman编码 编码算法 算法思想 利用Huffman树求最优前缀码 例子
step1:用字符ci作为叶子,pi(或fi)做ci的权,构造Huffman树 step2:将树的左右分支分别标记0和1,将根到叶子的路径上的标号依次相连,作为该叶子(字符)的编码。 例子

61 按算法建立的Huffman树唯一

62 §6.6.2 Huffman编码 算法实现 (对字符集编码,即叶子集编码) typedef struct { 上述编码是最优的前缀吗? 最优
∵ 叶子的码长 = 叶子的路径长度li ∴ 既是平均码长,又是二叉树的WPL 即 Huffman树是WPL最小的二叉树 => 平均码长最小 前缀码 ∵ 树中任一叶子不可能是其它叶子的祖先 ∴ 每叶子编码不可能是其它叶子编码的前缀 算法实现 (对字符集编码,即叶子集编码) typedef struct { char ch; // 放字符 char bits[n+1]; // 放位串‘\0’结束,码长不会超过n } CodeNode; // 存放Huffman编码的结点 typedef CodeNode HuffmanCode[n];

63 int c, p, i; // c和p分别指示树T中孩子和父亲的位置 char cd[n+1]; // 临时存放编码
void HuffmanCoding (HuffmanTree T, HuffmanCode H) { // 根据哈夫曼树T求哈夫曼编码表H int c, p, i; // c和p分别指示树T中孩子和父亲的位置 char cd[n+1]; // 临时存放编码 int start; // 指示编码在cd中的起始位置 cd[n] = ‘\0’; // 从后往前放编码 for (i = 0; i < n; i++){ //依次求叶子T[i]的编码 0 ≤ i ≤ n-1 H[i].ch = getchar();//读入叶子T[i]对应的字符,若建树时有字符则无需读入 start = n; c = i; //从叶子T[i]上溯至根,逆向求编码 while((p = T[c].parent) >= 0) { // 到根为止 if (T[p].lchild == c) cd[--start] = ‘0’; // 若T[c]是T[p]的左子树生成代码0 else cd[--start] = ‘1’; // 否则生成代码1 c = p; // 继续上溯 } strcpy(H[i].bits, &cd[start]); // 复制编码位串 } // endfor } // 时间: O(n.h)

64 §6.6.2 Huffman编码 4. 应用 (数据文件的压缩与解压) 压缩数据文件 (对文件编码) for (依次从f1中读入字符c) {
在Huffman编码表H中,找H[i].ch = c 将c转换为H[i].bits写入压缩文件f2中; // 按bits0,1串写入“位”,设f2是二进制文件 } 解压译码 (对压缩文件解码) for (依次读入f2中的位串) { // 直至文件结束 从Huffman树根T[m-1]出发 若当前读入0,走向左孩子,否则走向右孩子; 若到达叶子T[i],便译出字符H[i].ch写入还原文件中,然后 重新从根出发译码 Note: 实际压缩与解压时,编码的0/1位串,不是字符串,即写入压缩文件中的是“位”

65 §6.6.2 Huffman编码 上机题: 写一个二叉树演示系统,要求至少有下述功能
从键盘上输入广义表建立二叉链表 求树高 结点总数,叶子总数等 三种遍历、层次遍历、输出遍历序列 输出二叉树的广义表形式 写一个对英文的txt文件压缩和解压的程序,输出压缩比,请使用动态编码方式。

66 §6.7 回溯法与搜索树 回溯法思想 有一类问题,需要找出它的解集合,或要求找出满足某些约束条件下的最优解,最简单的方法是回溯法。
所谓回溯就是走回头路,即在一定的约束条件下试探地搜索前进,若前进中受阻,则回头另择道路继续搜索(搜索路线是一棵树) N皇后问题(Gauss 德国数学家) 高斯8后问题 (1850提出),即在8×8国际象棋棋盘上,安放8个皇后,要求彼此互不攻击,有多少个解,这些解的格局如何? 他本人未解决此问题 原因: 种格局,92个解(包括对称解)

67 §6.7 回溯法与搜索树 攻击(约束条件) 同一行、列,同一对角线的两皇后互相攻击
i+j: 135度对角线:同一对角线上元素行列号之和相等(2n-1条) ~2n-2 j-i: 45度对角线:同一对角线上元素行列号之差相等(2n-1条) –(n-1),…,0,…,n-1

68 §6.7 回溯法与搜索树 回溯法:从第1行开始依次放置皇后,每行从第1列开始试探位置是否安全,安全则放置,若某行所有位置均不安全,则回溯到上一行,重新放置。 将上述求解过程中棋盘状态的每一步变化用树来表示,则 可用4叉树表示(如书上Fig6.29),该树反映了状态空间中搜 索过程,不满足约束条件的结点不再生长(即被剪枝)。 先序遍历该树 N皇后算法

69 void Queens(i, col, diag45, diag135){ //i等均为值参
设try[0..n-1]存放解,下标为行,值为列,即:设try[i]=j,则(i, j)表示棋盘上第i行,第j列存一皇后,逐行放置,每行只有一皇后故可不考虑行冲突,只要考虑列和2条对角线冲突。 void Queens(i, col, diag45, diag135){ //i等均为值参 //全局量try进入此处时,部分解try[0..i]已求出,col,diag45,diag135 // 是集合,初始调用为Queens(-1, Ф,Ф, Ф) if (i == n-1) print try[0..n-1]; // 输出一个解 else // 试求部分解try[0..i+1],即在(i+1)行上放皇后 for (j = 0; j < n; j++) // 试探在第(i+1)行上放皇后 if (j∉col && j-(i+1) ∉diag45 && (i+1)+j∉diag135) { // (i+1, j)位置安全 try[i+1] = j; Queens(i+1, col∪{j}, diag45∪{j-i-1}, diag135∪{i+j+1}); } // endif } // 回溯过程要跟踪执行过程才能发现

70 §6.7 回溯法与搜索树 N皇后算法(续) 改进 非递归算法参见…,上述算法的执行过程就是Fig6.29的状态树(先序遍历)
实际算法可设置布尔数组(初始值均为false)来测试安全性 放置皇后(i+1, j) 令:col[j] = true, 表示第j列已有皇后 diag45[(n-1)+j-(i+1)]=true,表示该对角线上已有皇后 // 移位n-1 diag135[(i+1)+j]=true,表示该对角线上已有皇后 测试安全性 if (!col[j] && !diag45[n-i+j-2] && !diag135[i+1+j]) Note: 用数组要注意它们不是值参,因此可将其说明为结构,内含数组

71 §6.8 树的计数 问题:一棵具有n个结点的二叉树有多少种不同形态? 二叉树相似: T和T’相似: 二者皆空,否则
指形态相同,不考虑结点中数据是否相同,否则为树等价 二叉树的计数问题:求n个结点互不相似的二叉树数目bn. 二者皆空,否则 二者不空时,它们的左右子树相似

72 §6.8 树的计数 树的计数问题 n个结点的树的形态数目 ,∵树转换为二叉树后,根无右子树 遍历序列能否唯一确定一棵二叉树? 一般情况:
利用生成函数可求出此递推公式的解为: 树的计数问题 n个结点的树的形态数目 ,∵树转换为二叉树后,根无右子树 遍历序列能否唯一确定一棵二叉树? 二叉树确定后,其三种遍历序列唯一确定 反之,能唯一确定一棵二叉树吗?

73 由一棵二叉树的前序序列(或后序序列)+中序序列可唯一确定该二叉树
例:已知某二叉树的前序序列:ABCDEFG,中序序列:CBEDAFG,求对应的二叉树。 由前序序列+后序序列不能唯一确定一棵二叉树 例:前:ABC 后:CBA 由一个遍历序列更不能唯一确定一棵二叉树。

74 Ex. 6.26,6.27


Download ppt "Ch.6 树 2008.5.1."

Similar presentations


Ads by Google