目录
一、概要
这篇主要是根据《数据结构:用面向对象方法与C++yuyanmiaoshi(第二版)》的第五章边写博客边梳理了了一下二叉树的基本知识和遍历的知识。这篇博客简单介绍了一下二叉树的类型和这些类型所具有的特性,重点在于二叉树的遍历方式以及这些遍历的一些应用。重点在前序、中序、后序的递归遍历和非递归遍历的实现,以及根据这个遍历的线性关系在原来二叉树的基础上建立起的前驱后继线索的二叉树,重点以中序线索二叉树讲了其创立和增删查。最后还讲了层次遍历,层次遍历比较简单且好理解。
二、二叉树的定义
二叉树的定义以递归形式给出:一颗二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根结点加上两颗分别称为左子树、右子树的、互不相交的二叉树组成。二叉树的子树仍是二叉树,达到空子树时递归的定义结束。
二叉树的特点是每个结点最多有两个子女(左子女和右子女),二叉树中不存在度大于2的结点。二叉树的子树由左右之分,其子树顺序不能颠倒。故二叉树有五种形态:空树、只有根结点、只有根结点和左子树、只有根结点和右子树、有根结点+左子树+右子树。
三、二叉树的性质
- 在二叉树的第层最多有个结点。 (横向)
- 深度为的二叉树最少有个结点,最多有个结点。二叉树的结点个数范围为。 (纵向)
- 任何一棵非空二叉树,如果其叶节点数为,度为2的非叶节点树为,则。 证明:设二叉树中度为1的节点数为,由于二叉树只有度为0、1、2的结点,所以二叉树的总结点数为。设二叉树的边数为。根结点没有父结点,入边为0,其余结点(有且只有一个父结点)入边为1,则。又因为每个度为2的结点的出边是2,度为1的结点出边是1,度为0的结点出边是0,则有。联立消去得到。
四、二叉树的类型
4.1 满二叉树
满二叉树的每个结点的度只有0或2的情况(孩子数只有0和2),而且度为0的结点要在同一层。除了最后一层都是度为0的结点,其他层的结点的度都为2。二叉树得挂的满满当当的。
满二叉树深度为k,则其为()个节点。
4.2 完全二叉树
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值(从根节点到倒数第三层的节点的度都为2),且最下面一层的节点都集中在该层最左边的若干位置。也就是说最后一层从左到右按顺序挂节点。若最底层为第 层(根节点层数为1),则最底层包含的节点个数取值范围是 。
- 有个结点的完全二叉树(,左开右闭)的最小深度是。(有的教科书定义为,但如果没有结点,也就是时不适用)
- 将一棵有个结点的完全二叉树自顶向下、同一层自左向右连续给结点编号,然后按照此节点编号将树中各结点顺序地存放在一个一维数组中,并称编号为的结点为结点,那么结点有以下关系:
- 若,则结点为根结点,无父节点,若,则结点的父节点为
- 若,则结点的左子女为结点
- 若,则结点的左子女为结点
- 若结点编号为奇数,且,则它处于右兄弟的位置,其左兄弟为结点
- 若结点编号为偶数,且,则它处于左兄弟的位置,其右兄弟为结点
- 结点所在的层次为
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
4.3 二叉搜索树
二叉搜索树是一个有序树,它的每个结点都有值的意义,也就是能够比较大小,常用来表示字典结构。(数据结构:用面向对象方法与C++语言描述 的第七章静态搜索结构,从二分法的判定树引申出二叉搜索树的概念。)
- 每个结点都有一个作为搜索依据的关键码(key),所有结点的关键码互不相同。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
最二叉搜索树的左叶子结点最小,最右叶子结点最大。任一结点上的关键码大于它左子树上所有结点的关键码,同时小于它右子树上所有的关键码。
插入新元素时,先检查这个元素是否在树中已经存在,如果存在则不插入,否则把新元素加到搜索停止操作的地方。从根节点开始,遇键值较大者就向左,遇键值较小者就向右,一直到尾端,即为插入点。移除旧元素时,如果它是叶结点,直接拿走就是了;如果它有一个结点,那就把那个结点补上去;如果它有两个结点,那就把它右结点的最小后代结点(左边的)补上去(右结点后代的结点比右节点及其以上的结点值小,比左结点及其以上的值要大)。
二叉搜索树的详情在搜索结构里讲,插个门钥匙。
4.4 平衡二叉搜索树(AVL)
AVL(Adelson-Velsky and Landis)树是高度平衡的二叉搜索树,用于提高二叉搜索树的效率,减少树的平均搜索长度,具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
如果一棵二叉搜索树是高度平衡的,有n个结点,其高度可以保持在,平均搜索长度也可以保持在。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是,注意unordered_map、unordered_set底层实现是哈希表。
AVL的详情在搜索结构里讲,插个门钥匙。
五、二叉树的存储表示
5.1 数组存储
使用场景:二叉树的大小和形态不发生剧烈的动态变化的场合。(不然会造成空间很多浪费)
注意事项:兼顾树形结构特点,使各结点能够方便地定位到它的父节点和左右子女。
使用tips: 都是按照完全二叉树那样放,没有挂结点但不是二叉树最后一个结点的的地方就空着。
1、完全二叉树数组存储表示
对完全二叉树的所有结点按照层次自顶向下,同一层自左向右编号,得到一个顺序(线性)序列,可以根据完全二叉树的性质推算出其父结点、子女、兄弟等结点的编号从而找到这些结点。
2、一般二叉树数组存储表示
设有一颗一般的二叉树,需要存在一个一维数组中,为了能够简单地找到某一个结点的上下左右关系,也必须仿照完全二叉树那样,对二叉树的结点编号再放进去。在编号的时候遇到空子树,应在编号时假定此子树存在进行编号,在顺序存储时也要空出它的位置(也就是把它补成完全二叉树,补进来的结点编号对应的数组索引的空间存储为空)。这样才能反映二叉树结点之间的相互关系,由其存储位置找到它的父结点、子女、兄弟结点的位置。
这样可能会消耗大量存储空间:不能压缩没有存储数据的空间。
5.2 链表存储
二叉树的每一个结点可以有两个分支分别指向结点的左、右子树。故最基本的二叉树链表结点应该包括三个域:结点数据、左子女结点地址、右子女结点地址。这样就可以从根节点开始顺着指针扒拉到每个结点的左右子树。这种结构节点组成的是二叉链表。
这种只包含三个域的结点结构只能单向从上到下扒拉结点,对于当前结点,不能返回去扒拉到他的父结点。如果想要找他的父结点,就要在结构里面加一个父结点地址,即结构里有四个域:结点数据、左子女结点地址、右子女结点地址、父结点地址。这种结构结点组成的是三叉链表。
无论是二叉链表还是三叉链表,只要知道了根结点的地址就能找到所有结点。所以一般会有一个表头指针指向二叉树的根结点当作树的访问点。
二叉链表和三叉链表可以是静态链表结构,即把链表放在一个一维数组里,数组中每个元素就是一个结点,结点有三个域或者四个域,结点地址就是其所在数组位置的索引,所以这样数组就可以压缩空间了。
结点定义如下所示。
template <class T> //模板
struct BinTreeNode{
T data; //数据域
/*
* 左右子女的链域,存放左右子女结点的地址
* 如果是用数组存的话,这里就要改成数组索引类型int,放左右子女在数组中的位置
* 对应的初始化也要改掉
*/
BinTreeNode<T> * leftChild, *rightChild;
//初始化,结点的左右子女都为空
BinTreeNode():leftChild(NULL), rightChild(NULL) {}
//已知其左右子女的地址,创建该节点,默认左右子女都为空
BinTreeNode(T x, BinTreeNode<T> * l = NULL, BinTreeNode<T> * r = NULL):data(x), leftChild(l), rightChild(r) {}
};
六、二叉树遍历及其应用
图论中最基本的两种遍历方式:深度优先和广度优先。
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
- 深度优先遍历(递归法,迭代法):前序遍历、中序遍历、后序遍历
- 广度优先遍历(迭代法):层次遍历
深度优先遍历的这里前中后,指的就是中间节点的遍历顺序,只要记住前中后序指的就是中间节点的位置就可以了。中间节点的顺序就是所谓的遍历方式
- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
对于深度优先遍历的递归算法,三种遍历过程都具有相同的遍历路线,但其遍历的结果各不相同。对于每种遍历,树中每个结点都要经过三次(对于叶节点,其左、右子树视为空子树)。但前序遍历在第一次遇到结点时立即访问(看上面的遍历顺序,前序遍历的中间节点排在第一位),中序遍历是在第二次遇到结点时才访问(看上面的遍历顺序,中序遍历的中间节点排在第二位),后序遍历要到第三次遇到结点时才访问(看上面的遍历顺序,中序遍历的中间节点排在第三位)。
对于深度优先遍历的迭代算法,其实过程都一样,只是利用循环把递归的过程模拟出来了。(递归的本质是一个栈,没到递归的出口的时候会一直压栈暂不执行,直到有了出口才会按入栈的顺序一个个退出去执行。)这时候我们把看不见的栈变成看得见的栈,也就是利用STL里的栈的数据结构模拟递归就能实现迭代的遍历。
6.1 前序遍历
1、递归遍历法
先访问树的根结点,再访问其左子树,最后访问其右子树。如果递归下去访问的结点不存在(结点指针为空),即为空子树,那就可以返回退栈执行了。
template <class T>
void BinaryTree<T>::PreOrder(BinTreeNode<T>) * subTree, void(*visit)(BinTreeNode<T>*p))
{
if(subTree != NULL){ //递归结束的条件,如果subTree == NULL,则为空子树
visit(subTree); //从根结点起,先访问中间结点,访问函数自定义
PreOrder(subTree->leftChild, visit); //访问当前结点的左子树
PreOrder(subTree->rightChild, visit); //访问当前节点的右子树
}
};
前序遍历实现过程(将就看吧)
2、迭代遍历法
利用栈模拟递归实现前序遍历的迭代算法。在每个结点都可以是子树的根结点,根结点有左右子女,所以作为每个(子)树根结点的结点就是我们是中间节点。
首先定义一个存放二叉树结点数据类型的栈,声明语句如下所示:
template <class T>
stack<BinTreeNode<T> * >S; //定义一个存放BinTreeNode<T> * 数据的栈S
根据中左右的方法,每次循环先访问子树的根结点,然后从左孩子下去,其左子树遍历完后再遍历其右子树。由于是中左右,退栈顺序是先进后出,为了出栈的结点是先左再右,所以是先右孩子入栈,再左孩子入栈,出栈就是左结点先出,再出右结点。那么每次访问完根结点后,继续访问左子树,退栈的都是左孩子,留在栈里的都是右孩子,左孩子访问完后,再按顺序访问留在栈里的右孩子。访问左子树的顺序是从上到下的,访问右子树的顺序是从下到上的。
#include <stack>
template <class T>
void BinTree<T> ::PreOrder(void (* visit) (BinTreeNode<T> * p)) {
stack<BinTreeNode<T> * > S;
BinTreeNode<T> * p = root; //从根结点开始
/*
*最后一个结点退出后还要执行一次S.pop()
*具体见下面图示
*如果不加空指针垫的话,S.pop()执行前要加S.empty()的判断
*/
S.push(NULL);
while(p!=NULL){
visit(p);
if(p->rightChild!=NULL){ //一定右孩子先进,待会要左孩子先退栈访问
S.push(p->rightChild);
}
if(p->leftChild!=NULL){ //一定左孩子后进,待会要左孩子先退栈访问
S.push(p->leftChild);
}
/*
*其左右孩子都为空了(叶结点),
*相当于递归最底层结束了,返回访问上一结点了
*/
else{
p = S.top();
S.pop();
}
/*
*如果开头没有S.push(NULL);
*else应该改成:
else if(!S.empty()){
p = S.top();
S.pop();
}
*/
}
};
入栈的结点不能为空,如果左右孩子都为空的话,就证明到了叶节点了,就不用压栈了,而是执行退栈访问上一级结点的操作(最底端的递归完成了,要执行上一级递归的操作)。
迭代法的实现过程(依旧很潦草)
3、前序遍历的应用
(1)实现二叉树的复制构造函数
若二叉树s不空,首先复制根结点,相当于二叉树前序遍历算法中的访问根结点语句,然后分别复制二叉树根结点的左右子树,相当于二叉树前序遍历算法中的遍历左子树和右子树。
template <class T>
BinaryTree<T> :: BinaryTree(const BinaryTree<T>& s){
root = Copy(s.root);
}
//递归版
template <class T>
BinTreeNode<T> * BinaryTree<T>::Copy(BinTreeNode<T> * orignode){
if(orignode == NULL) //空树
return NULL;
BinTreeNode<T> * temp = new BinTreeNode<T>; //创造新结点
temp->data = orignode->data; //把根结点复制给新结点(访问根结点)
temp->leftChild = Copy(orignode->leftChild); //遍历左子树
temp->rightChild = Copy(orignode->rightChild); //遍历右子树
return temp; //返回根指针
}
//由于要创造新节点,迭代版不好实现(也能实现,就是麻烦)
};
(2)判断两颗二叉树是否相等
//重载==
template <class T>
int operator == (const BinaryTree<T>&a, const BinaryTree<T>&b){
return(equal(a.root, b.root))?true:false;
}
template <class T>
bool equal(BinTreeNode<T>*A, BinTeeNode<T>*B){
if(A == NULL && B == NULL)
return true; //都是空树
//递归判断
/*
*递归可以可以放在判断数据前边,但这样就不符合前序遍历的顺序了
*/
if(A != NULL && B != NULL && A->data == B->data //先访问根结点
&& equal(A->leftChild, B->leftChild) //再访问左子树
&& equal(A->rightChild, B->rightChild)) //最后访问右子树
return true;
else
return false;
}
(3)建立二叉树
输入结点值的顺序必须对应二叉树结点前序遍历的顺序,并约定以输入序列中不可能出现的值作为结束递归的值。
template <class T>
void BinaryTree::CreateBinTree(ifstream& in, BinTreeNode<T> * & subTree){
T item; //数据域
if(!in.eof){ //文件读取的方式输入
in >> item;
if(item != RefValue){ //没读到结束数据
subTree = new BinTreeNode<T>(item); //新建结点并赋值
if(subTree == NULL){ //新建结点没建起来
cerr << "存储分配出错!" <<endl;
exit(1); //结束
}
/*
*新建结点后左右孩子都为空
*在左右子树的递归里会新建结点赋值给原本的左右孩子更新指针域
*/
CreateBinTree(in, subTree->leftChild);
CreateBinTree(in, subTree->rightChild);
}
else subTree = NULL; //读到结束数据的结点为空
}
};
(4)以广义表的形式前序遍历输出二叉树
首先输出其根结点,然后再依次输出它的左子树和右子树,在输出左子树之前先打印左括号,在输出右子树之后打印右括号。依次输出的左右子树要求至少有一个不为空,若都为空就不需输出。其实也就是改一下前序遍历的访问函数就可实现。
template <class T>
void PrintTree(BinTreeNode<T>) * BT)
{
if(subTree != NULL){ //递归结束的条件,如果subTree == NULL,则为空子树
cout<<BT->data; //访问根结点
if(BT->leftChild != NULL || BT->rightChild != NULL){
cout<<'('; //访问左子树
PrintTree(BT->leftChild);
cout<<',';
PrintTree(BT->leftChild); //访问右子树
cout<<')';
}
}
};
6.2 中序遍历
1、递归遍历法
先访问树根结点的左子树,再访问其根结点,最后访问其右子树。如果递归下去访问的结点不存在(为空子树),返回退栈执行了。
template<class T>
void BinaryTree::InOrder(BinTreeNode<T> * subTree, void(*visit)(BinTreeNode<T> *p)
{
if(subTree != NULL){ //结点为空,访问到底了,递归终止,退栈执行
InOrder(subTree->leftChild, visit); //先访问其左子树
visit(subTree); //访问该根节点,visit函数是自己定义的
InOrder(subTree->rightChild,visit); //再访问其右子树
}
}
遍历实现原理依旧是压栈,只是这里是先压了左子树的栈,再进行访问,再压右子树的栈,所以访问的第一个结点应该是最左边的叶结点。
2、迭代遍历法
访问的第一个结点应该是最左边的叶结点,再访问根,最后访问右子树,重复执行。什么时候访问就什么时候退栈,这时候应该先通过循环扒拉到每个子树最左边叶结点,沿途不断执行左结点入栈。扒拉完了,也就是左结点的访问模拟完了,这时候该访问了。栈不空的时候退栈访问根结点,然后再把右子树进行一个压栈,重复执行上面的过程。
#include <stack>
template <class T>
void BinaryTree<T> :: InOrder(void (*visit)(BinTreeNode<T>*p)){
stack<BinTree<T>*> S;
BinTree<T>* p = root;
do{
while(p != NULL){ //把以p为根结点的子树进行左树压栈
S.push(p);
p = p->leftChild;
} //最后栈里面都是以最开始的p为根结点的子树的最左外圈结点,栈顶是该子树最左结点
/*
*p == NULL的时候其实已经访问了该子树的最左结点的左孩子(就是NULL)
*接下来退栈的时候就是访问根结点了
*访问完根结点进入他的右子树了
*如果右子树存在,p != NULL,就会继续访问右子树的左结点
*否则继续退栈,这时候是以现在访问的结点为左孩子,退栈再赋值就是其父结点(访问根)
*/
if(!S.empty()){//栈非空,说明while循环开始的时候的p为根结点的子树有左子树
p = S.top();
S.pop();
visit(p);//访问左结点
//这层的最小左子树是否还有右子树,有的话重复while压栈访问
p = p->rightChild;
}
}while(p != NULL || !S.empty());
6.3 后序遍历
1、递归遍历法
先访问树根结点的左子树,再访问其右子树,最后访问其根结点。如果递归下去访问的结点不存在(结点指针为空),即为空子树,那就可以返回退栈执行了。
template<class T>
void BinaryTree::PostOrder(BinTreeNode<T> * subTree, void(*visit)(BinTreeNode<T> *p)
{
if(subTree != NULL){ //结点为空,访问到底了,递归终止,退栈执行
InOrder(subTree->leftChild, visit); //先访问其左子树
InOrder(subTree->rightChild,visit); //再访问其右子树
visit(subTree); //最后访问该根节点,visit函数是自己定义的
}
}
2、迭代遍历法
(1)逻辑模拟
后序遍历比前序和中序的情况复杂,因为其在遍历完左子树(扒拉到最左结点)的时候还不能访问根结点,需要再遍历完右子树后才能访问根结点。所以栈工作记录中必须注明刚才是再左子树还是右子树中。故栈结点的结构如下。
template<class T>
struct stkNode{
BinTreeNode<T> * ptr; //指向树结点的指针
enum tag {L,R}; //该结点退栈标记
stkNode(BinTreeNode<T>*N=NULL):ptr(N),tag(L){} //构造函数
};
在算法中首先使用栈暂存根结点地址,再向左子树遍历下去,此时子树根结点的tag==L。当访问完左子树中的结点并从左子树回退的时候,要进入遍历子树根的右子树,此时改变子树根结点的tag==R。从右子树出来的时候才访问栈顶的根结点的值。
也就是说,后序遍历会经过三次根结点,第一次经过是要找他的左子树,第二次经过是要找他的右子树,第三次经过才是访问。第一次经过的时候给它打上标记一,第二次经过的时候给它打上标记二。检测到标记一的时候说明已经过了第一次经过,现在是第二次,要给打上标记二;检测到标记二的时候说明已经经历过了两次,现在是第三次,可以访问了。
#include <stack>
template <class T>
void BinaryTree<T>::PostOrder(void(*visit)(BinTreeNode<T> * p)){
stack<stkNode<T>> S;
stkNode<T> w;
BinTreeNode<T>*p = root; //遍历指针
do{
while(p!=NULL){ //先遍历左子树,p是左子树的根结点,如果p为空则左子树不存在
w.ptr = p;
w.tag = L;
S.push(w); //左子树经过结点,标记L进栈
p = p->leftChild; //左子树的左子树的根结点
}
/*
*此时是遍历完左子树的结点了,要开始压右子树进栈了
*右子树的根结点从栈里找
*因为此时栈顶是最左结点,按照左右中,此时要找的是栈顶结点的右子树
*这里是第二次把右子树的根结点(右孩子)给p后,紧接着就退栈访问了。
*等访问完栈里积压的左子树后,再拿着最后的右子树根结点进行访问
*/
int continue1 = 1; //继续循环标记,用于R,但我觉得可以删掉(?)
while(continue1 && !S.empty()){
w = S.top();
S.pop();
p = w.ptr;
if(w.tag == L){
w.tag = R; //现在开始走右子树
S.push(w); //作为右子树进栈,根结点先退再进位置是一样的
//朝右子树遍历,在右子树中也是进行左右中的遍历,这里只是取右子树根结点
p = p->rightChild;
break;
}
else if(w.tag == R){
visit(p);
}
}
}while(!S.empty()); //还有结点未遍历
cout<<endl;
}
(2)前序逻辑修改实现
以上所提到的方法是基于其自己的遍历路程模拟实现的。但前中后序之间的实现有关系的,也就是说掌握了前序就可以利用他们之间的关系改出后序的实现方式。
前序:中左右-->翻转-->右左中,但后序是左右中,所以把前序的实现逻辑改成中右左,翻转后就是左右中了。所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下。
#include <stack>
template <class T>
void BinTree<T> ::PreOrder(void (* visit) (BinTreeNode<T> * p)) {
stack<BinTreeNode<T> * > S;
vector<BinTreeNode<T> *> temp;
BinTreeNode<T> * p = root; //从根结点开始
/*
*最后一个结点退出后还要执行一次S.pop()
*具体见下面图示
*如果不加空指针垫的话,S.pop()执行前要加S.empty()的判断
*/
S.push(NULL);
while(p!=NULL){
temp.push_back(p);
//要把中左右修改成中右左来反转,所以这里进入的顺序改变
if(p->leftChild!=NULL){ //左孩子先进
S.push(p->leftChild);
}
if(p->rightChild!=NULL){ //右孩子后进
S.push(p->rightChild);
}
/*
*其左右孩子都为空了(叶结点),
*相当于递归最底层结束了,返回访问上一结点了
*/
else{
p = S.top();
S.pop();
}
/*
*如果开头没有S.push(NULL);
*else应该改成:
else if(!S.empty()){
p = S.top();
S.pop();
}
*/
}
reverse(temp.begin(), temp.end()); // 将结果反转之后就是左右中的顺序了
//再进行访问
for(auto it=temp.bgein(); it !=text.end()&&!it->empty();++it)
visit(*it);
};
6.4 前中后遍历的综合应用
1、二叉树的计数
有n个结点的不同二叉树有多种,由给定的前序序列和中序序列能够唯一地确定一棵二叉树(归纳法可以证)。如果能做到结点编号的前序排序正好是1,2,3,...,n,那么这棵二叉树有多少种中序排序(多少种合理的进栈方式),就能确定多少棵不同的二叉树。
根据前序遍历的定义,前序序列的第一个字母一定是树的根,而根据中序遍历的定义,中序遍历可以根据前序序列找到的树的根将序列划分成两个子序列。再取前序序列的第二个字母,看字母所在的中序序列的子序列,将其所在的子序列以B为根节点再划分……如此反复直到划分完成。
课本例子:
根据前序和中序递归构造代码:
template <class T>
BinTreeNode<T> * createBinaryTree(T * VLR, T * LVR, int n){ //n为中序划分中数组元素的个数
if(n == 0) //子树结点个数为0,也就是空子树
return NULL; //不需要划分,空子树根结点为NULL
int k = 0; //中序序列数组中根结点的下标
while( VLR[0] != LVR[k]) //找到前序中的根结点在中序序列数组的什么位置
k++;
//创建该元素的二叉树结点(根结点)
BinTreeNode<T>*t = new BinTreeNode<T>(VLR[0]);
//用左子序列的元素递归创建左子树
t->leftChild = createBinaryTree(VLR + 1, LVR, k);
/*
*用右子序列的元素递归创建右子树
*由于前序中左右,VLR是根结点,VLR+1到VLR+k是左子树的元素
*所以右子树的根结点是在VLR + k + 1的位置
*/
t->leftChild = createBinaryTree(VLR + k + 1, LVR + k + 1, n - k - 1);
return t;
};
2、线索二叉树
二叉树虽然是非线性结构,但二叉树的遍历可以给二叉树的结点集导出一个线性序列,二叉树结点存在关于这个线性序列的前驱和后继。
如果希望很快找到某一结点的前驱或者后继,但不想每次都对二叉树遍历一遍,就要记住每个结点的前驱和后继信息,可以在原来的二叉链表中加一个前驱指针域和一个后继指针域。但这样会浪费不少存储空间。所以利用原有空指针域来存放前驱和后继指针,一般是用空的leftChild存放前驱,用rightChild存放后继结点指针,这类指示前驱后继的指针叫做线索,加上了线索的二叉树 叫线索二叉树,对应的二叉链表叫做线索二叉链表。
线索二叉树的操作其实就是在原本的树上建立了一个线性逻辑,我们在利用这个线性逻辑的特性更快地实现在原本书上的线性查找和遍历。
(1)中序线索二叉树的建立和遍历
每个结点设立标志ltag和rtag来区别线索和子女指针,0表示子女指针,1表示线索指针。(只占一个二进制位,比指针域节省空间)。
//线索二叉树的结点类
template <class T>
struct ThreadNode{
int ltag,rtag; //线索标志
ThreadNode<T> * leftChild, *rightChild; //指针域
T data;
ThreadNode(const T item):data(item),leftChild(NULL),rightChild(NULL),ltag(0),rtag(0){ }
};
//线索二叉树类
template <class T>
class ThreadTree{
protected:
ThreadNode<T>* root; //根指针
//中序遍历建立线索二叉树
void createInThread(ThreadNode<T> * current, ThreadNode<T> * & pre);
//寻找结点t的父结点
ThreadNode<T>* parent(ThreadNode<T> * t);
public:
ThreadTree():root(NULL){ } //构造函数
//建立中序线索二叉树(调用createInThread(ThreadNode<T>*current, ThreadNode<T>*&pre);)
void createInThread();
//寻找中序下第一个结点
ThreadNode<T> * First(ThreadNode<T>*current);
//寻找中序下最后一个结点
ThreadNode<T> * Last(ThreadNode<T>*current);
//寻找结点在中序下的后继结点
ThreadNode<T> * Next(ThreadNode<T>*current);
//寻找结点在中序下的前驱结点
ThreadNode<T> * Prior(ThreadNode<T>*current);
//中序线索二叉树的前中后序遍历
void Inorder(void(*visit)(ThreadNode<T>*p));
void preorder(void(*visit)(ThreadNode<T>*p));
void postorder(void(*visit)(ThreadNode<T>*p));
};
//中序遍历对二叉树进行中序线索化
template <class T>
void ThreadTree<T>::creatInThread(){
ThreadNode<T>*pre = NULL; //前驱结点指针
if(root!=NULL){ //非空树
creatInThread(root, pre);
//后处理中序最后一个结点,最后一个指针没有右孩子,也没有后驱指针
pre->rightChild = NULL;
pre->rtag = 1;
}
};
template <class T>
void ThreadTree<T>::createInThread(ThreadNode<T> * current, ThreadNode<T> * & pre){
//通过递归中序遍历来线索化
if(current == NULL) return;
//当前结点的左子树
createInThread(current->leftChild, pre); //递归,左子树线索化
//当前结点的访问处理
if(current->leftChild == NULL){ //如果左孩子为空,可以建立currrent前驱点
current->leftChild = pre;
current->ltag = 1;
}
if(pre != NULL && pre->rightChild == NULL){//前驱点存在且其右孩子为空,求其后驱点(现在)
pre->rightChild = current;
pre->rtag == 1;
}
pre = current; //前驱点往下走;
//当前结点的右子树
createInThread(current->rightChild. pre); //递归,右子树线索化
}
//寻找中序下第一个结点
template <class T>
ThreadNode<T> * ThreadTree<T> :: First(ThreadNode<T> *current){
ThreadNode<T>*p = current;
while(p->ltag == 0){
/*
*最左下结点,
*只要找最左下结点就行,从子树根结点到最左下的左孩子都是存在的,不能变成后驱线索指针
*/
p = p -> leftChild;
}
return p;
};
//寻找结点在中序下的后继结点
template <class T>
ThreadNode<T> * ThreadTree<T> :: Next(ThreadNode<T> *current){
ThreadNode<T>*p = current->rightChild;
if(current->rtag == 0){ //没记后继指针,那该结点的后继结点是右子树中的中序第一个结点
return First(p);
}
else
return p; //记录了后继结点,那右孩子就是后继结点的指针
};
//寻找中序下最后一个结点
template <class T>
ThreadNode<T> * ThreadTree<T> :: Last(ThreadNode<T> *current){
ThreadNode<T>*p = current;
while(p->rtag == 0){
/*
*最右下结点,
*只要找最右下结点就行,从子树根结点到最右下的右孩子都是存在的,不能变成后驱线索指针
*/
p = p -> rightChild;
}
return p;
};
//寻找结点在中序下的前驱结点
template <class T>
ThreadNode<T> * ThreadTree<T> :: Prior(ThreadNode<T> *current){
ThreadNode<T>*p = current->leftChild;
if(current->ltag == 0){ //没记前驱指针,那该结点的前驱结点是左子树中的中序最后一个结点
return Last(p);
}
else
return p; //记录了前驱结点,那左孩子就是前驱结点的指针
};
//在中序二叉树中求父结点
template <class T>
ThreadNode<T> * ThreadTree<T>::parent(ThreadNode<T> * t){
ThreadNode<T> * p;
if(t==root) return NULL; //根结点无父结点
/*
*找以t为根的子树的最左结点,
*应该是等价于
* p = t;
* while(p->ltag == 0){
* p = p->leftChild;
* }
*/
for(p = t; p->ltag == 0; p = p->leftChild);
/*
*以t为根的子树的最左结点有前驱(即不是原来那一整颗树的最左结点),从该前驱开始找后继/右孩子
*如果找到的点p的前驱或者后继是t或者p为NULL(无右孩子或者是大树的最右结点)就结束
*/
if(p->leftChild != NULL)
for(p = p->leftChild; p != NULL && p->leftChild != t && p->rightChild != t; p = p->rightChild);
/*
*找到的p如果是NULL(它的前驱应该是整棵树最右结点)或者找到的的p没有左孩子/p是最左结点
*从右边下去再找
*/
if(p==NULL || p->leftChild == NULL){
for(p = t; p->rtag == 0; p = p->rightChild);
for(p = p->rightChild; p!=NULL&&p->leftChild != t && p->rightChild != t; p = p->leftChild);
}
return p;
};
/*
*中序线索二叉树的前中后遍历
*/
//中序线索二叉树的中序遍历
template <class T>
void ThreadTree<T>::Inorder(void(*visit)(ThreadNode<T> * p)){
ThreadNode<T>*p;
/*
*该树为中序线索二叉树
*第一个点一定是最左结点,First函数找到最左结点,访问,然后找到其后继
*访问后,找到当前结点的后驱结点访问,Next函数实现了
*/
for(p = First(root); p!=NULL; p=Next(p))
visit(p);
};
//中序线索二叉树的前序遍历
template<class T>
void ThreadTree<T>::preorder(void(*visit)(ThreadNode<T> * p)){
ThreadNode<T>*p = root;
while(p!=NULL){
//访问中(根结点)
visit(p);
/*
*访问左
*前序是中左右,其左子女是其左子树的根(中),所以有左子女先访问左子女
*注意:p的前驱不一定是其左子女,由于中左右,要先访问左子女(左子树的中)
*先判断左子女也是因为先进左子树
*/
if(p->ltag == 0){
p = p->leftChild; //有左子女
}
/*
*访问右
*前序是中左右,其右子女是其右子树的根(中),所以有右子女先访问右子女
*注意:情况2中p的后继不一定是其右子女,由于中左右,要先访问右子女(右子树的中)
*要放在左子女判断后再判断右子女,先进左子树再进右子树
*/
else if(p->rtag == 0){
p = p->rightChild; //没有左子女,有右子女
}
/*既无左子女也无右子女——叶结点(最左结点前驱是NULL,最右结点后继是NULL,存在线索指针域)
*
*若叶结点是其父结点的左孩子,则要找其右兄弟,即其后继结点(父结点)有无右孩子
* 有右兄弟,进入以右兄弟为根的子树
* 无右兄弟(前序“中左”无“右”,中序“左中”无“右”),以“中”为左找右兄弟(后继右孩子)
*
*若叶结点是其父结点的右孩子,访问该结点即访问完以其根的子树,“中左右”的子段结束
* 在中序线索中其后继是其父结点的父结点(假设父结点的父结点为t)
* 此时t已经被先访问过了(才会接下去走)
* 若t没有右孩子,这个“中左右”的子段结束,该找t的父结点进行判断(while)
* 若t有右孩子,进入以t的右孩子为根的子树进行遍历,先访问t的右孩子(中左右)
*/
else{
//沿着后继线索继续找直到找到有右子女的结点为止(rtag == 0或找不到了p == NULL跳出)
while(p != NULL&&p->rtag == 1){
//中序最后一个结点是最右结点,其后继是NULL,找完就会自己跳出去了
p = p->rightChild;
}
if(p!=NULL)p = p->rightChild; //不是最右结点,那就进入中的右孩子
}
}
};
//中序线索二叉树的后序(左右中)遍历
template <class T>
void ThreadTree<T>::postorder(void(*visit)(ThreadNode<T> * p)){
ThreadNode<T>*t = root;
/*
*找后序的第一个结点
*最左结点的右子树下找子树的最左结点t,进入t的右子树,重复直到找到叶结点
*其实就是最深那层的从左到右的第一个结点
*/
while(t->ltag == 0 || t->rtag == 0){
if(t->ltag == 0){ //沿着左子女链找到最左结点
t = t->leftChild;
}
else if(t->rtag == 0){ //进入最左结点下的右孩子,重复沿着左子女链找到最左结点的过程
t = t->rightChild;
}
}
//访问后序序列的第一个结点,从这个结点开始以后序和中序的关系找出后序序列
visit(t);
/*
*其实可以看成不断地找后序序列的子序列的第一个结点的过程,直到子序列大小为1
*难在怎么找到开始找结点的子树根结点
*/
ThreadNode<T> * p;
/*
*若最深那层的最左结点t有父结点p,即t不是整个树的根结点,就循环
*/
while((p = parent(t)) != NULL){
/*
*若t是父结点p的右孩子,即后序“左右中”已访问到“右”(现在的t),接下来访问“中”(现在的p)
* 对应下面循环的if(p->rightChild == t) {t = p; visit(t);}
*
*若t的父结点p有后继(p->rtag == 1),则证明p没有右孩子(t是p的左孩子)
* 则后序“左右中”访问到“左”(现在的t),接下来不用访问“右”,直接访问“中”(现在的p)
* 对应下面循环的if(p->rightChild == t) {t = p; visit(t);}
* 不直接判断p->leftChild == t是因为这种情况要同时满足t是左孩子且p没有右孩子
*/
if(p->rightChild == t || p->rtag == 1){
t = p;
}
/*
*这种情况是t是p的左孩子且p有右孩子,也就是说访问了t后还要访问以t的右兄弟为根的右子树
*/
else{
t = p->rightChild; //t的右兄弟
/*
*找右子树访问的开始结点,跟上面的第一个循环是一样的
*/
while(t->ltag == 0 || t->rtag == 0){
if(t->ltag == 0)
t = t->leftChild;
else if(t->rtag == 0)
t = t->rightChild;
}
}
visit(t);
}
};
(2)中序线索二叉树的插入与删除
中序线索二叉树的插入与删除会使前驱后继关系。
插入新结点r作为结点s的右子女
①s的rightChild是线索,则r直接成为s的右子女,原来s的后继线索变成r的后继线索,放到r的rigthChild的域。
②s的rightChild是右子女,则该右子女变成r的右子女,以该右子女为根的子树成为r的右子树,该子树中的最左结点的前驱线索指向r,r的前驱线索指向s。(s -> r -> 右子树最左结点)
//插入右结点
template <class T>
void ThreadTree<T>::InsertRight(ThreadNode<T> * s, ThreadNode<T> * r){
r->rightChild = s->rightChild; //要么传线索,要么传子女,都一样的
r->rtag = s->rtag; //后继线索标志也是同步的
//建立前驱线索
r->leftChild = s;
r->ltag = 1;
//建立右子女关系
s->rightChild = r;
s->rtag = 0;
if(r->rtag == 0){ //如果原来是右子女的话,r和右子女也要建立线索关系
//找子树最左结点
ThreadNode<T>*temp;
temp = First(r->rightChild);
//关系建立
temp->leftChild = r;
temp->ltag = 1;
}
};
插入新结点r作为结点s的左子女
①s的leftChild是线索,则r直接成为s的左子女,原来s的前驱线索变成r的前驱线索,放到r的leftChild的域。
②s的leftChild是左子女,则该左子女变成r的左子女,以该左子女为根的子树成为r的左子树,该子树中的最右结点的后继线索指向r,r的后继线索指向s。( 左子树最右结点 -> r -> s)
//插入左结点
template <class T>
void ThreadTree<T>::InsertRight(ThreadNode<T> * s, ThreadNode<T> * r){
r->leftChild = s->leftChild; //要么传线索,要么传子女,都一样的
r->ltag = s->ltag; //前驱线索标志也是同步的
//建立后继线索
r->rightChild = s;
r->rtag = 1;
//建立左子女关系
s->leftChild = r;
s->ltag = 0;
if(r->ltag == 0){ //如果原来是左子女的话,r和左子女也要建立线索关系
//找子树最右结点
ThreadNode<T>*temp;
temp = Last(r->leftChild);
//关系建立
temp->rightChild = r;
temp->rtag = 1;
}
};
中序线索二叉树上的删除
考虑删去结点的右子女结点r,有四种情况。
①被删结点r是叶结点,只需要修改s的后继线索为r的后继线索即可。
//相当于子中序序列的“右”给删了,那么“右”的父结点变成这个子序列的最后一个点跟下一个子序列连接
s->rightChild = r->rightChild; //重新链接
s->rtag = 1; //标志修改
②被删结点r只有右子树,摘掉r后要把r的右子女(右子树)接回s的右孩子位置。
ThreadNode<T> *fr = First(r->rightChild); //r的前驱线索位置代替点
fr->leftChild = r->leftChild; //继承r的前驱属性
s->rightChild = r->rightChild; //r的二叉树位置代替点
③被删结点r只有左子树,摘掉r后要把r的左子女(左子树)接回s的右孩子位置。
ThreadNode<T> *la = Last(r->leftChild); //r的后继线索位置代替点
la->rightChild = r->rightChild; //继承r的前驱属性
s->rightChild = r->leftChild; //r的二叉树位置代替点
④被删结点r有左右子树,摘掉r后要把r的左子女(左子树)接回s的右孩子位置,而r的右子女(右子树)要接到左子树中序下最后一个节点下面(左子树的最右结点)。
ThreadNode<T> *la = Last(r->leftChild); //r的后继线索位置代替点
la->rightChild = r->rightChild; //右子树挂到左子树的最右结点的右子女
la->rtag = r->rtag; //继承r的后继属性
s->rightChild = r->leftChild; //r的二叉树位置代替点
ThreadNode<T>*fr = First(r->rightChild);
fr->leftChild = la; //线索链接
前序线索二叉树和后序线索二叉树建立算法的区别仅在加入前驱和后继线索的时间不同。
6.5 层次序遍历
从二叉树的根结点开始,自上而下,自左向右依次访问。实现的数据结构是队列。每层的结点自左向右地进入队列,进完后先进先出,也就是自左向右出,出的每一个结点都看看有没有子女,有的话让子女进栈,由于出栈结点是从左到右出的,它们的孩子节点也是跟在队列后面从左到右进的,当这层的结点出完(访问完后),下一层的结点也就全部从左到右在队列里排好了,下一个出的就是下一层从左到右的第一个结点。
每个结点出栈的时候要做三个操作:①访问自己,②有左孩子的话左孩子在队列尾进队列,没有不操作,③有右孩子的话右孩子在队列尾进队列,没有不操作。
#include <queue>
template <class T>
void BinaryTree<T>::LevelOrder(void(*visit)(BinTreeNode<T>*root)){
queue<BinTreeNode<T>*> Q;
BinTreeNode<T>* p = root;
Q.push(p);
while(!Q.empty()){
p = Q.front();
Q.pop();
if(p->leftChild != NULL)
Q.push(p->leftChild);
if(p->rightChild != NULL)
Q.push(p->rightChild);
}
};