在展开树的定义之前,先引入一个概念:线性结构和非线性结构。
线性结构:是有序的数据元素的集合,存在着一对一的关系。
那么相对的,非线性结构的每个元素可能与零个或多个元素存在关系。
树就是一种非线性结构。显然,我们对每个元素的处理要比之前所学的线性表要复杂一些。
下面,我们来由浅入深了解树和二叉树的概念以及性质。
树型结构
- 节点之间有分支
- 具有层次关系
树的定义:
- 树是n(n≥0)个结点的有限集
- n=0称为空树。
树的定义是一个递归的定义。
1.有且仅有一个特定的称为根的结点
2.其余结点可以分为m(m>0)个互不相交的有限集,其中每个集合本身又是一棵树,并称为根的子树。
树的基本术语:
先了解一些树的基本术语,后续会用到~
- 结点:包含一个数据以及若干指向子树根的分支。
- 根结点:非空树中无前趋结点的结点。
- 结点的度:拥有子树的个数。
- 树的度:树内各节点度的最大值。
- 树的深度:树中结点的最大层次。
- 路径:从某个结点到其子树中另一个结点的分支,构成两个结点之间的路径。
- 森林:是m(m≥0)棵互不相交的树的集合。
- 树一定是森林,森林不一定是树。
二叉树,本质上还是树,二叉树是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,所有的树都能转换为二叉树。那么,什么是二叉树呢?
二叉树的定义
二叉树是n个有限元素的集合,树中所有节点的度不大于2。
递归定义:二叉树是一棵空树,或是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树,左子树和右子树又同样都是二叉树。
二叉树的特殊类型
1、满二叉树:如果一棵二叉树只有度为0的节点和度为2的节点,并且度为0的节点在同一层上,则这棵二叉树为满二叉树。
2、完全二叉树:深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从1到n的节点一一对应时,称为完全二叉树。
完全二叉树的特点是叶子节点只可能出现在层序最大的两层上,并且某个节点的左分支下子孙的最大层序与右分支下子孙的最大层序相等或大1。
通俗点讲,把满二叉树最底层的叶子节点从最右边摘掉一个,这就是一棵完全二叉树了。
当学习一种新的数据结构时,我们最关心的就是怎样遍历,下面,上代码。`
#include<iostream>
#include<stack>
#include<queue>
using namespace std;
template<class Elem>
struct BinNode //二叉树的结构体定义
{
Elem data; //数据域
BinNode<Elem>* left; //左孩子指针域
BinNode<Elem>* right; //右孩子指针域
BinNode(Elem x) //带参构造
{
data = x;
left = right = NULL;
h = 0;
}
};
二叉树的遍历
二叉树的遍历方式有三种,前序遍历,中序遍历,后序遍历。这里讲到的前,中,后都是针对根节点而言。
- 前序遍历:根节点->左子树->右子树
- 中序遍历:左子树->根节点->右子树
- 后序遍历:左子树->右子树->根节点
这三种遍历方式的子树,也是按照同样的方式来遍历的。那么,很容易就能想到递归。
//前序递归遍历
template<class Elem>
void BinTree<Elem>::rpreprint(BinNode<Elem>* r)
{
if (r == NULL) return;
cout << r->data << ' ';
rpreprint(r->left);
rpreprint(r->right);
}
//中序递归遍历
template<class Elem>
void BinTree<Elem>::rinprint(BinNode<Elem>* r)
{
if (r == NULL) return;
rinprint(r->left);
cout << r->data << ' ';
rinprint(r->right);
}
//后序递归遍历
template<class Elem>
void BinTree<Elem>::rlastprint(BinNode<Elem>* r)
{
if (r == NULL) return;
rinprint(r->left);
rinprint(r->right);
cout << r->data << ' ';
}
我们可以看到,这三种递归遍历方式的本质是一样的,只不过输出节点数据的位置不同,根本原因是访问根节点的时机不同,而每一个节点,又都是其子树的根节点。这又跟树的递归定义有关系了。
既然有递归,那么一定可以转化成迭代!
话不多说,上代码。
//前序迭代遍历,此处需要用到栈,来记录访问信息
template<class Elem>
void BinTree<Elem>::ipreprint(BinNode<Elem>* r)
{
stack<BinNode<Elem>*> st; //栈中元素为节点指针类型
if (r == NULL) return; //边界条件的判断
//利用循环压栈
while (r)
{
cout << r->data << ' '; //每次入栈前输出节点数据
st.push(r);
r = r->left;
while (r == NULL && !st.empty())//左子树为空时,迭代遍历右子树
{
r = st.top();
st.pop(); //没有子节点时,出栈
r = r->right;
}
}
}
//中序迭代遍历
template<class Elem>
void BinTree<Elem>::iinprint(BinNode<Elem>* r)
{
stack<BinNode<Elem>*> st;
if (r == NULL) return;
//弹栈时访问节点
while (r)
{
st.push(r);
r = r->left;
while (r == NULL && !st.empty())
{
r = st.top();
st.pop();
cout << r->data << ' ';//此处访问
r = r->right;
}
}
}
后序遍历在第二次入栈时访问,需要两个栈,一个记录节点,另一个记录访问次数
法2.后序遍历为左–右–根,可以创建一个栈临时存放根–右–左的结果,再逆序输出得到后序遍历
此外,二叉树还有层次优先遍历和深度优先遍历。
层次优先,顾名思义,一层一层的遍历,并从左到右输出节点数据。
//层次优先遍历
template<class Elem>
void BinTree<Elem>::ilayer(BinNode<Elem>* r)
{
queue<BinNode<Elem>*> q; //创建队列
q.push(r); //输入根节点
while (!q.empty()) //队列不为空
{
r = q.front(); //指针指向队头元素
q.pop();
cout << r->data << ' ';
if (r->left != NULL) //左子节点入队
{
q.push(r->left);
}
if (r->right != NULL) //右子节点入队,注意,此处不是选择分支语句
{
q.push(r->right);
}
}
}
二叉树的深搜,意思是先访问根节点,然后一路向左访问到最左叶子节点,再回溯到上一个节点,接着访问其右子节点,以此类推,细心的你可以发现,这段话的结果跟先序遍历结果是一样的。
除此之外,二叉树还有几种特殊的结构,将在下一章进行分析。