关于链式二叉树,其实递归是最重要的,我们学会了递归,也就对二叉链表的基本操作有了很深刻的了解。

对于二叉树的基本结构:

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
1 #define MAXLEN 100 2 typedef char elementType; 3 4 typedef struct lBNode{ 5  elementType data; 6 struct lBNode *lChild,*rChild; 7 }BiNode,*BiTree;

为了代码的可移植性与简洁性,我们一般都需要定义许多的typedef

其次就是二叉树的建立,二叉树的建立一般来说分为两种,用户交互性的输入,一般来说是按先序输入每个节点的信息,当前节点为空,则输入特殊字符,相关代码如图所示

void createBinaryTree( BiNode *&BT )
{
    elementType x;
    cin>>x;

    if ( x == '/' )
    {
        return;
    }
    else
    {
        BT = new BiNode;
        BT->data = x;
        BT->lChild = NULL;
        BT->rChild = NULL;
        createBinaryTree(BT->lChild);
        createBinaryTree(BT->rChild);
    }
}

但是,这种输入的方法是有问题的,因为树的节点信息多而且比较乱,一旦输错,很有可能会前功尽弃,所以,我们一般会建议以文件的方式读取二叉树的相关信息,将文件中的节点信息读入一个二维数组中,然后根据二维数组来构建二叉树。

//二叉树数据文件
//二叉树标识--BinaryTree,无此标识将判定文件格式不对。
//每行表示二叉树中的一个结点。由3项组成:
     //第一项是结点元素值;
     //第二项表示此结点有无左子树,其中:1--有左子树,0--无左子树;
     //第三项表示此结点有无右子树,其中:1--有右子树,0--无右子树。
//结点次序(从上到下)必须严格按照先序遍历次序排列。
//文件扩展名没有限制,程序只根据标识“BinaryTree”判定是否二叉树数据文件。
//文件中可以有空行。
//文件可以有注释行,注释行必须以“//”开始,且注释行必须是单独的行,不能混杂在标识行、数据行中
//此二叉树有12个结点

BinaryTree

A  1  1
B  1  1
C  0  0
D  1  1
E  0  0
F  0  0
G  1  1
H  1  1
I  0  0
J  1  0
K  0  0
L  0  0

 

而文件格式一般来说,除了前面的注释,空格之类的东西,一般来说,首先是"BinaryTree“等相关标志,保证打开文件正确,并且告诉计算机下面开始读取数据,首先是将这个文本读取到一个二维数组中,代码如图所示:

bool ReadFileToArray( char fileName[],char strLen[100][3],int &nArrlen )
{
    //fileName存放文件名
    //strLen[][3]存放节点的二维数组,数组的三列对应数据文件的三列
    //nArrlen返回二叉树节点的个数
    FILE *pFile;
    char str[1000];         //读取第一行文本的字符串
    char strTmp[10] = "";
    pFile = fopen(fileName,"r");
    if ( pFile == NULL )
    {
        cout<<"二叉树文件打开失败!"<<endl;
        return false;
    }
    while ( fgets(str,1000,pFile) != NULL )
    {
        TrimLeft(str);      //这个函数是为了去掉左面的空格,须自定义
        if ( str[0] == '\n' )
        {
            continue;           //空行跳过
        }
        strncpy(strTmp,str,2);
        if ( strcmp (strTmp,"//") == 0 )
        {
            continue;
        }
        else
        {
            break;
        }
    }
    //读取文件第一行,判断标识是否正确
        if ( strcmp(str,"BinaryTree\n") != 0 )
        {
            cout<<"文件打开错误!"<<endl;
            fclose(pFile);
            return false;
        }
    nArrlen = 0;
    char s[10];
    fgets(s,10,pFile);//读取文件中的空行
    while(fscanf(pFile,"%c %c %c\n",&strLen[nArrlen][0],&strLen[nArrlen][1],&strLen[nArrlen][2]) != EOF )
    {
            nArrlen++;
    }
    fclose(pFile);
    return true;
}

我们通过这个代码可以看出来,前面的循环是为了跳过注释行,以及对“BinaryTree"标志进行判断,这里还用到了自定义函数TrimLeft,用于对左面空格进行消除,整体的代码组织比较简单,可用于一些简单的创立,对于这个,就不再过多的描述了

我们在将这个数组存入数据之后,便可以仿照上面的交互式创建二叉树的过程,将数组数据作为输入的标准

bool CreateBiTreeFromFile( BiNode *&pBT,char strLen[100][3],int nLen,int &nRow )
{
    //strLen[100][3]储存二叉树的节点信息
    //nLen表示节点个数
    //nRow表示当前的行数
    if ( nRow > nLen || nLen == 0 )
    {
        return false;
    }
    pBT = new BiNode;
    pBT->data = strLen[nRow][0];
    pBT->lChild = NULL;
    pBT->rChild = NULL;
    int nRowNext = nRow;
    if ( strLen[nRowNext][1] == '1' )
    {
        nRow++;
        CreateBiTreeFromFile(pBT->lChild,strLen,nLen,nRow);
    }
     if ( strLen[nRowNext][2] == '1' )
    {
        nRow++;
        CreateBiTreeFromFile(pBT->rChild,strLen,nLen,nRow);
    }
    return true;
}

通过这些代码,我们可以看到它其实就是交互性建立二叉树,通过递归先序创建二叉树,这套方法最有效,也是最容易理解的。

在建立完二叉树之后,我们便可以进行我们所想的操作,我在上面说过,二叉树的大部分操作都是建立在遍历的基础上,而遍历又分为先序,中序,后序,按组成结构来说,又分为递归与非递归,递归操作简单便于理解,许多操作用递归往往只需要几行代码便可以轻松解决,在本人看来,递归操作,也就是分治,将一个大问题分解成几个相同的小问题来解决,而小问题又可以再分,直到一个特殊的出口,而我们人脑所需要考虑的,便是开头和结尾,我们只需要考虑如何分,然后如何找到出口。

if (是出口)

{********}

else (不是出口)

{********}

至于中间如何分,分多少个,我们便不需要考虑,把这些工作都交给计算机来操作,当然,具体的操作还需要我们一点点去摸索,这中间还涉及到参数的传递,返回值等一系列问题,这些都需要读者自己去慢慢摸索,我们现在先对于树的遍历进行讨论,三种遍历方式如图所示//

//先序遍历
void
PreOrderTraverse( BiNode *BT ) { if ( BT ) { cout<<BT->data<<","; PreOrderTraverse(BT->lChild); PreOrderTraverse(BT->rChild); } }
//中序遍历
void InOrderTraverse( BiNode *BT ) { if ( BT ) { InOrderTraverse(BT->lChild); cout<<BT->data<<","; InOrderTraverse(BT->rChild); } }
//后序遍历
void PostOrderTraverse( BiNode *BT ) { if ( BT ) { PostOrderTraverse(BT->lChild); PostOrderTraverse(BT->rChild); cout<<BT->data<<","; } }

这三种遍历方式的区别便在于输出的位置,对一棵二叉树来说,遍历的方式其实只有一种,那就是从左到右,从上到下,指针的移动也正是遵循这个,但是,输出的位置不同就导致出现了三种遍历的方式,下面附一张图,便于你理解,

 关于二叉树链表的相关操作 算法

假如你理解了递归遍历的要点,那么许多的比较常见的树操作都是根据这三种遍历方法来实现的:

1、求二叉树的高度

 1 int DepthTree ( BiNode *BT )
 2 {
 3     if ( BT == NULL )
 4     {
 5         return 0;
 6     }
 7     else
 8     {
 9         int left,right;
10         left = DepthTree(BT->lChild);
11         right = DepthTree(BT->rChild);
12 
13         return ((left > right)?left:right) + 1;
14     }
15 }

求高度,根据上面所说,分治并考虑开头和出口,求一个节点的高度,其实就是找到左右子树最高的高度并加一,如图else,这样递归下去,总会有出口,当节点为空时,便返回0

就这样,我们就可以很轻松的求出二叉树的高度

同样的还有很多:

求二叉树的节点数目:

 1 int GetNodeNumber ( BiNode *BT )
 2 {
 3     if ( BT == NULL )
 4     {
 5         return 0;
 6     }
 7     else
 8     {
 9         return GetNodeNumber(BT->lChild) + GetNodeNumber(BT->rChild) + 1;
10     }
11 }

求叶子节点数:

 1 int GetLeafNumber( BiNode *BT )
 2 {
 3     if ( BT == NULL )
 4     {
 5         return 0;
 6     }
 7     else if ( BT != NULL && BT->lChild == NULL && BT->rChild == NULL )
 8     {
 9         return 1;
10     }
11     else
12     {
13         return GetLeafNumber( BT->lChild ) + GetLeafNumber( BT->rChild );
14     }
15 }

从这段代码我们可以注意到,对于出口的考虑是至关重要的,假如我们只考虑到BT为空的情况,那么最后一路返回值都会是零,那么结果就会错误,但假如我们只考虑到叶子节点的条件,那么我们势必会导致程序崩溃,因为BT为空没有出口,只能执行else,那么就会出错。

3、求分支节点

 1 int GetBranchNumber ( BiNode *BT )
 2 {
 3     if ( BT == NULL || ( BT->lChild == NULL && BT->rChild == NULL) )
 4     {
 5         return 0;
 6     }
 7     else
 8     {
 9         return GetBranchNumber(BT->lChild) + GetBranchNumber(BT->rChild) + 1;
10     }
11 }

4、求父节点

 1 BiTree GetParent ( BiNode *BT,elementType x )
 2 {
 3     BiTree  temp1,temp2;
 4     if ( BT->lChild != NULL || BT->rChild != NULL )
 5     {
 6         if ( (BT->lChild != NULL && BT->lChild->data == x) || (BT->rChild != NULL && BT->rChild->data == x) )  //假如是当前节点的话,则返回
 7         {
 8             return BT;
 9         }
10         else
11         {
12             if ( BT->lChild != NULL )
13             {
14                 temp1 =  GetParent(BT->lChild,x);  //在左子树寻找
15 
16             }
17             if ( BT->rChild != NULL )
18             {
19                 temp2 =  GetParent(BT->rChild,x);  //在右子树寻找
20             }
21             if ( temp1 != NULL )
22             {
23                 return temp1;
24             }
25             if ( temp2 != NULL )
26             {
27                 return temp2;
28             }
29         }
30     }
31     else
32     {
33         return NULL;        //未找到的话则返回空
34     }
35 }

5、求孩子节点

 1 void GetChild ( BiNode *BT,elementType x )
 2 {
 3     if (BT)
 4     {
 5         if ( BT->data == x )
 6         {
 7             if ( BT->lChild != NULL )
 8             {
 9                 cout<<"左孩子存在,为:"<<BT->lChild->data<<endl;
10             }
11             else
12             {
13                 cout<<"左孩子不存在"<<endl;
14             }
15             if ( BT->rChild != NULL )
16             {
17                 cout<<"右孩子存在,为:"<<BT->rChild->data<<endl;
18             }
19             else
20             {
21                 cout<<"右孩子不存在"<<endl;
22             }
23         }
24         else
25         {
26             GetChild(BT->lChild,x);
27             GetChild(BT->rChild,x);
28         }
29     }
30 }

6、求某节点的层次

 1 int GetNodeLevel ( BiNode *BT,elementType x,int h )    //注意这个h是默认的值1
 2 {
 3     if ( BT == NULL )
 4     {
 5         return 0;
 6     }
 7     else if ( BT->data == x )
 8     {
 9         return h;
10     }
11     else
12     {
13         int l = GetNodeLevel(BT->lChild,x,h+1);//先遍历左子树,假如左子树不存在,再遍历右子树
14         if ( l != 0 )
15         {
16             return l;
17         }
18         else
19         {
20             return GetNodeLevel(BT->rChild,x,h+1);
21         }
22     }
23 }

7、交换左右子树的指针

 1 void SwapSubTree( BiNode *BT )
 2 {
 3     BiNode *temp;
 4 
 5     if ( BT != NULL )
 6     {
 7         temp = BT->lChild;
 8         BT->lChild = BT->rChild;
 9         BT->rChild = temp;
10         SwapSubTree(BT->lChild);
11         SwapSubTree(BT->rChild);
12     }
13 }

8、求两个节点最近的公共祖先

 1 BiTree CommonParent( BiNode *BT,elementType x1,elementType x2 )
 2 {
 3     if ( BT == NULL || BT->data == x1 || BT->data == x2 )
 4     {
 5         return BT;
 6     }
 7     else
 8     {
 9         BiTree left = CommonParent(BT->lChild,x1,x2);
10         BiTree right = CommonParent(BT->rChild,x1,x2);
11 
12         if ( left != NULL && right != NULL )
13         {
14             return BT;
15         }
16         else if ( left == NULL && right != NULL )
17         {
18             return right;
19         }
20         else if ( left != NULL && right == NULL )
21         {
22             return left;
23         }
24     }
25 }

9、二叉树的复制

 1 void BiTreeCopy ( BiNode *BT,BiNode *&bt )
 2 {
 3     if (BT)
 4     {
 5         bt = new BiNode;
 6         bt->data = BT->data;
 7         bt->lChild = NULL;
 8         bt->rChild = NULL;
 9         BiTreeCopy(BT->lChild,bt->lChild);
10         BiTreeCopy(BT->rChild,bt->rChild);
11     }
12 }

 

经过上面这么多的递归函数的训练,相信你也差不多对递归有一点理解了,其实他的关键便在于能否分治,以及在分治后,如何恰当的找到开头和出口。但是,递归也是具有很大的缺点的,一次次递归会占用大量的空间,他的时间和空间效率远比循环,非递归等操作要低,而且递归是一个整体的过程,我们很难让他中间停止。

但是,目前我们还没找到比递归遍历二叉树更好的方法,绝大多数高校都在课本中介绍了二叉树遍历的非递归遍历方法,但是,实际上这个是没有什么实际应用价值的,因为他只是对通过栈实现递归的模拟,而且他的时间和空间性能远比不上系统栈,所以,除了极个别函数之外,我们一般不建议使用它

和递归一样,非递归的操作也是从三种遍历开始的:


//先序非递归遍历
1
void PreOrderTraverseNR( BiNode *BT ) 2 { 3 BiNode *p; 4 Stack S; 5 initialStack(&S); 6 p = BT; 7 8 while( p || !stackEmpty(&S) ) 9 { 10 if (p) 11 { 12 cout<<p->data<<","; 13 pushStack(&S,p); 14 p = p->lChild; 15 } 16 else 17 { 18 popStack(&S,&p); 19 p = p->rChild; 20 } 21 } 22 }
  //中序非递归遍历
23 void InOrderTraverseNR( BiNode *BT ) 24 { 25 BiNode *p; 26 Stack S; 27 p = BT; 28 initialStack(&S); 29 30 while ( p || !stackEmpty(&S) ) 31 { 32 if (p) 33 { 34 pushStack(&S,p); 35 p = p->lChild; 36 } 37 else 38 { 39 popStack(&S,&p); 40 cout<<p->data<<","; 41 p = p->rChild; 42 } 43 } 44 }
  //后序非递归遍历
45 void PostOrderTraverseNR( BiNode *BT ) 46 { 47 BiNode *p; 48 Stack S; 49 int tag[MAXLEN]; 50 p = BT; 51 initialStack(&S); 52 53 while ( p || !stackEmpty(&S) ) 54 { 55 if (p) 56 { 57 pushStack(&S,p); 58 tag[S.top] = 0; 59 p = p->lChild; 60 } 61 else 62 { 63 stackTop(&S,&p); 64 if ( tag[S.top] == 0 ) 65 { 66 tag[S.top] = 1; 67 p = p->rChild; 68 } 69 else 70 { 71 popStack(&S,&p); 72 cout<<p->data<<","; 73 p = NULL; 74 } 75 } 76 } 77 }

对于非递归遍历便不再多说,他也仅仅是一种模拟罢了,先序和中序理解起来比较简单,自行找一个树进行模拟一下即可,对于后序遍历的树,它多加了一个tag用来标记是否遍历,后序遍历的左右根因为一个节点想要输出,必须要在左右节点子树都遍历过的情况下才能输出。

对于这种鸡肋的算法,其实也有很多函数也是通过这三种遍历来实现的。

求节点路径

这个算法其实有很多实现的方法,但是,我个人觉得比较正统的便是后序非递归遍历,因为后序遍历的特点便是左右子树先输出然后再输出根节点,也就是说,在找到一个节点的时候,那么根据栈先进后出的特点,栈中储存的便是一个又一个根节点,这便是天然的路径。

 1 int BiNodePath ( BiNode *BT,elementType x )
 2 {
 3     Stack S;
 4     initialStack(&S);
 5     BiNode *pt = BT;
 6     int tag[MAXLEN];
 7     int n;
 8     while ( pt != NULL || !stackEmpty(&S) )
 9     {
10         if (pt)
11         {
12             pushStack(&S,pt);
13             tag[S.top] = 0;
14             pt = pt->lChild;
15         }
16         else
17         {
18             stackTop(&S,&pt);
19             if ( tag[S.top] == 0 )
20             {
21                 tag[S.top] = 1;
22                 pt = pt->rChild;
23             }
24             else
25             {
26                 if ( pt->data == x )
27                 {
28                     int j = 0;
29                     while (!stackEmpty(&S))    //在找到这个节点时,弹出所有的栈元素,并结束程序
30                     {
31                         stackTop(&S,&pt);
32                         cout<<pt->data<<"--";
33                         popStack(&S,&pt);
34                         j++;
35                     }
36                     cout<<endl;
37                     return j;
38                 }
39                 popStack(&S,&pt);
40                 pt = NULL;
41             }
42         }
43     }
44 }

同样以这个算法为基础,我们便可以衍生出许多其他的算法

所有叶子节点到根的路径

 1 void LeafPath( BiNode *BT,BiNode *SBT )  //SBT为传入的树的根节点,便于后面寻找路径
 2 {
 3     if ( BT )
 4     {
 5         if ( BT->lChild == NULL && BT->rChild == NULL )
 6         {
 7             BiNode *temp = SBT;
 8             BiNodePath(temp,BT->data);
 9         }
10         LeafPath(BT->lChild,SBT);
11         LeafPath(BT->rChild,SBT);
12     }
13 }

最长路径

 1 void LongestPath ( BiNode *BT,BiNode *SBT,int height )//SBT为传入的树的根节点,便于后面寻找路径
 2 {
 3     if ( BT )
 4     {
 5         if ( BT->lChild == NULL && BT->rChild == NULL )
 6         {
 7             BiNode *temp = SBT;
 8             if ( height == 0 )
 9             {
10                 height = DepthTree(temp);
11             }
12             int j = BiNodePath(temp,BT->data);
13             if ( height == j )
14             {
15                 cout<<"此为最长路径"<<j<<endl;
16             }
17         }
18         LongestPath(BT->lChild,SBT,height);
19         LongestPath(BT->rChild,SBT,height);
20     }
21 }

以上便是以递归遍历和非递归遍历为首的一套树的相关函数

除此之外,还有许多其他的函数,他们也是上面各种函数的一种拓展

1、层次遍历

所谓层次遍历,就是逐层输出树的节点值,这里要用到队列,前面的无论是先序,中序,还是后序,都是有一种规律在约束着,先左后右,注定右面的要先入栈,但却无法输出,但队列不同,他满足先进先出,根节点输出后,便访问下面的两个节点,先访问,就先输出,这也便是层次输出

 1 void LevelOrderTraverse( BiNode *BT )
 2 {
 3     if ( BT == NULL )
 4     {
 5         cout<<"空树!"<<endl;
 6         return;
 7     }
 8     seqQueue Q;
 9     initialQueue(&Q);
10     inQueue(&Q,BT);
11     BiNode *t = BT;
12     BiNode *last,*nlast;  //这里的last和nlast是为了使输出的时候逐行输出
13     last = nlast = t;
14     while ( !queueEmpty(Q) )
15     {
16         queueFront(Q,&t);
17         cout<<t->data<<" ";
18         if ( t->lChild != NULL )
19         {
20             inQueue(&Q,t->lChild);
21             nlast = t->lChild;
22         }
23         if ( t->rChild != NULL )
24         {
25             inQueue(&Q,t->rChild);
26             nlast = t->rChild;
27         }
28         outQueue(&Q);
29 
30         if ( last == t )
31         {
32             cout<<endl;
33             last = nlast;
34         }
35     }
36 }

2、顺序表和链表的相互转化

void ArrayToBiTree ( BiNode *&BT,char str[],int i ) /*为了防止访问未知元素,必须提前*/
{                                                   /*将所有元素初始化*/
    if ( str[i] != '/' && i <= strlen(str) )//这里的str必须提前进行初始化即str[MAXLEN] = "";(所有的MAXLEN个元素都为\0)
    {
        BT = new BiNode;
        BT->data = str[i];
        BT->lChild = NULL;
        BT->rChild = NULL;

        ArrayToBiTree (BT->lChild,str,2*i);
        ArrayToBiTree (BT->rChild,str,2*i+1);
    }
}
void BiTreeToArray ( BiNode *BT,char str[],int i )
{
    if (BT)
    {
        str[i] = BT->data;
        BiTreeToArray (BT->lChild,str,2*i);
        BiTreeToArray (BT->rChild,str,2*i+1);
    }
    else
    {
        str[i] = '/';
    }
    int j = 1;
    str[0] = '/';
    while ( j < (i-1)/2 )    //将中间空缺的元素补齐
    {
        if ( str[j] == '\0')
        {
            str[j] = '/';
        }
        j++;
    }
}

 

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄