目录
1. 二叉树的定义
二叉树(Binary Tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。如:
逻辑上的数据结构由两部分组成:数据元素以及它们之间的关系。
二叉树特点
每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
左子树和右子树是有顺序的,次序不能任意颠倒。(二叉树是有序树)
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树具有五种基本形态:1.空二叉树。2.只有一个根结点。3.根结点只有左子树。4、根结点只有右子树。5.根结点既有左子树又有右子树。
特殊二叉树
斜树:所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度(高度)相同。
有人会想,这也能叫树呀,与我们的线性表结构不是一样吗。对的,其实线性表结构就可以理解为是树的一种极其特殊的表现形式。
因为事物总是朝着复杂的方向发展,线性表相对来说是一种简单的数据结构,更为复杂的树结构有这种简单结构的元素并不奇怪。
满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。如 6-5-5:
满二叉树的特点有:(1)叶子只能出现在最下一层。出现在其他层就不可能达成平衡。(2)非叶子结点的度一定是2。否则就是“缺胳膊少腿”了。(3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<i<n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树,如图6-5-6所示。
完全二叉树的特点:(1)叶子结点只能出现在最下两层。(2)最下层的叶子一定集中在左部连续位置。(3)倒数二层,若有叶子结点,一定都在右部连续位置。(4)如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。(5)同样结点数的二叉树,完全二叉树的深度最小。
一个判断某二叉树是否是完全二叉树的办法,那就是看着树的示意图,心中默默给每个结点按照满二叉树的结构逐层顺序编号,如果编号出现空档,就说明不是完全二叉树,否则就是。
2. 二叉树的性质
性质1:在二叉树的第 i 层上至多有 2^(i-1) 个结点(i≥1)。—观察、归纳
性质2:深度为k的二叉树至多有2^k-1个结点(k≥1)。—观察、归纳
性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。—推导
性质4:具有n个结点的完全二叉树的深度为 (表示不大于x的最大整数)。—推导
所谓性质,就是某些事物特有的一些客观规律。观察、推导、归纳、总结,得出一些符合该事物的结论,记住了就便于应用。
3. 二叉树的存储结构
二叉树的顺序存储结构
顺序存储结构一般只用于完全二叉树,相应的下标会对应其同样的位置。
二叉链表
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。结点结构图如表6-7-1所示。
其中data 是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。
4. 遍历二叉树
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树遍历方法
前序遍历:规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。如图6-8-2所示,遍历的顺序为:ABDGHCEIF。
中序遍历:规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。如图6-8-3 所示,遍历的顺序为:GDHBAEICF。
后序遍历:规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。如图6-8-4所示,遍历的顺序为:GHDBIEFCA。
层序遍历:规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。如图6-8-5 所示,遍历的顺序为:ABCDEFGHI。
前序遍历算法
二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且极其简洁明了。先来看看二叉树的前序遍历算法。
中序遍历算法
后序遍历算法
推导遍历结果
。。。自己练习(多刷题,熟能生巧)
5. 二叉树的建立
如果我们要在内存中建立一个如图 6-9-1 左图这样的树,为了能让每个结点确认是否有左右孩子,我们对它进行了扩展,变成图6-9-1右图的样子,也就是将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如"#"。我们称这种处理后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。比如图6-9-1的前序遍历序列就为AB#D##C##。
6. 线索二叉树
线索二叉树原理
对于一个有 n 个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1条分支线数,也就是说,其实是存在2n一(n-1)=n+1个空指针域。
我们可以考虑利用这些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。
在每个结点再增设两个标志域Itag和rtag,注意ltag和rtag只是存放0或1数字的布尔型变量,其占用的内存空间要小于像 kchild和rchild 的指针变量。 其中:ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
线索二叉树结构的实现
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
7. 树、森林与二叉树的转换
有很多复杂的问题都是可以有简单办法去处理的,在于你肯不肯动脑筋,在于你有没有创新。
树转换为二叉树
1. 加线。在所有兄弟结点之间加一条连线。
2. 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3. 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
森林转换为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。步骤如下:
1. 把每个树转换为二叉树。
2. 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,也就是反过来做而已。如图 6-11-4 所示。步骤如下:
1. 加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点……哈,反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。 2. 去线。删除原二叉树中所有结点与其右孩子结点的连线。 3. 层次调整。使之结构层次分明。
二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。那么如果是转换成森林,步骤如下:
1.从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有右孩子连线都删除为止,得到分离的二叉树。
2.再将每棵分离后的二叉树转换为树即可。
树与森林的遍历
树的遍历分为两种方式。
1.一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。 2.另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。 比如下图的树,它的先根遍历序列为 ABEFCDG,后根遍历序列为EFBCGDA。
森林的遍历也分为两种方式:
1.前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。比如下图三棵树的森林,前序遍历序列的结果就是ABCDEFGHJI。
2.后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。比如下图三棵树的森林,后序遍历序列的结果就是BCDAFEJHIG。
发现:森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。
这也就告诉我们,当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。这其实也就证实,我们找到了对树和森林这种复杂问题的简单解决办法。
8. 赫夫曼树及其应用
赫夫曼树
那么压缩而不出错是如何做到的呢?简单说,就是把我们要压缩的文本进行重新编码,以减少不必要的空间。尽管现在最新技术在编码上已经很好很强大,但这一切都来自于曾经的技术积累,我们今天就来介绍一下最基本的压缩编码方法——赫夫曼编码。
在介绍赫夫曼编码前,我们必须得介绍赫夫曼树,而介绍赫夫曼树,我们不得不提这样一个人,美国数学家赫夫曼(David Huffman),也有的翻译为哈夫曼。他在1952年发明了赫夫曼编码,为了纪念他的成就,于是就把他在编码中用到的特殊的二叉树称之为赫夫曼树,他的编码方法称为赫夫曼编码。也就是说,我们现在介绍的知识全都来自于近 60 年前这位伟大科学家的研究成果,而我们平时所用的压缩和解压缩技术也都是基于赫夫曼的研究之上发展而来,我们应该要记住他。
赫夫曼树定义与原理
赫夫曼大叔说,从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。树的路径长度就是从树根到每一结点的路径长度之和。
如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。假设有n个权值{w1,w2,…,wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带权 wk,每个叶子的路径长度为 Ik,我们通常记作,则其中带权路径长度 WPL最小的二叉树称做赫夫曼树。也有不少书中也称为最优二叉树。
构造赫夫曼树的赫夫曼算法描述:
赫夫曼编码
赫夫曼研究这种最优树的目的是为了解决当年远距离通信(主要是电报)的数据传输的最优化问题。
编码中非0即1,长短不等的话其实是很容易混淆的,所以若要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码。
发送方和接收方必须要约定好同样的赫夫曼编码规则。