介绍
关于二叉树的一些术语,这里不再重复。本文主要从二叉树的逻辑结构出发,使用链式储存,以递归的方法去介绍三种遍历方法:先序、中序、后序遍历。如果想了解原理,虽然内容有点多,但请慢慢看完。如果想记方法,请直接看后面总结部分,完整代码附在文章最后。
我们先来了解二叉树的逻辑结构。二叉树的根本身作为一个节点,也就是一个父亲的存在。一个父亲有两个孩子。这两个孩子还可以各自带两个孩子,同时呢,每个父亲都有一个家(也就是存放数据的东西)以此类推。
在了解了二叉树树的基本逻辑结构后。首先造一个二叉树的树节点这样结构体类型。那么,这个结构体的内容呢,就应该包括一个存放数据的域(家),还有两个结构体指针(两个孩子也可以有自己的孩子,所以应该是结构体指针的类型)。
typedef struct TreeNode//树的节点
{
int data;
struct TreeNode *left;//左孩子
struct TreeNode *right;//右孩子
}TreeNode,*Tree;//一个开辟空间 一个存放地址
由于树这种逻辑结构是非线性的结构,嗯,物理空间上我们又要求储存的时候尽量的节省空间,所以直接选择链式存储。参考链表,现在开始尝试:
![](https://i-blog.csdnimg.cn/blog_migrate/c8367c8fe7ffd96e4d0b37d922b8c238.png)
我们发现,每一次储存数据,我们都要重复这个开辟空间以及获取地址这样的步骤。其实这样的方法写二叉树是行不通的。我们知道二叉树是递归定义的,所以我们可以用递归实现二叉树。那么,我们先简单来介绍一下递归。
递归的规则
我们知道,递归其实就是自己调用自己。但是,如果我们想要用好递归,我们就必须了解递归的规则:
每级函数调用都有自己的变量(每次调用自己都从上次调用的地方开始而不是像从主函数的入口那样重新开始)
每级函数先将调用之后的语句(包括返回(其中返回带的n的值也保存))存入栈中,再执行调用(调用的级别优先于返回)
每次函数调用都会返回一次
总结:
递归函数中 ,位于调用之前的语句,按照被调函数的顺序执行
递归函数中 ,位于调用之后的语句按照被调函数相反的顺序执行
这里先简单介绍一下递归,如果有需要会另外发一篇递归的文章。
如何创建节点
我们先来自定义一个返回类型为Tree的函数用来递归创建节点,且名为CreateNode吧:
Tree CreateNode()//生成树节点
{
int data;//声明一个int型变量用来寄存数据
Tree tree;//声明一个结构体指针类型用获取开辟的空间的基地址
printf("请输入数据(Abandon输入0):");
scanf("%d",&data); //暂存到data中
getchar();//吸收一些无用的字符,避免不必要的错误
if(data==0)return Abandon; //返回的条件,也就是递归的终止
else{
tree = (TreeNode*)malloc(sizeof(TreeNode));//开辟一个类型为Tree Node大小为一个TreeNode空间的大小的空间
tree->data = data;//获取数据域的地址并用来储存data的数据 结构体指针访问结构体成员用->而不是.
printf("%d的左子树: ",data); //在调用点之前按顺序输出
tree->left = CreateNode();//递归创建左子树
printf("%d的右子树: ",data); //在调用点之后倒着输出
tree->right = CreateNode();//递归创建右子树
return tree;//返回根节点 为什么能实现呢 第一次使用这个函数的时就已经把这个tree的地址内容压到栈底了整个递归结束它才会return 这个tree
}
}
㈠这里我们是先一路创建左子树,直到我们输入0,达到返回的条件返回NULL(这个节点没有开辟空间,它的根节点就是最后的节点)。㈡然后取出栈中的语句:| printf(...);tree->right=CreateNode; |然后就往最后一个节点的右子树走,重复上面㈠㈡的步骤;总之输入零就往回走,执行栈中的语句,直到执行完栈的的语句,也就是一开始就压入栈底的return tree;这就是CreateNode执行的方法。我想这个图能够辅助一下理解:
![](https://i-blog.csdnimg.cn/blog_migrate/fd8307c7f89b849251bb61a9e5bf6a67.jpeg)
先序遍历 Preorder Traverse
好,那么我们现在开始完成先序遍历;首先我们得了解什么是先序遍历,其实就是按照刚才创建二叉树的节点顺序遍历。之前递归的规则说过,在调用点之前的语句会按顺序执行。那么,我们不妨用CreateNode的语句改造一下,改成我们的先序遍历函数。且命名为PreorderTraverse吧:
void PreorderTraverse(Tree &tree)//先序遍历
{//先序遍历跟输入的顺序保持相对一致所以会按着输入的顺序输出
if(!tree)return;
else{
printf("->%d",tree->data);//在调用点之前按顺序输出
PreorderTraverse(tree->left);
PreorderTraverse(tree->right);//在调用点之后倒着输出
}
}
这里我们看到printf语句在调用点之前,所以呢它不会被压入到栈中,每次调用之前都会把printf执行了。总之,最终的结果就是它会按照你输入的数据的顺序输出。
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/fa58e42323073072da585cc2272c5d52.png)
至于原理,我想这个图可以辅助一下理解:
![](https://i-blog.csdnimg.cn/blog_migrate/41dbaa063efebe0eb698019e3131d00c.jpeg)
中序遍历 Inorder Traverse
中序遍历InorderTraverse直接上代码:
void InorderTraverse(Tree &tree)//中序遍历
{//先遍历完左子树然后从左子树尽头开始输出(有左先走到左的尽头) ,读取栈中的语句
if(!tree)return;
else{
InorderTraverse(tree->left);
printf("->%d",tree->data);
InorderTraverse(tree->right);
}
}
我们可以看到中序遍历的代码其实跟先序遍历的代码差不多,区别是:printf语句在第一个调用点之后,在第二个调用点之前。那这样会造成什么样的效果呢?来,用递归的思路了解。在第一个调用点之后就意味着printf语句会被压入到栈中。就比如我们往左遍历完最后一个节点的左子树遇到了data=0,那么它就会开始返回执行栈中的语句。先是执行| printf(...); |输出最后一个节点的数据,然后继续取栈的语句| printf(...); InorderTraverse(tree->right); |也就是把最一个节点的根节点的数据输出再往右走。然后重复这样的步骤,直到执行完栈中的语句。
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/d45a9759fe7823f9a9e88b3fbeefbb50.png)
至于原理,我想这个图可以辅助一下理解:
![](https://i-blog.csdnimg.cn/blog_migrate/edcceebc305d48d301ba36db782d5c4f.jpeg)
后序遍历 Postorder Traverse
后序遍历PostorderTraverse直接上代码:
void PostorderTraverse(Tree &tree)//后序遍历
{//后序遍历从根节点遍历完左子树后继续遍历右子树然后从栈取出语句输出
if(!tree)return;
else{
PostorderTraverse(tree->left);
PostorderTraverse(tree->right);
printf("->%d",tree->data);
}
}
这里我们看到后序遍历的代码其实跟中序遍历的代码差不多,区别是:printf语句在两个调用点之后。那么,这样子会造成怎么样的效果呢?来,用递归的思路了解一下。在两个调用点之后,就意味着它会同第二个调用点一起被压入栈中,并且从栈中取出是先执行第二个调用点的。就比如我们往左遍历完最后一个节点的左子树遇到了data=0,那么它就会返回然后执行栈中的语句。先是执行第二个调用点也就是|PostorderTraverse(tree->right);|往右走,如果遇到data=0就返回然后执行栈中语句,就是刚才在第二个调用点之后的语句 |printf(...); |输出最后一个节点的数据。然后继续取栈中的语句,也就是倒数第二个节点压入栈中的语句| PostorderTraverse(tree->right); |先往右走(这个后面还有一个printf语句)。如果这个节点的左子树data=0,那么它会先往右走。如果它的右子树data=0,取栈语句也就是printf它的数据。然后就继续取栈中的语句,也就是刚刚说的倒数第二个节点的压入栈中的语句|printf(...); |。重复这样的步骤直到执行完栈中的语句。
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/fa1829a468698c7f753bf026c744fbaa.png)
至于原理,我想这个图可以辅助一下理解:
![](https://i-blog.csdnimg.cn/blog_migrate/3ac3eee7781d5720dfa1bcf92f9b2899.jpeg)
总结
不管是先序还是中序还是后序,其实它们的代码跟创建节点的代码差不多,如果把输出语句去掉,事实上它们的执行顺序是一样的。如果不能够理解原理,那么请记住:
先序遍历每个节点: 先根 再左 再右
左左到底然后右 有左左到底然后再右 总之就像绕着二叉树跑一圈
中序遍历每个节点:先左 再根 再右
实际上就是二叉树垂直映射下来的顺序
后序遍历每个节点:先左 再右 再根
其实就像把整个二叉树比作一大串葡萄,每个节点就是一小串葡萄。每一小串葡萄都是先剪掉左边的然后是右边的 然后再剪下根。大串的也是这样。
下面是图示:
先序遍历Preorder Traverse:
![](https://i-blog.csdnimg.cn/blog_migrate/a4ad85c9df0cd1aeddc0b29c11e1ac66.jpeg)
中序遍历Inorder Traverse:
![](https://i-blog.csdnimg.cn/blog_migrate/54be1217408b5f0d751f206932b769b0.jpeg)
后序遍历Postorder Traverse:
![](https://i-blog.csdnimg.cn/blog_migrate/a026b7ca39facbc2dba3010ea456a3ac.jpeg)
完整代码
这是完整代码有需要自行取:
#include<stdio.h>
#include<stdlib.h>
#define Abandon NULL
typedef struct TreeNode//树的节点
{
int data;
struct TreeNode *left;//左孩子
struct TreeNode *right;//右孩子
}TreeNode,*Tree;//一个开辟空间 一个存放地址
Tree CreateNode()//生成树节点
{
int data;//声明一个int型变量用来寄存数据
Tree tree;//声明一个结构体指针类型用获取开辟的空间的基地址
printf("请输入数据(Abandon输入0):");
scanf("%d",&data); //暂存到data中
getchar();//吸收一些无用的字符,避免不必要的错误
if(data==0)return Abandon; //返回的条件,也就是递归的终止
else{
tree = (TreeNode*)malloc(sizeof(TreeNode));//开辟一个类型为Tree Node大小为一个TreeNode空间的大小的空间
tree->data = data;//获取数据域的地址并用来储存data的数据 结构体指针访问结构体成员用->而不是.
printf("%d的左子树: ",data); //在调用点之前按顺序输出
tree->left = CreateNode();//递归创建左子树
printf("%d的右子树: ",data); //在调用点之后倒着输出
tree->right = CreateNode();//递归创建右子树
return tree;//返回根节点 为什么能实现呢 第一次使用这个函数的时就已经把这个tree的地址内容压到栈底了整个递归结束它才会return 这个tree
}
}
void PreorderTraverse(Tree &tree)//先序遍历
{//先序遍历跟输入的顺序保持相对一致所以会按着输入的顺序输出
if(!tree)return;
else{
printf("->%d",tree->data);//在调用点之前按顺序输出
PreorderTraverse(tree->left);
PreorderTraverse(tree->right);//在调用点之后倒着输出
}
}
void InorderTraverse(Tree &tree)//中序遍历
{//先遍历完左子树然后从左子树尽头开始输出(有左先走到左的尽头) ,读取栈中的语句
if(!tree)return;
else{
InorderTraverse(tree->left);
printf("->%d",tree->data);
InorderTraverse(tree->right);
}
}
void PostorderTraverse(Tree &tree)//后序遍历
{//后序遍历从根节点遍历完左子树后继续遍历右子树然后从栈取出语句输出
if(!tree)return;
else{
PostorderTraverse(tree->left);
PostorderTraverse(tree->right);
printf("->%d",tree->data);
}
}
int main()
{ char sign;
printf("是否创建根节点?Y/N\n:");
scanf("%c",&sign);
getchar();
if(sign=='Y'||sign=='y'){
Tree tree;
tree=CreateNode();
printf("先序遍历:");
PreorderTraverse(tree);
printf("\n中序遍历:");
InorderTraverse(tree);
printf("\n后序遍历:");
PostorderTraverse(tree);
}else return 0;
}