二叉树的基本概念
二叉树类似于2次树,但和2次树有一些不同:
1.度为2的树至少有一个结点的度为2,而二叉树没有这种要求。简而言之二叉树可以退化成一条链。
2.度为2的树可以不区分左右子树,二叉树中左右子树(结点)的次序严格区分排列。
有关满二叉树等基本术语和树那里一样,逻辑表达法和树那里也是一样的。除此之外的有:
若二叉树中最多只有最下面两层的结点的度数可以小于2,并且最下面一层的叶子的结点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。
完全二叉树还可以定义为:一棵深度为k,n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行层序编号。如果编号为i的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。
非空完全二叉树的特点如下:
1.叶子结点只可能在最下面两层中出现(定义)。
2.对于最大层次中的叶子结点,都依次排列在该层的最左边的位置上(定义)。
3.如果有度为1的结点,只可能出现一个,且该节点只有左孩子而无右孩子(这个结点即是最大层次中最右边的结点的双亲结点)。
4.按层序编号,一旦出现编号为i的结点是叶子节点或只有左孩子,则编号大于i的结点均为叶子结点(和性质3本质上是一样的)。
5.当结点个数n为偶数时,最下层的结点个数为奇数,整个树种存在一个单分支结点,否则为偶数,不存在单分支节点。
二叉树的性质
1.二叉树的叶子结点数等于双分支结点数+1。
叶子节点个数计作x,单分支结点个数记作y,双分支结点个数记作z。由树的性质1,树中的结点数=所有结点的度数+1,有:x+y+z=y+2*z+1,化简得x=z+1。
性质2和性质3即是树的性质2,3。
4.完全二叉树中层序编号为i的结点有以下性质:
4.1 若2i<=n,则编号为i的结点为分支结点。
就是叶子结点的个数等于分支结点的个数或者分支结点的个数+1。这个结论非常显然,因为由性质1我们知道二叉树的叶子结点数等于双分支结点数+1,然后我们又由完全二叉树的特点5知道,当n为偶数时,存在一个单分支结点,此时分支节点总数=1+双分支结点数=单分支结点数+双分支结点数,i=n/2。
同理n为奇数的结论可以类似的证明。
结论4.2即为上面特点5和性质4.1的结合。
4.3 若编号为i的结点有左孩子结点,左孩子结点的编号为2i,若有右孩子结点,右孩子结点的编号为2i+1。
设i所在的深度为x,即2x-1≤i≤2x-1,同层结点i之前的结点有i-2x-1+1个,每个结点有两个孩子结点,于是结点i孩子结点之前有2x-1+2*(i-2x-1+1)=2i-1。于是左孩子的编号即为2i-1+1=2i,右孩子的编号为2i+1。
4.4 除根结点以外,若一个结点的编号为i,则它的双亲结点的编号为i/2的向下取整。
和4.3是双生定理。
5.具有n个结点的完全二叉树的高度为log2(n+1)的向上取整或log2(n)向下取整+1。
二叉树与树,森林之间的转换
树,森林与二叉树之间存在一个互相转化的关系,即任何一个森林或一棵树可以通过某种方式唯一地对应一棵二叉树,同时二叉树也能够唯一地对应到一个森林或一棵树。
树转换为二叉树
我们以下面的这棵树为例:
1.将书中相邻的兄弟结点之间加上一条连线,注意是相邻的兄弟不是同层的结点,例如下面的结点E和结点F需要连线,结点F和结点G就不需要。
2.对树中的每个结点只保留它与长子之间的连线,删除与其他孩子结点的连线。
这个时候整棵树已经转化成了以原树根结点为根结点的二叉树了。对于一个结点,如果它存在兄弟结点,兄弟节点即是它的右孩子结点,如果它存在孩子节点,剩余的长子结点即是它的左子节点。调整后的结构如下:
森林转化为二叉树
1.首先将森林中的每一棵树转化成二叉树。
(转化后的二叉树)
2.从第二棵二叉树起,将后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子结点,即后一棵二叉树作为前一棵二叉树的右子树。
由树到二叉树的转化可知,对于最上层的根结点,只有最左边的子节点转化为左子节点,根结点不存在兄弟结点,于是根结点右孩子节点的位置一定为空。于是一定可以将后一颗二叉树插入到前一棵二叉树根结点的右孩子结点的位置。
转化后的结果如下图所示:
二叉树还原成树
从树转化成二叉树的步骤反推二叉树到树的还原就能够想到第一步的过程是将树的根结点与它长子之外的孩子结点重新建立联系。根据树转化成二叉树的过程我们可以知道,长子结点作为二叉树根的左子节点,与长子结点相邻的孩子结点,作为长子结点的右子结点,右子结点的右子节点·······那么第一步就是:
1.若某结点是起双亲的左孩子,则将该结点的右孩子,右孩子的右孩子等都与该结点的双亲结点相连。
接下来就是断掉原本树中相邻结点的联系,也就是转化后的二叉树中根节点到右子节点的连线······
2.删除原二叉树中所有的双亲结点到右子节点的连线并进行调整。
二叉树还原成森林
森林转化成二叉树的过程,是将每棵树转化后的二叉树插入到前一棵二叉树右子节点的位置。也就是说对于二叉树还原成森林,二叉树的根结点和所有右孩子(即右孩子,右孩子的右孩子······)的个数为m,转化后的森林中的树的个数即为m。
1.我们对于根结点和所有右孩子的个数为m的二叉树,每次分割出当前二叉树的右子树,分割m次。
2.将这些二叉树转化为树。
二叉树的存储结构(链式)
顺序存储结构也是能够存储二叉树,例如完全二叉树和满二叉树,顺序存储结构就是非常好的存储方法,在顺序存储结构处理二叉树代码非常简洁,效率也不低。
但是对于绝大多数的二叉树,采用顺序存储结构,需要很多的空结点才能够将一棵二叉树构造成一棵完全二叉树。这样最坏的情况是二叉树退化成了一条链,需要补全左子树所有的结点,空间浪费非常严重。
所以对于普通的二叉树一般考虑用链式存储结构。
data存储值,lchild指向左孩子,rchild指向右孩子
//lchild指向左子节点,rchild指向右子节点
struct BTNode{
ElemType data; BTNode *lchild; BTNode *rchild;};
可以再添加一个parent指针指向双亲结点。
创建二叉树
对括号表达法表示的二叉树字符串str创建成二叉树,和孩子链存储结构的建树基本是一样的。用一个栈来存放含有子节点的根结点,遍历字符串的所有字符:
//根据括号表达法str创建二叉树
void CreateBTree(BTNode * &b,char *str){
BTNode *St[MaxSize],*p=NULL;//St为存储含有子节点的双亲结点的栈,top为栈顶,新的结点即为栈顶结点的子节点
int top=-1,k,j=0; char ch;//k用于标识当前遍历到的子节点为左子节点还是右子节点,左为1,右为2
b=NULL; ch=str[j];
while (ch!='\0'){
switch(ch){
//如果为左半括号,说明当前的结点存在子节点,将该结点进栈,第一个子节点肯定是左子节点,k=1
case '(':top++; St[top]=p; k=1; break;
case ')':top--; break;//栈顶结点的子树遍历完全,出栈
case ',':k=2; break;//遇到逗号,说明有新的子节点,新的子节点即为右子节点,k=2
default:p=(BTNode *)malloc(sizeof(BTNode));//遇到字符,说明有新的结点,给结点分配空间和赋值
p->data=ch; p->lchild=p->rchild=NULL;
if (b==NULL) b=p;//遇到的第一个字符即为根结点
else{
//当前结点是栈顶结点的子节点,建立根结点指向孩子结点的指针
switch(k){
//根据k来判断到底是左子节点还是右子节点
case 1: St[top]->lchild=p; break;
case 2: St[top]->rchild=p; break;
}
}
}j++; ch=str[j];
}
}
输出二叉树
和上一章节的基本是一样的:
//输出二叉树
void DispBTree(BTNode *b){
if (b!=NULL){
printf("%c",b->data);//打印根结点
if (b->lchild!=NULL||b->rchild!=NULL){
//有孩子结点打印括号,递归打印左右子树
printf("("); DispBTree(b->lchild);//打印左半括号,递归打印左子树
if (b->rchild!=NULL) printf(",");//有右孩子节点时才输出,
DispBTree(b->rchild); printf(")");//递归打印右子树,打印右半括号
}
}
}
销毁二叉树
先向下销毁左右子树,再销毁根结点:
//销毁二叉树
void DestroyBTree(BTNode *&b){
if (b!=NULL){
//先销毁左右子树,最后销毁根结点
DestroyBTree(b->lchild); DestroyBTree(b-&g