一、树的定义
树(Tree)是n(n>=0)个结点的有限集。 n=0又称为空树。在任意一课非空的树中:(1)有且仅有一个特定的称为跟(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
树是一种一对多的数据结构。
需要注意的是:
(1)当n>0时根结点是惟一的,不可能存在多个根结点。
(2)m>0时,子树的个数没有限制,但它们一定是互不相交的。如果相交,就不符合树的定义。
结点分类
结点拥有的子树称为结点的度。度为0的结点称为叶结点或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。如图所示,树的最大结点是D的度,为3,则树的度也为3.
树中结点最大的层次称为树的深度或高度。
如果将树中结点的各子树看成从左到右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
森林是m(m>=0)棵互不相交的树的集合。
二、树的存储结构
简单的顺序存储结构和链式存储结构表示不了树一对多的关系。试想,数据元素挨个的存储,谁是谁的双亲,谁是誰的孩子呢?
树有自己的三种不同表示法:双亲表示法、孩子表示法、孩子兄弟表示法。
1. 双亲表示法
在每个结点中,附设一个指示器指示双亲结点在数组中的位置。结点结构表如下。
data | parent |
---|
其中,data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组的下标。
标出双亲,左右结点则构成树。
2. 孩子表示法
先介绍一种多种链表表示法:每个结点有多个指针域,其中每个指针指向一棵子树的根结点。
方案一:
指针域的个数就等于树的度。
data | child1 | child2 | child3 | …… | childd |
---|
其中data是数据域,child1到childd是指针域,用来指向该结点的孩子结点。对于图6-4-1的树来说,树的度是3,所以指针域的个数是3。如下图。
这种方法对于树中各结点的度相差很大是,显然是浪费空间的,因为有很多的结点,它的指针域都是空的。不过如果树的各结点度相差很小时,那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成了优点。
为了按需分配空间,我们考虑方案二。
方案二:
每个结点指针域的个数等于该结点的度。
data | degree | child2 | child2 | …… | childd |
---|
其中data是数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各孩子结点。如下图。
这种方案也有缺点,就是会给结点的维护带来麻烦。所以,我们提出孩子表示法。
孩子表示法: 把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则次单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。如下图。
为此,设计两种结点结构,一个是孩子链表的孩子结点,如下图。
child | next |
---|
其中child是数据域,用来存储某个结点在表头数组的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点,如下表。
data | firstchild |
---|
其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点的孩子链表头指针。
但是,这种方法也找不到某个结点的双亲。于是有了双亲孩子表示法。如下图。
3. 孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和词结点的右兄弟。
data | firstchild | rightsib |
---|
其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。如下图。
三、二叉树的定义
二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
二叉树的特点:
- 每个结点最多有两棵子树,所以二叉树中不存在大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
- 左子树和右子树是有顺序的,次序不能颠倒。就像人的左右手。
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树有5种基本形态:
- 空二叉树。
- 只有一个根结点。
- 根结点只有左子树。
- 根结点只有右子树。
- 根结点既有左子树又有右子树。
斜树:只有左子树或者只有右子树。是一种特殊的线性表。
满二叉树:所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上。
满二叉树的特点有:- 叶子只能在最下一层。出现在其他层就不可能达到平衡。
- 非叶子结点的度一定是2。否则就是“缺胳膊少腿了”。
- 在同样深度的二叉树中,满二叉树的结点个数越多,叶子树越多。
完全二叉树:
- 叶子结点只能在最下两层。
- 最下层的叶子一定集中在左部连续的位置。
- 倒数第二层,若有叶子结点,一定都在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即不存在右子树的情况。
- 同样结点数从二叉树,完全二叉树的深度最小。
判断一棵树是否是完全二叉树,心中默默给每个结点按照满二叉树的结构逐层顺序编号,如果编号出现空档,就不是完全二叉树,否则就是。
满二叉树是特殊的完全二叉树。
完全二叉树必须先满足左后满足右,缺的元素只能是满二叉树最下一层的,高度差小于或等于1。
四、二叉树的存储结构
(一)顺序存储结构
一般只有完全二叉树才考虑顺序存储结构,因为完全二叉树的严格性,可以充分利用顺序存储空间。其他二叉树都会造成空间的浪费,特别是右斜树。
(二)二叉链表
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,称为二叉链表。
lchild | data | rchild |
---|
其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。
五、遍历二叉树
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序一次访问树中所有结点,使得每个结点被访问一次且仅被访问一次。
前序遍历
根–左子树–右子树
中序遍历
左子树–根–右子树
后序遍历
左子树–右子树–根
层序遍历
根–第一层从左到右–第一层从左到右……
已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
已知前序遍历序列和后序遍历序列,不可以唯一确定一棵二叉树。
六、线索二叉树
为了充分利用二叉链表的空指针,把空指针指向前驱和后继,这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。
对二叉树一某种次序比那里时期变为线索二叉树的过程称作是线索化。线索化的过程就是在遍历的过程中修改空指针的过程。
为了判别某一结点的lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,引入ltag和rtag两个标志域。
lchild | ltag | data | rtag | rchild |
---|
其中,ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;rtag为0时指向该结点的右孩子,为1时指向该结点的后继;因为对6-10-1的图的二叉链表图可以修改如下图。
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是不错的选择。
七、二叉树的性质
- 在一棵高度为k的二叉树中,结点总数为至多为2k(k次方)-1
八、树、森林与二叉树的转换
(一)树转化为二叉树
1. 加线。在所有兄弟结点之间加一条连线。
2. 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3. 层次调整。以树的根结点为轴心,将整棵树瞬时间旋转一定的角度。使之结构层次分明。之一第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
(二)森林转换为二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作,如下:
1. 把每个树转换为二叉树;
2. 第一棵树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用连线连接起来。当多有的二叉树连接起来后就得到森林转化的二叉树。
(三)二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程。
1. 加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子的结点……哈,反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
2. 去线。删除原二叉树中所有结点与其右孩子结点的连线。
3. 层次调整。使之结构层次分明。
(四)二叉树转换为森林
判断一棵树是否能转换为一棵树还是森林,就看它是否有右结点,若有,就可以转为森林,否则,就是一棵树。
1. 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有右孩子连线都删除为止,得到分离的二叉树。
2. 再将每棵分离后的二叉树转换为树即可。
(五)树与森林的遍历
树的遍历分为两种方式:
1. 一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。
2. 另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。
森里的遍历方式也分为两种方式:
1. 前序遍历:先访问森林中第一棵树的根结点,然后再依次先跟遍历根的每棵子树,再依次用同样方式遍历除去每一棵树的剩余树构成的森林。
2. 后序遍历:先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。
树和森林的遍历可参看(三)(四)图中的笔记。
神奇的是,森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。这也就告诉我们,当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。
九、赫夫曼树
赫夫曼树:带全路径长度WPL最小的二叉树。
先取最小权值的结点作为叶子结点,逐级递增就能构造出哈夫曼树。把左结点标为0,右结点标为1,就能构造出赫夫曼编码。
十、题目
- 某棵完全二叉树上有699个节点,则该二叉树的叶子节点数为(350)。
解析:n0=n2+1;
n=n0+n1+n2=n0+n1+n0-1=699
由于完全二叉树中度为1的节点只有0个或1个两种情况,所以,将0或1带入上面公式,整理后得: n0=(n+1)/2或者n0=n/2; 看看n是否能被2整除,能则用n0=n/2。否则用n0=(n+1)/2 既叶子节点为n0=(n+1)/2=350。 - 一棵有124个叶节点的完全二叉树,最多有(248 )个节点。
解析:n0 = n2 + 1,于是度为2的结点个数123个
完全二叉树中度为1结点个数最多1个
因此该完全二叉树中结点最多有123 + 1 + 124 = 248个