5. 树与二叉树
5.1 二叉树的概念
树形结构是一种非线性结构。二叉树每个结点最有两个子树,并且,二叉树是有序树,二叉树的子树有左右之分。其子树也是二叉树。
二叉树可以是空树,度为2的有序树至少有一个结点为2,至少有3个结点。度为2的有序树的左右孩子是相对与另一个孩子来说的,若某个结点只有一个孩子,则这个孩子不分左右次序。而二叉树无论其孩子结点数是否为2,均需要确定是其左右次序。
5.2 二叉树的存储结构
5.2.1 顺序存储结构
顺序存储结构仅适用于完全二叉树。因为在最坏的情况下,一个深度为k且只有k个结点的单支树(只有右分支)却需要长度为2^k - 1的以为数组。
5.2.2链式存储结构
表示二叉树的链表中的结点至少包括三个域:数据域和左、右指针域。容易得证,在含有n个结点的二叉链表中有n+1个空链域。
5.3 二叉树的性质★
5.3.1 两中特殊的树
-
满二叉树。一个书高h,并且含有2^h - 1个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。满二叉树的结点都集中在二叉树的最下一层,并且除叶子节点之外的每个结点度数均为2。
- 可以对满二叉树含层序编号:约定从根节点(根节点编号为1)开始,自上而下,自左向右。
- 编号为i的结点,其双亲结点为i/2(向下取整)。左孩子为2i,有孩子为2i+1。
-
完全二叉树。高为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。其特点如下:
-
若i ≤ n/2(向下取整),则节点为分支结点,否则为叶子结点。
-
叶子结点只可能在层次最大的两层数显。最大层次中的叶子结点都依次排列在该层最左边的位置上。
-
若有度为1的结点,则可能只有一个,且该节点只有左孩子,没有右孩子。
-
按层序编号后,一旦出现某结点为叶子结点或只有左孩子,则编号大于i的结点均为叶子结点。
-
若n为奇数,则每个分支节点都有左孩子和右孩子;若n为偶数,则编号最大的分支结点只有左孩子,没有右孩子,其他分支节点左、有孩子都有。
-
5.3.2 二叉树的性质
-
非空二叉树的叶子节点数等于度为2的节点数加1。即n0 = n2 + 1。证明
-
非空二叉树第k层最多有2^(k-1)个结点。证明:公比为2的等比数列。
-
高度为h的二叉树最多有2^h -1 个结点。证明:等比数列求前n项和。
-
对二叉树从上到下、从左到右一次编号,有以下关系:
- 当i>1,该结点双亲编号为i/2。当i为偶数,它是编号为i/2的左孩子。当i为奇数,它是编号为(i-1)/2的右孩子。
- 结点i所在层次为log(2)i +1(向下取整)。
-
具有n个结点的完全二叉树的高度为log(2)(n+1)或log(2)n +1。证明
5.4 按各种次序遍历二叉树的递归算法
//先序遍历
void PreOrder(BiTree T){
if(T != NUll){
visit(T);//访问根节点
PreOrder(T->lChild);//递归遍历左子树
PreOrder(T->rChild);//递归遍历右子树
}
}
//中序遍历
void InOrder(BiTree T){
if(T != NUll){
InOrder(T->lChild);//递归遍历左子树
visit(T);//访问根节点
InOrder(T->rChild);//递归遍历右子树
}
}
//后序遍历
void PostOrder(BiTree T){
if(T != NUll){
PostOrder(T->lChild);//递归遍历左子树
PostOrder(T->rChild);//递归遍历右子树
visit(T);//访问根节点
}
}
5.5 按各种次序遍历二叉树的非递归算法
5.5.1 中序遍历
- 借助栈,我们来分析中序遍历的访问过程:
① 沿着根的左孩子,依次入栈,直至左孩子为空,说明已找到可以输出的结点。
②栈顶元素出栈并访问:若其孩右子为空,继续执行②;若其右孩子不空,将右子树执行①。
- 根据分析可以写出中序遍历的非递归算法如下:
void InOrder2(BiTree){
InitStack(s); BiTree p = T; //初始化栈S,p是遍历指针
while(p || IsEmpty(S)){ //栈不空或p不空时循环
if(p){ //一路向左
Push(S,p); //当前结点入栈
p = p->lchild; //左孩子不空时,一直想左走
else{ //出栈,并转向栈结点的右子树
Pop(S,p);visit(p); //栈顶元素出栈,访问出栈结点
p = p->rchild; //向右子树走,p复制为当前结点的右孩子
}
}
}
5.5.2先序遍历
先序遍历的思想和中序遍历的思想是类似的。只需把访问结点放在入栈操作的前面。
void PreOrder2(BiTree){
InitStack(s); BiTree p = T; //初始化栈S,p是遍历指针
while(p || IsEmpty(S)){ //栈不空或p不空时循环
if(p){ //一路向左
visit(p);Push(S,p); //访问出栈结点,当前结点入栈
p = p->lchild; //左孩子不空时,一直想左走
else{ //出栈,并转向栈结点的右子树
Pop(S,p); //栈顶元素出栈
p = p->rchild; //向右子树走,p复制为当前结点的右孩子
}
}
}
5.5.3 后序遍历
后续遍历的非递归实现时三种遍历算法最难的。因为在后续遍历中,要保证左孩子和右孩子都已经被访问并且左孩子在右孩子访问前访问根节点。
后续非递归遍历算法思路分析:从根节点开始,将其入栈,沿着左子树一直往下搜索,知道搜索到没有左孩子的结点,此时不能栈并访问,因为如果其有右子树,还需按相同的规则对右子树处理。直至上述操作进行不下去,若栈顶元素想要出栈被访问,要么右子树为空,要么右子树刚被访问完(此时左子树早已访问完)。
void PostOrder(BiTree T){
InitStack(S);
p = T;
r = NUll;
while(){
}
}
5.6 建立二叉树的各种算法
由二叉树的先序序列和中序序列可以唯一确定一棵二叉树。
由二叉树的后序序列和中序序列可以唯一确定一棵二叉树。
由二叉树的层序序列和中序序列可以唯一确定一棵二叉树。
5.7 建立最优二叉树和哈夫曼编码的方法
5.7.1 建立哈夫曼树(最优二叉树)的方法
① 根据给定的n个权值{w1,w2,……,wn}构成n棵二叉树的集合F = {T1,T2,......,Tn},其中每棵二叉树Ti只有一个带权为wi的根节点,其左右子树都为空;
② 在F中选取两棵权值最小的根节点作为左右孩子构造成一棵新的二叉树,新的二叉树的根节点的权值为左右子树的权值之和;
③ 在F中删除这两棵树,同时将新得到的二叉树加入F中。
④ 重复②和③,知道F中只含一棵树位置。这棵树便是哈夫曼树。
5.7.2 哈夫曼编码
若要设计长短不等的编码,则必须是人一个字符的编码都不是另一个字符的编码的前缀,这中编码称为前缀编码。
由哈夫曼树得到哈夫曼编码是很自然的过程。每个数显的字符当作一个独立的结点,其权值为它出现的频度(或次数),构造出哈夫曼树。我们可以把字符编码的解释为从根至该字符路径上边标记的徐磊,规定0为转向左孩子,规定1转向有孩子。
注意:0和1表示转向左孩子还是有孩子没有明确定义。左、有孩子的顺序也是任意的,所以构造出的哈夫曼树并不唯一,但WPL相同且为最优。
5.8 树的各种存储结构及其特点
这里,我们介绍3种常用的链表结构。
-
双亲表示法
假设以一组连续空间存储树的结点,同时在每个结点中附设一个指示器指示其双亲在链表中的位置。找双亲特别方便,找孩子要遍历整个结构。 -
孩子表示法
由于树中每个结点可能有多个孩子,可用多重链表,即每个结点有多个指针域,其中每个指针指向子树的根节点。孩子表示法便于涉及孩子的操作实现,不适用于双亲结点的操做实现。
-
孩子兄弟表示法
链表中结点的两个链域分别指向第一个孩子和下一个兄弟结点。易于实现找孩子的操作。例如:要访问x的第i个孩子,先从firstchild域找到第一个孩子,然后沿着nextsibling域找i-1步,便可找到x的第i个孩子。当然,如果为每个结点增设一个PARENT域,则同样方便实现PARENT(T,x)的操作。
5.9树与二叉树、森林与二叉树的相互转换
-
树转换成二叉树的画法:
①在兄弟之间加一条线;
②对每个结点,只保留与它第一个孩子的连线,与其他孩子的连线全部抹掉;
③以树根为轴心,顺时针旋转45°。
-
将森林转换成二叉树的规则与树类似。
①将森林中的每棵树转换成相应的二叉树;
②每棵树的根也可以视为兄弟关系,在每棵树的根之间加一条连线;
③以第一棵树的根为轴心旋转45°。
-
二叉树转换成森林的规则:
若二叉树非空,则二叉树的根及其左子树为第一颗二叉树形式,故将根的右链断开。二叉树根的右子树又可视为由除第一棵树外的森林转传承的二叉树,应用同样的方法,知道最后只剩一颗没有右子树的二叉树为止,在将每棵二叉树一次转换成树。二叉树转换成树和森林是唯一的。

浙公网安备 33010602011771号