上面一篇文章讲了有关树,二叉树的基本概念,以及树,二叉树,森林之间的相互转换,没看的uu可以戳一下这个文章的链接:数据结构:树与二叉树Part1:基本概念,树的转换(C语言实现)
下面就来介绍一下二叉树的数学性质,二叉树的创建,二叉树的遍历。
二叉树的数学性质
性质一:第层次最多只有
个结点
性质二:深度为的二叉树,最大结点的结点总数是
个,二叉树的边(edge,就是两个结点之间的连接线)最多有结点总数-1=
个。
性质三:对于一个二叉树而言,所有结点按照度来分类的话,可以分为0,1,2三种,如果已知三种类型的结点数量分别为,那么结点总数就是这三者相加,而边(两结点之间的连接线)的总数,就是
。结合性质二我们可知,由于边数
结点个数
,因此我们可以得到下式
,从而有
,即叶子结点的个数是度为二的结点个数+1。
性质四:对于一棵完全二叉树来说,由于它仅有最后一层有空缺(满二叉树没有),前面层次结点都是度为2,而最后一层又全部都是度为0的叶子结点,因此由性质三,完全二叉树和满二叉树的结点总数为,其中,满二叉树的
,k为深度。
性质五:设完全二叉树的深度为k,结点总数为n,那么存在这样的不等式:,两边同时取对数,可得
,由于k是整数,那么
。
性质六:对于一棵完全二叉树来说,设结点总数为,按照从上至下,从左至右的顺序来对结点进行编号,如下图所示:
- 对于一个 标号
的结点来说,它的父结点的编号为
,比如5的父结点就是2.5的取整结果为2。
- 对于一个拥有左右孩子结点的结点
来说,它的左孩子结点为
,右孩子结点为
。
- 对于除了最后一层外的层次里的结点,如果其编号
满足
,那么该结点没有左孩子结点(如果孩子结点是叶子结点,那么也没有右孩子结点,因为完全二叉树必须要先有左叶子结点再有右叶子结点);如果
,那么该结点没有右孩子结点(比如上图中的3号结点,
)
二叉树的创建
和前面的队列一样,二叉树由于要有左孩子结点和右孩子结点,因此在定义结点的时候,要设置指向左子树的指针(left)和指向右子树的指针(right),在这里我定义的数据类型为字符类型,是作为每个结点的编号,后面试具体情况可能会有所不同。
下面是结点的定义代码。
typedef struct treenode{
char num;
struct treenode* left;//指向左子树的指针
struct treenode* right;//指向右子树的指针
}treenode;
定义一棵完整的二叉树,要视这个树的具体情况来进行一步一步的创建,好像没有什么捷径可走。
我写了一个创建的函数,传入需要定义的父结点,左子树和右子树的指针,以及编号。
void createTree(treenode*parent,treenode*left,treenode*right,char num){
parent->num=num;
parent->left=left;
parent->right=right;
}
这是主函数里面的定义,我定义的是上面的图的树(图再放一遍)
这是代码,在主函数里面,后面的叶子结点的指向要置为空(手敲是真的麻烦)。
treenode* NodeA=(treenode* )malloc(sizeof(treenode));
treenode* NodeB=(treenode* )malloc(sizeof(treenode));
treenode* NodeC=(treenode* )malloc(sizeof(treenode));
treenode* NodeD=(treenode* )malloc(sizeof(treenode));
treenode* NodeE=(treenode* )malloc(sizeof(treenode));
treenode* NodeF=(treenode* )malloc(sizeof(treenode));
createTree(NodeA,NodeB,NodeC,A);
createTree(NodeB,NodeD,NodeE,B);
createTree(NodeC,NodeF,NULL,C);
createTree(NodeD,NULL,NULL,D);
createTree(NodeE,NULL,NULL,E);
createTree(NodeF,NULL,NULL,F);
二叉树的遍历
遍历的操作可以分为三种:访问当前节点(称之为D),遍历左子树(L),遍历右子树(R)。
因此我们可以精简地列出三种遍历方式:DLR(前序遍历),LDR(中序遍历),LRD(后序遍历)。
1.前序遍历
前序遍历就是先访问当前结点,再遍历左子树,再遍历右子树。
以上面我们的树为例。我们从A结点开始,先向左到B结点,此时B就成了当前结点;按照我们DLR的顺序,此时,D操作(访问当前结点)就代表处于B结点的位置,然后再访问B的左子树D,就是L操作。此时D结点没有子树了,那么在按照顺序,进行R操作,访问B的右子树即E结点。此时,E也没有后继结点了,那么我们再去返回到A的右子树,为C,处于C结点就是D操作,向下访问F结点就是L操作,所有结点就都遍历完了。顺序就是A-B-D-E-C-F。
函数递归方法
我们先使用函数递归的方法 ,传入根结点,在函数内部如果根节点不为空就先访问左子树,再访问右子树。这里我一开始想要更新根结点,但是好像没有找到行之有效的方法,然后看了一下教材,才明白:在函数内部,进行判断的时候,只要结点下面的左右子树不为空,就会继续递归,而继续递归时,函数的参数是根结点的左(右)子树,那么在这个函数里,根结点的左(右)子树就变成了新的根结点了,这样,更新根结点的问题就完美解决了。
下面来看这个递归函数:
void Preorder(treenode* root){
if(root!=NULL){
printf("%c\n",root->num); //输出
Preorder(root->left); //去往左子树,隐性地完成了根结点的更新
Preorder(root->right); //去往右子树,也是完成了根结点的更新
}
}
非递归方法:使用栈进行实现
第二种方法是不使用递归的方法,递归固然简便,我看的教材上还介绍了不使用递归的方法,使用栈来存储之前结点的信息,就比如之前先访问了A结点,但是后面A的右子树C等没有访问,当我们把A的左子树遍历完之后,我们还要返回到A的右子树进行遍历,因此要先行把根结点的相关信息进行存储。准确来说,是每一次遍历到一个根结点的时候,就把该结点的地址存储到栈里,利用栈先进后出的特性,再需要遍历右子树的时候,那之前存储在栈里的所有当前结点倒着再取出,取其右子树。
下面我再拿这棵树进行具体的讲解。从根结点A开始,把A存储到栈里,先向左到B;把B存储到栈中,再向左到D,没有后继了就不存储了;再进行出栈操作,后存储的B先出栈,取其右子树E;先存储的A后出栈,再取其右子树C,C再取其左子树F,最终完成遍历。
下面是代码实现:
void PreorderByStack2(treenode*root){
stack*stk;
stk=initstack();
while(1){ //设置外层循环来保证遍历每一个节点的右子树
while (root) //内层循环遍历左子树
{
stackpush(stk,root); //将每一个当前结点入栈
printf("%c\n",root->num);
root=root->left; //更新根结点为当前根结点的左子树
}
if(stk->size==0){
break;
}
root=stackpop(stk); //开始遍历右子树,先把之前存储的根结点出栈
root=root->right; /*将根结点更新为右子树,循环结束
继续循环时,根结点就是这个右子树了,因为右子树也可能有左右子树,
因此得继续循环*/
}
}
这里针对while(1)这个循环我还想提一下,就是我一开始的时候出现了这样的问题,我没有设置while(1)这个循环,出现的问题是有的右子树无法进行遍历。因为内层的while循环是遍历最根结点的左子树的,而遍历完后,将所有当前结点出栈时是遍历的右子树,而这些右子树又有可能有左子树和右子树,因此他们也是当前结点,也需要在这个外层的大循环中继续进行检查。
我再以下图为例进行说明
在上图中我们先是ABDG的顺序,这是左子树遍历;同时,栈里存储了D,B,A,D是栈顶。然后出栈,先出D的右子树H,然后根结点更新为H,返回外层大循环;输出后,root先是变成H的左子树NULL,然后再一次变成出栈的B,同理,root再次被更新为B的右子树E,返回大循环,这里就体现出我所说的大循环的作用了,因为E还有I,J等等子树,这就需要大循环再次进行检查!!
中序遍历
中序遍历就是先遍历左子树,再访问所有的当前结点,最后再遍历右子树。以之前的树为例:
先遍历1,2,4,然后1,2,4就变成了当前结点,打印4,没有右子树;打印2,再进行R操作,访问右子树5,5没有后继;再访问1,进行R操作即访问3。而3作为当前结点则继续先访问左节点6,6作为当前结点打印后,3再作为当前结点打印,完成遍历。所以完整顺序是4-2-5-1-6-3.
函数递归方法
和前序遍历相似,只是遍历左子树和打印当前结点的顺序上发生了变化。
void inorder(treenode*root){
if(root)
{
inorder(root->left);
printf("%c\n",root->num);
inorder(root->right);
}
}
非递归方法:栈实现
其实只要把第一个弄懂了,这个其实大同小异,只不过是把打印节点与访问左子树调换了一个位置而已。
void inorderbystack(treenode*root){
stack*stk=initstack();
while(1){
while(root){
stackpush(stk,root);
root=root->left;
} //遍历左子树
if(stk->size==0){
break;
}
root=stackpop(stk);
printf("%c\n",root->num); //这里是打印当前结点
root=root->right; //遍历右子树
}
}
3.后序遍历
后序遍历就是先遍历左节点,再遍历右结点,最后打印当前结点,说白就是,遇到一个节点,先检查他有没有左节点,如果有就继续,如果没有就继续检查他的右节点,如果有则继续,直到检查不出左右节点为止,打印该节点还是以之前的树为例。
先遍历左结点,1有左节点2,2有左节点4,然后我们对4进行检查发现没有左节点,好的,一看也没有右节点,那就直接打印4;然后就对4的父结点2,2已经遍历完左子树了,那就再看2的右子树,是5,而5是个叶子结点,那就打印5;此时,对于2这个父结点来说,它的左右子树都遍历完成了,那就可以打印输出2了;此时,再针对2的父结点1,1的左子树已经遍历完了,那就看1的右子树3,3一看还有一个左子树6,那就先到6。6是个叶子结点,直接打印;此时3没有右子树,那就说明LR操作已经完成了,直接打印3;最后,最大的父结点1的左右子树都遍历完了,这才去打印1。后序遍历结束!整个的顺序是:4-5-2-6-3-1。
函数递归方法
还是和前面大同小异啦~~顺序一变就好了~~
void Postorder(treenode*root){
if(root)
{
Postorder(root->left);
Postorder(root->right);
printf("%c\n",root->num);
}
}
非递归方法:栈实现
实在还是没有搞清楚(我真的会哭死~)诸位看官请再给小人一点时间,争取明天弄懂了再给大家上代码~还有层次遍历...唉...
这篇笔记已经花了我一整天的时间了~~如果大家觉得有用,看在五千字的面子上,请给个一键三连再走吧~~谢谢大家~~