Chap 6 树
6.2 树的定义
树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:
l 有且仅有一个特定的称为根(Root)的结点;
l 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1.T2…Tm,其中每一个集合本身又是一棵树,并且称为根的子树(Sub Tree).
l n>0时,根节点是唯一的;
l m>0时,子树的个数没有限制,但他们一定是互不相交的;
6.2.1 结点分类
结点拥有的子树称为结点的度(Degree),度为0的结点称为叶节点(Leaf)或终端节点,不为0的结点称为非终端节点或分支结点。除了根节点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值;
6.2.2 结点间关系
结点的子树称为该结点的孩子(Child),相应的该结点称为孩子的双亲(Parent)。同一个双亲的孩子之间互称兄弟(Sibling)。结点的祖先是从根到该结点所经分支上的所有结点,以某一结点为根的子树中的任一结点都称为该结点的子孙;
6.2.3 树的其他相关概念
节点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。双亲在同一层的结点互称为堂兄弟。树中结点的最大层次称为树的深度(Depth)或高度;
如果树中结点的各子树看成从左至右是有次序的,不能互换的,则乘该树为有序树,否则称为无序树;
森林(Forest)是m(m>=0)棵互不相交的树的集合;
6.3 树的抽象数据类型
InitTree(*T):构造空树;
DestroyTree(*T):销毁树;
ClearTree(*T):若存在则清空;
。。。。。。。。
6.4 树的存储结构
6.4.1 双亲表示法
在每个结点中,附设一个指示器指示其双亲结点在数组中的位置;
[data][parent]
data 数据域,parent 指针域,用来存储双亲在数组中的下标,根结点的指针域为-1;
还可以增加一个域用来存储第一个孩子(firstChild)或兄弟(rightSibling),没有则为-1;
[data][parent] [firstChild]
[data][parent] [rightSibling]
6.4.2 孩子表示法
每个结点有多个指针域,其中每个指针域指向一棵子树的根结点,这种方法叫做多重链表表示法;
l 方案一:指针域的个数等于树的度,但对于树中各结点的度相差很大时,浪费空间,因为有很多结点的指针域是空的;date数据域/child1-childd指针域
[data] [child1] [child2] [child3] [……] [childd]
l 方案二(即孩子表示法):每个结点指针域的个数等于该结点的度,专门取一个位置来存储结点指针域的个数;即date数据域/degree度域/child1-childd指针域;
[data][degree] [child1] [child2] [……] [childd]
² 把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一位数组中;包含两种结点结构:
Ø 表头数组的表头结构(指向的是自己的孩子):data数据域,存储某结点的数据信息;firstchild头指针域,存储该结点的孩子链表的头指针;
[data] [firstChild]
Ø 孩子链表的孩子结点(指向的是自己的兄弟):child数据域,存储某个结点在表头数组中的下标;next指针域,存储指向某结点的下一个孩子结点的指针;
[child] [next]
**双亲孩子表示法:在表头结构中添加一个parent数据域,值为该结点的双亲;
6.4.3 孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟;
[data][firstChild] [rightSibling]
data数据域;firstchild指针域,存储第一个孩子的存储地址;rightsibling指针域,存储该结点的右兄弟结点的存储地址;(可以增加一个parent指针域来查找双亲)(此方法把一棵复杂的树变成了一棵二叉树)
6.5 二叉树的定义
二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成;
6.5.1 二叉树的特点
l 每个结点最多有两棵子树,不存在度大于2的结点;
l 左子树和右子树是有顺序的,次序不能颠倒;
l 某个结点只有一棵子树,也要区分左子树和右子树;
l 二叉树的五种形态:
n 空二叉树;
n 只有一个根结点;
n 根结点只有左子树;
n 根结点只有右子树;
n 根结点左右子树都有;
6.5.2 特殊二叉树
l 斜树:左斜树(只有左子树),右斜树(只有右子树);
l 满二叉树:所有的结点都有左右子树,所有叶子结点都在同一层;
l 完全二叉树:对于一棵具有n个结点的二叉树按层序编号,编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i 的结点在二叉树中位置完全相同;(完全二叉树不一定是满的);
l 二叉树的特点:
n 叶子结点只能出现在最下两层;
n 最下层的叶子结点一定集中在左部连续位置;
n 倒数二层,若有叶子结点,一定都在右侧连续位置;
n 如果结点度为1,则该结点只有左孩子;(此种情况应是最后一个结点吧?)
n 同样点数的二叉树,完全二叉树的深度最小;
6.6 二叉树的性质
l 在二叉树的第i层上至多有2i-1个结点;
l 深度为k的二叉树至多有2k-1个结点(k>=1);
l 对于任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1;设n1为度是1的结点数,那么T的结点总数n=n0+n1+n2;
l 具有n个结点的完全二叉树的深度为[log2n]+1;([x]表示不大于x的最大整数)
l 对一棵有n个结点的完全二叉树的结点按层次编号,对于任一结点(1<=i<=n)有:
n 如果i=1;则结点i是二叉树的根;如果i>1,则其双亲是结点[i/2];
n 如果2i>n,则结点i无左孩子(为叶子结点),否则其左孩子是结点2i;
n 如果2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1;
6.7 二叉树的存储结构
6.7.1 二叉树的顺序存储结构(适用性不强)
用一维数组存储二叉树中的结点,结点的存储位置(数组的下标)要能体现结点间的逻辑关系;
l 完全二叉树:结点按照层序顺序依次存入到数组中;数组下标对应结点层序顺序的编号;
l 一般二叉树:不存在的结点设置为^,但会造成空间的浪费,所以顺序存储结构一般只用于完全二叉树;
6.7.2 二叉链表
二叉树每个结点最多有两个孩子,所以为他设计一个数据域和两个指针域;
[leftChild][data] [rightChild]
还可以增加一个指向其双亲的指针域,称之为三叉链表;
6.8 遍历二叉树(traversing binary tree)
6.8.1 二叉树遍历原理
是指从根结点出发,按照某种次序一次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次;
6.8.2 二叉树遍历方法(若树为空,则空操作返回)
l 前序遍历:从根结点开始,先左子树后右子树;
l 中序遍历:从最左边的叶子或结点开始,先遍历根每个结点的左子树,然后访问根结点,最后遍历右子树,到最右边的叶子或结点终止;
l 后序遍历:从左到右,每棵子树都先叶后结;最后一个访问根结点;
l 层序遍历:从上到下,从左到右按照层序依次遍历;
6.8.3前序遍历算法(根结点在前)
利用递归,先打印结点数据,然后依次递归调用自身函数遍历左子树,右子树;
6.8.4 中序遍历算法(根结点在中)
和前序遍历只是顺序上的不同;
先递归调用自身函数遍历左子树,然后打印结点数据,再遍历右子树;
6.8.5 后序遍历算法(根节点在后)
顺序不同,先递归调用自身函数遍历左子树,再遍历右子树,然后打印结点数据;
6.8.6 推导遍历结果
二叉树遍历性质:
已知前序、中序或后序、中序,可以得到唯一确定的一棵二叉树,但已知前序和后序不可以;
6.9二叉树的建立
和遍历二叉树一样,也是利用递归的原理,只不过是把遍历时打印结点的地方改成生成结点;
6.10线索二叉树
6.10.1 线索二叉树原理
一方面:对于一个有n个结点的二叉链表,一共有2n个指针域,n-1条分支线数,也就是说,存在2n-(n-1)=n+1个空指针域,浪费内存资源;
另一方面:在二叉链表中,如果不遍历的话,不能知道某个节点的前驱和后继是谁,只知道每个结点的左右孩子的地址;
所以,可以考虑利用这些空地址,存放指向结点子某种遍历次序下的前驱和后继结点的地址;
把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree);
把空指针域中的rightChild指向它的后继结点,leftChild指向它的前驱结点;这样就相当于把一棵二叉树转变成了一个双向链表;
把对二叉树以某种次序遍历使其变为线索二叉树的过程称作线索化;
如何区分rightChild/leftChild指向的是左右孩子还是前驱/后继?在每个结点增设两个标志域leftTag/rightTag(boolean型,取值为0(指向孩子)或1(指向前驱/后继));
[leftChild][leftTag] [data] [rightTag] [rightChild]
6.10.2线索二叉树结构实现
实质就是将二叉链表中的空指针改为指向前驱或后继的线索,由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程;
和双向链表结构一样,在二叉树线索链表上添加一个头结点,令其leftChild域的指针指向二叉树的根结点,其rightChild域的指针指向中序遍历时访问的最后一个结点;反之,令二叉树的中序序列中的第一个结点中的leftChild指针域和最后一个结点的rightChild指针域均指向头结点;这样的好处就是:即可以从第一个结点顺着后续进行遍历,也可以从最后一个结点起顺着前驱进行遍历;
6.11树、森林与二叉树的转换
6.11.1 树转换为二叉树
l 加线:在所有兄弟结点之间加一条线;
l 去线:对树中每个结点,只保留它与第一个孩子结点的连线;
l 层次调整:以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使层次分明(注意:第一个孩子是二叉树结点的左孩子,其兄弟转换过来作为它的右孩子);
6.11.2 森林转换为二叉树
l 把每棵树转换为二叉树;
l 第一棵二叉树不动,从第二棵二叉树开始,后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子;
6.11.3 二叉树转换为树
是树转换为二叉树的逆过程;
l 加线:某结点的左孩子的n个右孩子结点都作为此结点的孩子;
l 去线:删除原二叉树中所有结点与其右孩子结点的连线;
l 层次调整;
6.11.4 二叉树转换为森林
先判断二叉树能否转换成森林:即看这棵二叉树的根结点有没有右孩子;
l 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,连线删除,以此类推;
l 再将分离后的二叉树转换为树;
6.11.5 树与森林的遍历
l 树的遍历:
n 第一种-先根遍历:即先访问树的根结点,然后依次先根遍历每棵子树;
n 第二种-后跟遍历:即先依次后根遍历每棵子树,在访问根结点;
l 森林的遍历:
n 第一种-前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样的方式遍历剩余的树;
n 第二种-后序遍历:先访问森林中第一棵树,以后根遍历的方式遍历每棵子树,然后访问根结点,再依次用同样的方式遍历剩余的树;
6.12 赫夫曼树及其应用
6.12.1 赫夫曼树
做数据分层级统计的时候,令根结点做概率最大的数据判断,减少判断次数;
6.12.2 赫夫曼树的定义与原理
中树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称为路径长度;
树的路径长度就是从树根到每一结点的路径长度之和;
树的带权路径长度为树中所有叶子结点的带权路径长度之和(叶子结点带权Wk,每个叶子路径长度为Lk);
其中带全路径长度WPL最小的二叉树为赫夫曼树;
如何构造赫夫曼树:把最小权值树两棵树作为左右子树构造一棵新的二叉树,所得新树的根结点的权值为左右子树的权值之和;以此类推;
6.12.3 赫夫曼编码
利用赫夫曼树原理,左分支代表0,右分之代表1;从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,即赫夫曼编码;
虽然长短不一,但保证了任一字符的编码都不是另一个字符编码的前缀;