目录
1 基本概念
树是n(n>=0)个节点的有限集,n=0时称为空树。在任意一棵非空树中:
- 有且仅有一个特定的称为根的节点;
- 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树。
树的节点包含一个数据元素及若干指向其子树的分支。
节点拥有的子树数称为结点的度。
度为0的结点称为叶结点或终端结点;度不为0的结点称为分支结点或非终端结点。
根结点之外,分支结点也称为内部结点。
树的度是树内各结点的度的最大值。
树中结点的最大层次称为树的深度或高度。
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
森林是m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
线性结构与树结构的对比:
- 线性结构:第一个数据元素无前驱;最后一个数据元素无后继;中间元素一个前驱一个后继。
- 树结构:根结点无双亲,唯一;叶结点无孩子,可以多个;中间结点,一个双亲多个孩子。
2 树的存储结构
利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示。这里介绍三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟表示法。
2.1 双亲表示法
树这种结构除了根结点外,其余每个结点不一定有孩子,但是一定有且仅有一个双亲。假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置,也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。
使用双亲表示法,由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,双亲表示法如下所示:
这样的存储结构,可以根据结点的parent指针很容易找到它的双亲结点,所用的时间复杂度为O(1),知道parent为-1时,表示找到了树结点的根。可如果需要找到结点的孩子是什么,需要遍历整个结构。
如果需要找到结点的孩子结点,可以增加一个结点最左边孩子的域,这样就可以很容易得到结点的孩子,如果没有孩子的结点,这个域就设置为-1。
如果关注兄弟中间的关系,可以增加一个右兄弟域来体现兄弟关系,也就是说,每一个结点如果它存在右兄弟,则记录下右兄弟的下标,如果右兄弟不存在,则设置为-1。
2.2 孩子表示法
把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数据中。
这样的结构对于查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。
如果需要找到某结点的双亲,需要遍历整棵树才行,稍微改进下结合双亲表示法和孩子表示法就可以避免遍历整棵树,如下:
2.3 孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
这种表示法,给查找某个结点的某个孩子带来来方便,如果要找双亲,这个表示法有缺陷,可是在价格parent指针域来解决。
3 二叉树
二叉树是n个结点的有限集合,该集合或者为空,或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
3.1 二叉树的特点
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两个子树,而是最多有,没有子树或者有一棵子树都是可以的。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
3.2 特殊二叉树
3.2.1 斜树
所有的结点都只有左子树的二叉树叫左斜树;所有结点都是只有右子树的二叉树叫右斜树。这两者都统称为斜树。斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度相同。如下图:
3.2.2 满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树就称为满二叉树。满二叉树的特点:
- 叶子只能出现在最下层,出现在其他层就不可能达成平衡;
- 非叶子结点的度一定是2;
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多;
3.2.3 完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满二叉树。完全二叉树的所有结点与同样深度的满二叉树,它们按层序编号相同的结点是一一对应的,这里有个关键词是按层序编号。例如:
树1中因为5结点没有左子树,却有右子树,那就使得按层序编号的第10个编号空档来;同样树2中由于3结点没有子树,所以使得6、7编号的位置空档来;树3又是因为5编号下没有子树造成10,11编号空档,所以这3个树都不是完全二叉树。
完全二叉树的特点:
- 叶子结点只能出现在最下两层;
- 最下层的叶子一定集中在左部连续位置;
- 倒数二层,若有叶子结点,一定都在右部连续位置;
- 如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况;
- 同样结点树的二叉树,完全二叉树的深度最小;
4 二叉树的性质
性质1: 在二叉树的第i层上最多有个结点(i>=1)
性质2: 深度为k的二叉树最多有个结点(k>=1)
性质3: 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,那么n0 = n2 + 1;分支线总数为结点总数减去1;二叉树中度只可能为0,1,2,所以设其中n0表示度为0的结点树,n1表示度为1的结点树,n2表示度为2的结点树,树T的结点总数为n = n0 + n1 + n2
性质4: 具有n个结点的完全二叉树的深度为(表示不大于 + 1)
性质5: 如果对一棵有n个结点的完全二叉树(其深度为)的结点按层序编号(从第1层到第层,每层从左到右),对任一结点i(1<=i<=n)有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点
- 如果2i > n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
- 如果2i + 1 > n,则结点i无右孩子;否则其右孩子是结点2i + 1
以深度为4,结点树为10的完全二叉树为例:
对于第一条,i=1时就是根结点;i>1时,比如结点7,它的双亲就是不大于7/2的正数3
对于第二条,比如结点6,因为2*6=12大于结点数10,所以结点6无左孩子,它是叶子结点;结点5,因为2*5=10等于结点数10,所以它的左孩子是结点10.
对于第三条,比如结点5,2*5+1=11大于结点树10,所以结点5没有右孩子;结点4,因为2*4+1=9小于结点数10,所以它的右孩子结点是9
5 二叉树的存储结构
5.1 二叉树的顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑,比如双亲与孩子的关系,左右兄弟的关系。
例如一棵完全二叉树:
将这棵完全二叉树按照相应的下标对应的位置存入到数组中,其顺序存储结构如图:
完全二叉树定义严格,所以用顺序结构也可以表现出二叉树的结构。
对于一般的二叉树,尽量层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,把不存在的结点设置为‘’而已。例如:
注意其中结点D、H、I、F不存在其顺序存储结构为:
如果是一棵深度为k的右斜树,它只有k个结点,却需要个存储单元空间,这显然是浪费空间,所以顺序存储结构一般只用于完全二叉树。
5.2 二叉链表
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,称这样的链表为二叉链表。例如:
中间为数据域,做左边指向左孩子结点,右边指向右孩子结点。
6 遍历二叉树
二叉树的遍历是指从根结点出发,按照某种次序访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
6.1 前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树,例如:
遍历顺序为ABDGHCEIF
6.2 中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树,例如:
遍历顺序为GDHBAEICF
6.3 后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右紫薯,最后是访问根结点,例如:
遍历的顺序是GHDBIEFCA
6.4 层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问,例如:
遍历顺序是ABCDEFGHI
前序遍历根结点在第一个,后序遍历根结点在最后。已知前序遍历和中序遍历,或者已知后序遍历与中序遍历,可以唯一确定一棵二叉树。
7 线索二叉树
对于一个右n个结点的二叉链表,每个结点右指向左右孩子的两个指针域,所以一共是2n个指针域名,而n个结点的二叉树,一共有n-1条分支线数,也就是说,其实是存在2n-(n-1)= n+1个空指针域,这些空间不存任何事务,白白浪费内存。把这些空间利用起来,指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。例如:
图中指向是按照中序的方式指向前驱结点的,也可以按照需要使用前序和后序指向结点。
线索二叉树等于是把一棵二叉树转换称来一个双向链表,这样对插入删除、查找结点比较方便。对二叉树以某种次序遍历使其变成线索二叉树的过程称为线索化。如果在不知道顺序的时候,怎么知道指针是指向左孩子还是指向前驱?右怎么知道指针是指向右孩子还是后继?因此在每个结点在增设两个标志域,用来区分前驱或后继。
每个结点左边的标志域如果是0表示指向该结点的左孩子,为1时指向该结点的前驱;结点右边的标志如果是0指向该结点的右孩子,为1时指向该结点的后继。
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索,由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改指针的过程。在有了线索二叉树后,对它进行遍历其实就是等于操作一个双向链表。
和双向链表结构一样,在二叉树线索链表上添加一个头结点,并令其lchild域指向二叉树的根结点,rchild域的指针指向中序遍历时访问的最后一个结点(如下图2),或者令二叉树的中序序列中的第一个结点中,lchild域指针和最后一个结点的rchild域名指针均指向头结点(如下图3和4),这样定义的好处局势我们即可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
8 树、森林与二叉树的转换
8.1 树转换为二叉树
将树转换为二叉树的步骤如下:
- 加线,在所有的兄弟结点之间加一条连线;
- 去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线;
- 层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明,注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子
8.2 森林转换为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。步骤如下:
- 把每个树转换为二叉树
- 每一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来,当所有的二叉树连接起来后就得到来由森林转换来的二叉树
8.3 二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,步骤如下:
- 加线,若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点作为次结点的孩子,将该结点与这些右孩子结点用线连接起来
- 去线,删除原二叉树中所有结点与其右孩子结点的连线
- 层次调整,使结构层次分明
8.4 二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。步骤如下:
- 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,在查看分离后的二叉树,若有孩子存在,则连线删除...,直到所有右孩子连线都删除为止,得到分离的二叉树
- 再将每棵分离后的二叉树转换为树即可
8.5 树和森林的遍历
树的遍历分为两种:
- 一种是先根遍历,即先访问树的根结点,然后依次先根遍历根的每棵子树
- 另一种是后根遍历,即先依次后根遍历每棵子树,然后在访问根结点
森林的遍历分为两种:
前序遍历:先访问森林中第一棵树的根结点,然后在依次先根遍历根的每棵子树,再依次用同样方式遍历除去每一棵树的剩余树构成的森林
后序遍历:先访问森林中每一棵树,后根遍历的方式遍历每棵子树,然后在访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林
9 赫夫曼树及应用
9.1 赫夫曼树
如下图:
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数称为路径长度,如上图二叉树a中,根结点到结点D的路径长度就为4,二叉树中b中根结点到结点D的路径长度为2,树的路径长度就是从根到每个结点的路径长度之和。二叉树a的树路径长度就为1+1+2+2+3+3+4+4=20,二叉树b的树路径长度就为1+2+3+3+2+1+2+2=16.
如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。假设有n个权值,n个叶子结点构造二叉树,计算每个树的带权路径长度,其中带权路径长度WPL最小的二叉树称作赫夫曼树。也有树称为最优二叉树。
二叉树a的WPL = 5*1 + 15*2 + 40*3 + 30*4 + 10*4 = 315
二叉树b的WPL= 5*3 + 15*3 + 40*2 + 30*2+10*2 = 220
通过WPL可以很明显看到二叉树b比二叉树a性能高
9.2 构造赫夫曼树
由如上二叉树a如何构造赫夫曼树,步骤如下:
先把有权值的叶子结点按照从小到大的顺序排列称一个有序序列,即:A5,E10,B15,D30,C40
取头两个最小权值的结点作为一个新结点N1的两个子结点,注意相对较小的是左孩子,这里就是A为N1的左孩子,E为N1的右孩子,如下图,新结点的权值为两个叶子权值的和5+10=15
将N1替换A与E,插入到有序序列中,保持从小到大的排列,即N15,B15,D30,C40
重复步骤二,将N与B作为一个新结点N2的两个子结点,如下图,N2的权值=15+15=30
重复以上步骤最后构造出赫夫曼树如下图