树的遍历规则
树的遍历有三种方式,前序、中序还有后续遍历。
| 遍历方式 | 规则 |
|---|---|
| 前序/先序遍历 | 先遍历根结点,再遍历左子树,然后再遍历右子树 |
| 中序遍历 | 先遍历左子树,再遍历根结点,最后遍历右子树 |
| 后序遍历 | 先遍历左子树,再遍历右子树,最后遍历根结点 |
简单来说前序遍历、中序遍历、后续遍历的规则就是根左右、左根右、左右根。
之前我们用顺序表实现了堆结构,这是一种二叉树的结构,这里我们用链表结构(二叉链)来实现二叉树。
创建一棵二叉树
链表结构中二叉树是由一个个结点组成的,二叉树结点的结构如下:
c
typedef char BTDataType;//类型重命名
//二叉树结点的结构
typedef struct BinaryTreeNode
{
BTDataType data;//数据域
struct BinaryTreeNode* left;//指向左孩子的指针
struct BinaryTreeNode* right;//指向右孩子的指针
}BTNode;
然后我们可以封装一个函数BuyNode用来创建结点:

然后我们封装一个函数CreateBinaryTree来调用该函数创建多个结点,然后手动把这些结点链接起来形成一个二叉树。

创建出来的二叉树就是这样的:

前序遍历
前序遍历先遍历根结点,所以先打印A,然后遍历A的左子树,左子树以B为根结点,打印B,然后遍历B的左子树。B的左子树以D为根结点,打印D,然后遍历D的左子树。D的左子树以G为根结点,打印G,然后遍历G的左子树。G的左子树以NULL为根结点,说明遍历到头了,打印NULL(不打印也可以)。往上返,G的左子树遍历完后,遍历G的右子树。G的右子树以NULL为根结点,说明遍历到头了,打印NULL,往上返。D的左子树遍历完,遍历D的右子树,D的右子树以NULL为根结点,说明遍历到头了,打印NULL,往上返。B的左子树遍历完了,遍历B的右子树。B的右子树以NULL为根结点,打印NULL,往上返。A的左子树遍历完了,遍历A的右子树。A的右子树以C为根结点,打印C,再遍历C的左子树。C的左子树以E为根结点,打印E,再遍历E的左子树 。E的左子树以NULL为根结点,说明遍历到头了,打印NULL,往上返。E的左子树遍历完了,该遍历E的右子树。E的右子树以NULL为根结点,说明遍历到头了,打印NULL,往上返。C的左子树遍历完了,该遍历C的右子树,C的右子树以F为根结点,打印F,遍历F的左子树。F的左子树以NULL为根结点,说明遍历到头了,打印NULL,往上返。F的左子树遍历完了,该遍历F的右子树了。F的右子树以NULL为根结点,说明遍历到头了,打印NULL,往上返。C的左右子树遍历完了,再往上返。A的左右子树遍历完了,说明遍历结束。
这就是根左右的遍历顺序。

中序遍历和后续遍历无非就是改变遍历根结点的顺序,和上述分析过程一样,这里我就给出来了,可以自己遍历一遍试试。

我们之前学过,树的结构是递归的,那么树的遍历也应该是递归遍历的,接下来我们将体会递归的暴力美学。
前序遍历无非就是根左右的顺序,根root进来后要先判断root是否为空,为空则打印NULL,直接返回。我们要先打印根结点,然后再递归遍历根结点的左子树和右子树。

这样先序遍历的代码就完成了,是不是很简单?
我们打印一下:

与我们分析出来的结果一模一样,说明代码逻辑没问题,如果你对如何递归的存有好奇,没有关系,接下来我们将分析它是如何递归的?
递归分析

说到递归就要牵扯到函数栈帧了,我们知道每次调用函数都会创建函数栈帧,调用结束后会销毁函数栈帧,这里我们用实线代表函数栈帧创建的过程,虚线代表函数栈帧销毁的过程。
我们调用该函数,函数栈帧创建,传进去的根结点为A,A不为空,打印A,然后递归调用函数传A的左孩子B,函数栈帧创建:

B不为空,打印B,然后再调用该函数传B的左孩子,函数栈帧创建:

D不为空,打印D,然后调用函数传D的左孩子,函数栈帧创建:

G不为空,打印G,调用函数传G的左孩子过去,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返:

回到G的函数栈帧中,G的左子树遍历完成,调用函数传G的右孩子,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返,G的左右子树遍历完成,函数栈帧销毁,再往上返:

回到D的函数栈帧中,D的左子树遍历完成,调用函数传D的右孩子,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返,回到D的函数栈帧中,D的左右子树遍历完成,函数栈帧销毁,再往上返:

回到B的函数栈帧中,B的左子树遍历完成,调用函数传B的右孩子,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返回到B的函数栈帧中,B的左右子树遍历完成,函数栈帧销毁,再往上返:

回到A的函数栈帧中,A的左子树遍历完,调用函数传A的右孩子,函数栈帧创建:

C不为空,打印C,调用函数传C的左孩子,函数栈帧创建:

E不为空,打印E,调用函数传E的左孩子,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返,回到E的函数栈帧中,E的左子树遍历完,调用函数传E的右孩子,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返,回到E的函数栈帧中,E的左右子树遍历完成,函数栈帧销毁,再往上返:

回到C的函数栈帧中,C的左子树遍历完,调用函数传C的右孩子,函数栈帧创建:

F不为空,打印F,调用函数传F的左孩子,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返,回到F的函数栈帧中,F的左子树遍历完,调用函数传F的右孩子,函数栈帧创建:

NULL为空,打印然后直接返回,函数栈帧销毁,往上返,回到F的函数栈帧中,F的左右子树遍历完,函数栈帧销毁,再往上返:

回到C的函数栈帧中,C的右子树遍历完了,函数栈帧销毁,再往上返:

A的左右子树遍历完了,函数栈帧销毁,先序遍历函数调用结束。
整个递归过程化画出来就像一棵二叉树:

这就是整个函数递归的过程了。
中序遍历
中序遍历就是把根结点的打印放在中间就好了:


与预期结果一致,代码逻辑没什么问题。
后序遍历
后序遍历就是把根结点的打印放在最后就好了:


与预期结果一致,代码逻辑没什么问题。