一、三种遍历方式
1.前序遍历
①遍历方式
前序遍历也叫先根遍历,遍历的先后顺序为根 → 左子树 → 右子树。
②图解说明
我们从递归的思想来理解:
- 以A为根节点,左右为两颗子树。先遍历根节点,再遍历以B为根节点的左子树。待左子树全部遍历完后,再遍历以C为根节点的右子树
- 以B为根节点,左右为两颗子树。先遍历根节点,在遍历以D为根节点的左子树。待左子树全部遍历完后,再遍历以E为根节点的右子树
- ……
- 对H结点而言,左子树为空,所以遍历H的右子树。右子树也为空,所以此时D的左子树遍历完全,遍历D的右子树
- 重复上述过程直至遍历完全部的结点
③源码剖析
在递归过程中,由于root->left的遍历在root->right之前,所以会一直进入对左子树的递归,直到左子树为空。右子树只有在递归回溯的时候才会被遍历。
④算法图解
以下面这棵树为例说明,注意感受回溯的过程,这才是递归的核心:
打印结果:
⑤算法本质
前序遍历本质上是深度优先搜索,一直碰到NULL时才会回头
2.中序遍历
①遍历方式
中序遍历也叫中根遍历,遍历的先后顺序为左子树 → 根 → 右子树。
②图解说明
同样的我们从递归的思想来理解:
- 以A为根节点,左右为两颗子树。先遍历左子树,待左子树全部遍历完后,回到根节点A,再遍历以C为根节点的右子树
- 以B为根节点,左右为两颗子树。先遍历左子树,待左子树全部遍历完后,回到根节点B,再遍历以E为根节点的右子树
- ……
- 对H结点而言,左子树为空,回到根节点H,再遍历右子树,右子树也为空,所以开始回溯,回到根节点D遍历D
- 重复上述过程直至遍历完全部的结点
③源码剖析
3.后序遍历
①遍历方式
后序遍历也叫后根遍历,遍历的先后顺序为左子树 → 右子树 → 根。
②图解说明
同样的我们从递归的思想来理解:
- 以A为根节点,左右为两颗子树。先遍历左子树,待左子树全部遍历完后,回到根节点A来遍历右子树,待右子树全部遍历完后回到根节点,遍历根节点
- 以B为根节点,左右为两颗子树。先遍历左子树,待左子树全部遍历完后,回到根节点B来遍历以E为根节点的右子树,待右子树全部遍历完后回到根节点,遍历根节点
- ……
- 对H结点而言,左子树为空,回到根节点H来遍历右子树,右子树也为空,所以开始回溯
- 重复上述过程直至遍历完全部的结点
③源码剖析
④应用举例:
对一颗二叉树的销毁就要通过后序遍历来实现:
【题外话】
还有一种遍历方式是层序遍历,顾名思义,它的遍历模式是一层一层遍历的,它所使用的基本思想是广度优先搜索,读入数据的时候将每一行数据入列。在读出数据的时候,首先将该结点的两个左右子树入列,再将该结点弹出。由此实现层序遍历的效果。
二、有关二叉树的基本问题
1.求结点个数
【形式①:使用全局变量】
int cnt = 0;
void BinaryTreeSize(BTNode* root)
{
if (root == NULL)
return;
cnt++;
BinaryTreeSize(root->left);
BinaryTreeSize(root->right);
}
【评析】
这里使用全局变量来传递结点个数。采取的遍历方式前序遍历,即先根节点再左子树再右子树。注意重复使用的时候首先要将全局变量先置为0.
【思想】
可不可以使用静态局部变量来传递呢?不适用!原因如下:
- 静态变量的作用域仅在递归函数中,所以设计的函数需要把它return回来
- 静态变量的在递归函数中的初始化仅进行一次,不能再次修改为0
- 静态局部变量具有线程安全问题
【形式②:采用传址调用】
void BinaryTreeSize(BTNode* root , int* cnt)
{
if (root == NULL)
return;
++(*cnt);
BinaryTreeSize(root->left, cnt );
BinaryTreeSize(root->right, cnt);
}
【形式③:分治的思想】
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) +
BinaryTreeSize(root->right) + 1;
}
①分治思想介绍
把复杂的问题分解成规模更小的子问题,再将子问题继续分解,直到子问题不能再被分解为止。
②代码解读
求一颗树的结点数的问题可以分解为求一颗树中左子树的结点个数 + 右子树的结点个数 + 加上根节点个数(也就是1)。而求左子树的结点个数可以同理分解。重复分解复杂问题,直到问题不可以被分解,也就是root = NULL的情况时也就可以确定结点个数为0。
大家在理解问题的时候从思想的高度进行把握就很简单了。
2.求叶子结点个数
【形式①:遍历+计数】
void BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return;
if (!root->left && !root->right)
{
cnt++;
return;
}
BinaryTreeLeafSize(root->left);
BinaryTreeLeafSize(root->right);
}
【形式②:分治思想】
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (!root->left && !root->right)
{
return 1;
}
return BinaryTreeLeafSize(root->left)
+ BinaryTreeLeafSize(root->right);
}
【代码解读】
求整棵树的叶子结点困难,那就分别求左子树和右子树的叶子结点个数之和,再将左子树中的叶子结点个数分解为左子树的叶子结点个数和右子树的叶子结点个数,由此问题得解。
3.求第k层的结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
}
【代码解读】
同样是分治思想的使用
4.求二叉树的深度
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL)
return 0;
int leftdepth = BinaryTreeDepth(root->left);
int rightdepth = BinaryTreeDepth(root->right);
return leftdepth < rightdepth ? rightdepth + 1 : leftdepth + 1;
}
【代码解读】
这里涉及一个比较的过程,那么如何在比较中求出最大深度呢?我们还是采用分治的思想。求一棵树的最大深度 = 左右子树中深度的最大的 + 1(根节点还占据一格的深度),而求左右子树的深度时通用的 = 左右子树中深度的最大的 + 1。我们由此不断将问题分解为,直到root等于0我们知道需要返回。