二叉树是一种常用的数据结构,在程序中也经常需要使用二叉树,但是你所使用语言却并不一定提供了二叉树这种数据类型,所以为了方便使用,我们可以自己实现一个二叉树的数据类型。在需要时就像使用其他已定义的类型一样方便。
下面给出一些本人写的算法和解释(基于C语言),希望对读者写一个二叉树数据类型有所帮助。
0、递归的四条基本法则
由于二叉树中的算法大多使用递归来实现,而且使用递归实现也使代码非常简洁和易于理解。但是写一个好的递归算法并不是一件容易的事,所以我觉得在开始这些算法的讲解之前有必要向大家说说递归实现的一些法则。而且本文中的代码都是以下面的法则作为依据的(至少我是这样认为)。
1)基准情形。必须总有某些基准情形,它无需递归就能解出。
2)不断推进。对于那些需要递归的情形,每一次递归调用都必须要使求解状况朝接近基准情形的方向推进。
3)设计法则。假设所有的递归调用都能进行。
4)合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。
1、数据的储存结构和定义
#define TRUE 1
#define FALSE 0
//定义自己的数据类型
typedef char DataType;
typedef int BOOL;
typedef struct BiNode
{
DataType cData; //用于储存真正的数据
struct BiNode *LChild;//指向左孩子
struct BiNode *RChild;//指向右孩子
}BiNode, *BiTree;
2、基本操作的实现
1)遍历
遍历二叉树是其他操作的基础,二叉树的很多操作都是建立在遍历的基础上的,掌握了遍历对其他算法的理解和实现都大有帮助,那么我们就先来看一看遍历的算法,在二叉树中,根据访问根的次序分为3种,即先序遍历(先访问根,再先序访问左子树,最后先序访问右子树),中序遍历(先中序访问左子树,访问根,最后中序访问右子树)和后序遍历(先后序访问左子树,再后序访问右子树,最后访问根),还有一种就是层次性遍历(借助队列进行),它们的实现如下:
BOOL PreOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*))
{
//先序遍历二叉树,对每个结点调用Visit一次,且仅一次
//实现对结点的某种操作,Visit失败,则遍历失败
if(BT != NULL)
{
if((*Visit)(BT))//访问根结点
{
if(PreOrderTraverse(BT->LChild, Visit))//先序访问左子树
if(PreOrderTraverse(BT->RChild, Visit))//先序访问右子树
return TRUE;
return FALSE;
}
}
else
return TRUE;
}
//----------------------------------------------------------------------
BOOL InOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*))
{
//中序遍历二叉树,对每个结点调用Visit一次,且仅一次
//实现对结点的某种操作,Visit失败,则遍历失败
if(BT != NULL)
{
if(InOrderTraverse(BT->LChild, Visit))//中序访问左子树
{
if((*Visit)(BT))//访问根结点
if(InOrderTraverse(BT->RChild, Visit))//中序访问右子树
return TRUE;
return FALSE;
}
}
else
return TRUE;
}
//----------------------------------------------------------------------
BOOL PostOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*))
{
//后序遍历二叉树,对每个结点调用Visit一次,且仅一次
//实现对结点的某种操作,Visit失败,则遍历失败
if(BT != NULL)
{
if(PostOrderTraverse(BT->LChild, Visit))//后序访问左子树
{
if(PostOrderTraverse(BT->RChild, Visit))//后序访问右子树
if((*Visit)(BT))//访问根结点
return TRUE;
return FALSE;
}
}
else
return TRUE;
}
//----------------------------------------------------------------------
BOOL LevelOrderTraverse(BiTree BT, BOOL(*Visit)(BiNode*))
{
//层次性遍历二叉树,对每个结点调用Visit一次,且仅一次
//实现对结点的某种操作,Visit失败,则遍历失败
//使用数组模拟一个循环队列
if(BT == NULL)
return TRUE;
const int nCapicity = 300;
BiTree DT[nCapicity];
int nFront = 0, nRear = 1;
DT[0] = BT; //根结点入队
int nSize = 1;
while(nSize != 0) //队列非空
{
if(DT[nFront]->LChild)
{
//左子树非空,左子树入队
DT[nRear] = DT[nFront]->LChild;
++nRear;
++nSize;
}
if(DT[nFront]->RChild)
{
//右子树非空,右子树入队
DT[nRear] = DT[nFront]->RChild;
++nRear;
++nSize;
}
//访问队头元素,并出队
if(!(*Visit)(DT[nFront]))
return FALSE;
++nFront;
--nSize;
if(nSize > nCapicity)
return FALSE;
if(nRear == nCapicity)
nRear = 0;
if(nFront == nCapicity)
nFront = 0;
}
return TRUE;
}
说明:从上面的代码我们可以看到,如果在函数中除去结点的访问,则先序、中序和后序的遍历代码是完全一样。可见这三种次序的遍历仅在访问根的次序上存在差异。
2)销毁以BT为根结点的树
BiTree DestoryBiTree(BiTree BT)
{
//释放所有的树结点,并把指向树根的指针置空
//只能用后序free,否则需要1个(中序)或2个(前序)临时变量
//来保存BT->LChild和BT->RChild
if(BT)
{
DestoryBiTree(BT->LChild);
DestoryBiTree(BT->RChild);
free(BT);
}
return NULL;
}
说明:本人认为销毁操作以后序来销毁比较好,因为它是最为直观的做法,因为如果采用先序来销毁,则需要两个变量来保存BT的左孩子(BT->LChild)和右孩子(BT->RChild),因为先销毁根,即free(BT)后,就不能再利用BT却直接引用其左孩子或右孩子,即不能使用这样的语句:DestoryBiTree(BT->LChild);DestoryBiTree(BT->RChild);。同样的道理,中序销毁需要一个变量来保存BT的右子树。
此外,此算法可用于销毁整棵树或树的任意子树,只要BT是所要删除的树的根的指针即可。
3)查找二叉树中结点值为c的结点
BiTree FindNode(BiTree BT, DataType c)
{
//返回二叉树BT中值为c的结点的指针
//若c不存在于BT中,则返回NULL
if(!BT)
return NULL;
else if(BT->cData == c) //找到相应的结点,返回其指针
return BT;
BiTree BN = NULL;
BN = FindNode(BT->LChild, c); //在其左子树中进行查找
if(BN == NULL) //没有找到,则继续在其右子树中进行查找
BN = FindNode(BT->RChild, c);
return BN;
}
说明:查找操作可选用先序、中序和后序查找中的任一种都可,这里采用的是先序的查找。此外如果你所用的语言支持引用类型,函数的定义变为BiTree FindNode(BiTree BT, const DataType &c)效率会更佳,由于C语言没有引用类型,所以只能写成上面的样子了。
4)求以BT为根结点的二叉树深度
int BiTreeDepth(BiTree BT)
{
//求树的深度
//从二叉树深度的定义可知,二叉树的深度应为其左、右子树深度的最大值加1,
//因为根结点也算1层。
if(BT == NULL) //若为空树,则返回-1
return -1;
else
{
int nLDepth = BiTreeDepth(BT->LChild); //求左树的深度
int nRDepth = BiTreeDepth(BT->RChild); //求右树的深度
if(nLDepth >= nRDepth)
{
return nLDepth+1;
}
else
{
return nRDepth+1;
}
}
}
说明:有些书上认为空树的深度为0,只有一个结点的二叉树的深度为1,但是这里我采用空树的深度为-1,只有一个结点的二叉树的深度为0的做法。
5)求二叉树中某结点的双亲结点
BiTree GetParent(BiTree BT, DataType c)
{
//获得值为c的结点的双亲结点,
//若c为根结点或不存在于树中,则返回NULL
if(!BT || BT->cData == c)
return NULL;
if((BT->LChild && BT->LChild->cData == c) ||
(BT->RChild && BT->RChild->cData == c))
return BT;
BiTree Parent = NULL;
Parent = GetParent(BT->LChild, c);
if(Parent == NULL)
Parent = GetParent(BT->RChild, c);
return Parent;
}
说明:在判断其左孩子或右孩子的值前,首先要判断其左孩子或右孩子是否为空,例如,若BT的左子树为空,则表达式BT->LChild->cData这样的语句是会产生异常的,所以在判等之前一定要检查其孩子是否为空。
此外,函数返回NULL意味着有两种可能的情况,一是此结点为树的根结点(根结点没有双亲结点),二是这个结点不存在于树中。所以在应用时,如果检测到返回值为NULL则还要判断值为c的结点是否是根结点,若它不是根结点,则表示在树BT中不存在值为c结点。
与查找同样的道理,如果你所用的语言支持引用类型,函数的定义变为BiTree GetParent (BiTree BT, const DataType &c)效率会更佳。
6)找出二叉树中的最大、最小值
BiTree MaxNode(BiTree BT)
{
//返回二叉树BT中结点的最大值
if(BT == NULL) //空树则返回NULL
return NULL;
BiNode *pMax = BT; //默认以树根作为当前最大结点
BiNode *tmp = MaxNode(BT->LChild); //找出左子树的最大结点
if(tmp != NULL)
{
//左子树存在,且左子对的最大结点大于当前最大结点
if(tmp->cData > pMax->cData)
pMax = tmp;
}
tmp = MaxNode(BT->RChild); //找出右子树的最大结点
if(tmp != NULL)
{
//右子树存在,且右子树的最大结点大于当前最大结点
if(tmp->cData > pMax->cData)
pMax = tmp;
}
return pMax;
}
说明:找出最小结点的算法思想实现与此相同,在这里不再给出。这个算法主要要注意的就是左子树或右子树是否存在,以免因为访问内存的错误而让程序发生异常。因为左子对不存在时,根据代码可知它会返回NULL,则不能对其进行引用,即不能使用tmp->cData之类的语句。
7)求二叉树中的叶子结点和非叶子结点的个数
int LeavesCount(BiTree BT)
{
//返回二叉树BT中叶子结点的个数
if(BT == NULL)
return 0; //BT为空树,返回0
int nCount = 0;
if(!(BT->LChild || BT->RChild))
++nCount; //BT为叶子结点,加1
else
{
//累加上左子树上的叶子结点
nCount += LeavesCount(BT->LChild);
//累加上右子树上的叶子结点
nCount += LeavesCount(BT->RChild);
}
return nCount;
}
//----------------------------------------------------------------------
int NotLeavesCount(BiTree BT)
{
//返回二叉树BT中非叶子结点的个数
if(BT == NULL || (!(BT->LChild || BT->RChild)))
return 0; //若为BT为空树或为叶子结点,返回0
else
{
int nCount = 1; //此时根结点也是一个非叶子结点
//累加上左子树的非叶子结点个数
nCount += NotLeavesCount(BT->LChild);
//累加上右子树的非叶子结点个数
nCount += NotLeavesCount(BT->RChild);
return nCount;
}
}
说明:表达式:!(BT->LChild || BT->RChild)为判断一个结点是否为叶子结点,若为叶子结点,则值为真,否则为假。
3、补充
1)所有的算法中,对树的参数的传递都为传递所需要的树的根结点的指针,接口较为统一,使用方便简单,不易出错。
2)可对DataType进行重新定义来完全复用这些算法。
3)这些操作都是二叉树中很基本的操作,可通过这些操作组合出更多的功能和操作,这些函数本人通过简单的测试,没有发现运行错误。
如发现算法有错误,请各位读者指出!