二叉树是一种常用的数据结构,在程序中也经常需要使用二叉树,但是你所使用语言却并不一定提供了二叉树这种数据类型,所以为了方便使用,我们可以自己实现一个二叉树的数据类型。在需要时就像使用其他已定义的类型一样方便。
下面给出一些本人写的算法和解释(基于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*))
- {
-
-
- 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*))
- {
-
-
- 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*))
- {
-
-
- 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*))
- {
-
-
-
- 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)
- {
-
-
-
- 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)
- {
-
-
- 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)
- {
-
-
-
- if(BT == NULL)
- 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)
- {
-
-
- 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)
- {
-
- if(BT == 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)
- {
-
- if(BT == NULL)
- return 0;
-
- int nCount = 0;
- if(!(BT->LChild || BT->RChild))
- ++nCount;
- else
- {
-
- nCount += LeavesCount(BT->LChild);
-
- nCount += LeavesCount(BT->RChild);
- }
- return nCount;
- }
-
- int NotLeavesCount(BiTree BT)
- {
-
- if(BT == NULL || (!(BT->LChild || BT->RChild)))
- return 0;
- else
- {
- int nCount = 1;
-
- nCount += NotLeavesCount(BT->LChild);
-
- nCount += NotLeavesCount(BT->RChild);
- return nCount;
- }
- }
说明:表达式:!(BT->LChild || BT->RChild)为判断一个结点是否为叶子结点,若为叶子结点,则值为真,否则为假。
3、补充
1)所有的算法中,对树的参数的传递都为传递所需要的树的根结点的指针,接口较为统一,使用方便简单,不易出错。
2)可对DataType进行重新定义来完全复用这些算法。
3)这些操作都是二叉树中很基本的操作,可通过这些操作组合出更多的功能和操作,这些函数本人通过简单的测试,没有发现运行错误。
如发现算法有错误,请各位读者指出!