二叉树
二叉树是树型数据结构中的特例,对于每一个节点(除根节点外)都仅有一个前驱和两个后继,根节点仅有两个后继。
众所周知,自然界中的树有一种数学特性叫做分形,意思大概就是“大个的我由很多个小个的我组成”。有一种自相似的特性。而二叉树这种数据结构呢也具有这种特性,具有自相似性。所以通常我们见到和二叉树有关的构建算法,遍历算法啊等等都可以通过递归的方式来解决,而且形式优雅简洁代码量还少。
自相似的结构(或者说子相似(自创勿喷)也可以)
二叉树的子结构
观察上图发现,其实树的基本问题因为本身结构组织具有自相似性,因此都可以找到一个最小子问题,当最小子问题解决了,就能以递归的形式在解决最终的问题,而树的最小子问题基本都围绕其最小子结构来讨论。
最小子结构
而最小子结构又以父节点有没有孩子呀,有多少个孩子呀,左孩子还是有孩子呀,分为下面四种形式。
只有一个孩子的情况其实是对称的,只要在设计递归的时候,包含了先往某个方向递归在往另外一个地方递归的操作就可以把这两种请况化为一种情况。
所以我们真正考虑问题时也就是设计递归规则,结束条件。只要基于最小子结构的三种情况:节点没有孩子,节点只有一个孩子,节点有两个孩子。设计出解决问题的步骤和条件,解决了最小子问题,就可以利用树的自相似结构通过递归的形式,解决最终的父问题。
多说无用,代码才是王道。接下来贴贴代码,慢慢讨论,如何基于这种思想去解决树的基本问题.
抽象数据类型代码
本人习惯于用C++,所以代码基于C渣渣实现
- 基本数据结构的实现
#include <iostream>
#include <assert.h>
#include <queue>
using namespace std;
//二叉树的实现
template< typename T>
//节点
struct Tree_Node
{
T data;
Tree_Node<T>* left_node;//左孩子
Tree_Node<T>* right_node;//右孩子
//直接初始化
Tree_Node(T insert_data=T()):data(insert_data),
left_node(NULL),
right_node(NULL){}
};
//二叉树的类的实现
template<typename T>
class Tree
{
protected:
Tree_Node<T> root;
int tree_depth;
int min_leaf_level;
int node_num;
int n0, n1, n2;//叶子节点,度为1的节点,度为2的节点
public:
Tree()= default;
Tree(const char*);
void Traverse(const char *function);
void Get_Node_Info() {
cout << "叶子节点数:" << n0 << endl
<< "度为1的系节点数:" << n1 << endl
<< "度为2的节点数" << n2<<endl;
}
//获取二叉树的深度
int Get_Depth() { return tree_depth; }
//获取最浅叶子所在层数
int Get_Min_Leaf() { return min_leaf_level; }
//获取节点数量
int Get_Node_Num() { return node_num; }
protected:
//生成树
Tree_Node<T>* create_tree(Tree_Node<T>* node);
//递归的遍历方法
void Pre_Order_Traverse(Tree_Node<T>* node);//前序遍历
void In_Order_Traverse(Tree_Node<T>* node);//中序遍历
void Post_Order_Traverse(Tree_Node<T>* node);//后序遍历
//非递归遍历
void Level_Order_Traverse(Tree_Node<T>* node);//层序遍历
//求二叉树深度
int Depth(Tree_Node<T>* node);
//求二叉树的最浅叶子节点所在层数
int Find_Min_Leaf_Level(Tree_Node<T>* node);
//求二叉树所有节点的数量
int Get_Node_Num_r(Tree_Node<T>* node);//递归获得
int Get_Node_Num_t(Tree_Node<T>* node);//层序遍历获得
//查找特定数值的节点
bool search_node(Tree_Node<T>*& get_node, T value);
};
因为考虑到后面还要写二叉树的特殊应用像什么二叉搜索树,平衡二叉树之类的东西,打算直接写个二叉树的基类,后面直接继承就可以使用了。同时把一些具体操作的方法给封装起来。
template<typename T>
//初始化节点信息
Tree<T>::Tree(const char*function):node_num(0),n0(0),n1(0),n2(0)
{
if (!strcmp(function, "recursion")) {//以递归的方法构造二叉树
create_tree(&root);//执行此函数进入二叉树的构造
}else {//非递归的方法,还没去研究
}
//获取深度
tree_depth = Depth(&root);
//获取最浅叶子节点所在的层
min_leaf_level = Find_Min_Leaf_Level(&root);
//获得节点数量,按递归展开,但是时间复杂度是o(1.618^n),节点数量大的话运算递归的开销就很大了。
//node_num = Get_Node_Num_r(&root);
//所以个人更推荐使用通过层次遍历来获得节点数量,复杂度仅仅为o(n)
node_num = Get_Node_Num_t(&root);
n2 = n0 - 1;//计算度为2的节点数量
n1 = node_num - n2 - n0;//计算度为1的节点数量
}
好了再来看看下面这张图
二叉树的递归构造过程
在构造树的时候,当我们要构造叶子节点的时候(关注没有孩子的最小子结构),因为特点是没有孩子了,比如我们先向左递归进入到了红色的节点,我们令红色节点为空并且返回,就会回到上一层即叶子结点层,然后向右递归,同时令节点为空,就会返回叶子结点层,这样一个叶子就构造好了。当明白了叶子节点如何构造后,单孩子和双孩子的情况就很明了了。通过递归的方法,如果先递归构造左子树,则最先构造好的最小子结构是位于树结构的最左下方的。
流程如下图。
下面就是create_tree函数的实现
template<typename T>
Tree_Node<T>* Tree<T>::create_tree(Tree_Node<T>* node)
{
//检查输入是否是根节点
if (node == &root) {
cout << "请输入根节点数据:";
cin >> node->data;
if ((char)(node->data) == '#') {//如果根节点为空,推出构造
node->data = T();
return node;
}
}
else {
node = new Tree_Node<T>();
assert(node != NULL);
cin >> node->data;
}
cout << endl;
//以输入‘#’作为空节点的输入,空节点最后会被删除
//当然不同数据结构就要更换一下
if ((char)(node->data) == '#') {
delete node;
node = NULL;
return node;
}
cout << "输入左节点数据:";
//递归创建左子树
node->left_node = create_tree(node->left_node);
cout << "输入右节点数据:";
//递归创建右子树
node->right_node = create_tree(node->right_node);
return node;
}
二叉树的四种遍历
当理解了递归构造过程后,二叉树的遍历自然也能很好理解了。二叉树的遍历有四种,分别是前序,中序,后序,层序。前三种一般情况况下都是用递归来解决,层序遍历一般基于队列来实现。
具体的规则如下
前序:根 ---左 --- 右
中序:左 ---根 --- 右
后序:左 ---右 --- 根
层序:按层遍历
前三种虽然看起来遍历的顺序看起来不同,但其实在进行递归的时候,路径是和构造的时候一样的,只是看最小子结构的哪一个节点先输出数据。例子如下:
只要理解了二叉树的递归形式的构造过程,结合上面三幅图,可以很直观的理解和想象遍历的过程。
下面附上前序,中序,后序的代码
template <typename T>
void Tree<T>::Pre_Order_Traverse(Tree_Node<T>* node)
{
//前序遍历顺序,根 左 右
if (node==NULL) return;//不是节点,则返回。
cout << node->data <<" ";
Pre_Order_Traverse(node->left_node);
Pre_Order_Traverse(node->right_node);
}
template <typename T>
void Tree<T>::In_Order_Traverse(Tree_Node<T>* node)
{
//中序遍历顺序,左 根 右
if (node == NULL) return;
In_Order_Traverse(node->left_node);
cout << node->data << " ";
In_Order_Traverse(node->right_node);
}
template <typename T>
void Tree<T>::Post_Order_Traverse(Tree_Node<T>* node)
{
//后序遍历顺序,左 右 根
if (node == NULL) return;
Post_Order_Traverse(node->left_node);
Post_Order_Traverse(node->right_node);
cout << node->data << " ";
}
- 快速写出残缺型二叉树的遍历输出结果
上面三幅图为了方便理解,都使用了完全二叉树作为例子。要是哪天遇到了长的比较奇怪的二叉树,比如下图。
如果要快速写出上图的树的三种遍历输出结果,就会显得比较吃力了,要花点时间,即使动手写起来也容易出错,有没有直观的可以快速写出遍历顺序的方法呢?
答案:是有的。如果我们给残缺的非叶子节点补上个节点为空的孩子(假孩子)。拿上图为例
这样是不是就清晰很多了呀。
可以很快写出答案了
前序遍历:ABD#GI#EC#FH# —–> ABDGIECFH
中序遍历:#DIG#BEA#CHF# —–> DIGBEACHF
后序遍历:#I#GDEB#H#FCA —–> IGDEBHFCA
二叉树的层序遍历另外写了一篇文。
引申问题
二叉树的引申问题非常多,就论使用递归来解决的问题就有不少了。但这些问题的解决,都可以从对二叉树的遍历和最小子结构的理解中获得思路。
先来一个最简单的问题热热身。
求二叉树的深度
再来一个相似的问题
求二叉树的最浅叶子层数