数据结构-------二叉树的基本操作(递归实现)

        首先介绍二叉树的概念。

        不同于链表等线性表,树是一种非线性表。除了根节点没有前驱外,其余节点都有唯一的一个双亲结点和多个或0个孩子节点。其中,对每个节点来说,最多有两个孩子节点的称为二叉树。二叉树的几种形式如下图:


        对于二叉树的任一结点,都可以表示为以上的任一一种形式。所以,一个二叉树是由无数个子树组成。每棵子树都可以表示为上图中的任一一种形式。因此,在对二叉树进行遍历时,可以采取递归的形式来进行。

1. 二叉树的表示方法

        二叉树中的节点最多有两个孩子节点,分别为左孩子和右孩子。所以,这里采用孩子表示法来表示二叉树的结点。一个二叉树的结点包含一个数据域,该结点的左孩子及右孩子。

//二叉树中各节点的数据类型定义
typedef char TreeType;

//二叉树中各节点的结构定义
typedef struct TreeNode
{
    TreeType data;//节点的数据域
    struct TreeNode* lchild;//指向左节点的指针
    struct TreeNode* rchild;//指向右节点的指针
}TreeNode;       

2. 二叉树的初始化

        与用头结点的指针来表示一个链表类似,这里用根节点的指针来表示一棵树。初始时,树中没有任何结点,所以将根节点的指针取值置为空即可。因为这里要改变根节点的指针取值,所以,需要传递二级指针作为参数:

void TreeInit(TreeNode** proot)                                                                                                       
{
    if(proot == NULL)
    {
        //非法输入
        return;
    }

    *proot = NULL;
    return;
}

3. 二叉树的先序遍历

        先序遍历的顺序为:先遍历根节点,然后遍历左子树,最后遍历右子树。在遍历左右子树时,将左右子树再当做一棵树,按照先序遍历的顺序对其进行遍历。所以要对左右子树进行递归遍历。根节点为空时,即为递归出口。

        代码如下:

//先序遍历树
void PreOrder(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        printf("# ");//输出#表示空节点                                                                                                
        return;
    }

    //输出根节点
    printf("%c ",root->data);
    //递归遍历左子树
    PreOrder(root->lchild);
    //递归遍历右子树
    PreOrder(root->rchild);
    return;
}

4. 二叉树的中序遍历

        与先序遍历类似,在中序遍历时,先递归遍历左子树,然后访问根节点,最后递归遍历右子树。当结点为空时,即到达递归出口。

//中序遍历
void InOrder(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        printf("# ");
        return;
    }

    //中序遍历左子树
    InOrder(root->lchild);
    //输出根节点
    printf("%c ",root->data);
    //中序遍历右子树
    InOrder(root->rchild);
    return;
}

5. 二叉树的后序遍历

        有二叉树的先序遍历类似,在后序遍历时,先递归遍历左子树,再递归遍历右子树,最后访问根节点。结点为空时即为递归出口。

//后序遍历
void PostOrder(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        printf("# ");
        return;
    }
    //后序遍历左子树
    PostOrder(root->lchild);
    //后序遍历右子树
    PostOrder(root->rchild);
    ////输出根节点
    printf("%c ",root->data);
    return;
}

6. 二叉树的层序遍历

        二叉树的层序遍历需要借助一个队列来实现。

(1)首先将根节点入队

(2)取队首元素

(3)如果队首元素不为为空,转至(4),否则,直接返回

(4)访问队首元素,将队首元素出队,如果队首元素的左右孩子不为空,分别将左右孩子入队,转至(2)

        这里需要注意的是,入队列时保存的时结点的指针,所以,队列中保存的时树的结点指针类型的数据。有关队列的基本操作见:顺序队列/链式队列

        例如:



        代码如下:

//层序遍历
void LevelOrder(TreeNode* root)                                                                                                       
{
    if(root == NULL)
    {
        //空树
        return;
    }

    //借助队列来完成层序遍历
    SeqQueue queue;//定义一个顺序队列
    SeqQueueInit(&queue);//初始化队列
    
    //1. 将根节点的指针入队列
    SeqQueuePush(&queue,root);
    int ret;
    SeqQueueType front;
    while(1)
    {
        //2. 取队首元素,如果队列为空,说明已经遍历结束
        ret = SeqQueueTop(&queue,&front);
        if(ret == -1)
        {
            //队列已空,树已经遍历完
            break;
        }
   //3. 打印队首元素
        printf("%c ",front->data);
        //4. 队首元素出对列
        SeqQueuePop(&queue);
        //5. 如果队首元素的左右节点非空,则将左右节点的指针入队
        if(front->lchild != NULL)
        {
            SeqQueuePush(&queue,front->lchild);
        }
        if(front->rchild != NULL)
        {
            SeqQueuePush(&queue,front->rchild);
        }
        //6. 循环2.~5.
    }
    return;
}

7. 根据先序遍历的结果创建一棵树

        根据先序遍历的序列:ABC,还原出一棵树,则该树是不定的,如:


        所以除了知道先序遍历的序列,还需要知道空树的位置。如果空树用#表示,则AB##C##可唯一的表示为上述的左图。ABC####可唯一的表示为上述的右图。所以根据先序遍历的结果来还原一棵树时,除了先序序列外还需要空树的位置。

        因为已知的是先序遍历的结果,所以在创建树时,也是根据先根节点,再递归创建左子树,最后递归创建右子树的顺序来创建的。

        假如有一个字符串序列“AB##C##”。

(1)首先,定义当前位置下标index用来表示字符串中字符的下标,初始为0。

(2)然后根据index处的值来创建树的结点。当遇到空树时,不做处理,直接返回即可。

(3)最后,返回创建的树的根节点的指针。

        在利用递归函数创建结点时,

(1)根据A创建根节点。然后,index++,指向B

(2)再递归创建左子树。在创建左子树时,也是按照:根节点1->左子树1->右子树1的顺序来创建的。所以,

          根据B创建左子树的根节点1,然后index++,此时index指向#,说明左子树的左子树1为空树。index++

          index还指向#,说明左子树的右子树1为空树。此时根节点的左子树创建完毕。然后index++。

(3)再递归创建根节点的右子树。在创建右子树时,也是按照:根节点2->左子树2->右子树2的顺序来创建的。

          所以,此时,根据C创建右子树的根节点2,然后index++,此时,index指向#,说明右子树的左子树2为空

          树。index++,index还指向#,说明右子树的左子树2为空树。此时,index已经遍历完整个字符串序列

          了,直接返回即可。

        上述创建过程如下图所示:


        代码如下:

//根据先序遍历的结果创建一棵树
//arr为带#的先序序列字符串,size为字符串的长度,index为位置下标,null_flag为空树的标志
TreeNode* _TreeCreate(TreeType arr[],size_t size,size_t* index,char null_flag)
{                                                                                                                                     
    if(arr == NULL ||  size < 0 || index == NULL)
    {
        //非法输入
        return NULL;
    }
    
    if(*index == size)
    {
        //数组已遍历结束,树创建完成
        return NULL;
    }
    if(arr[*index] == null_flag)//null_flag是空树的标志,这里为#
    {
        //空树
        return NULL;
    }
    //创建根节点
    TreeNode* root = CreateNode(arr[*index]);
    (*index)++;
    //递归创建左子树
    root->lchild = _TreeCreate(arr,size,index,null_flag);
    (*index)++;
    //递归创建右子树
    root->rchild = _TreeCreate(arr,size,index,null_flag);
    return root;
}
TreeNode* TreeCreate(TreeType arr[],size_t size,char null_flag)
{
    size_t index = 0;
    TreeNode* root =  _TreeCreate(arr,size,&index,null_flag);
    return root;
}

8. 克隆一棵树

        树是递归创建的。所以在克隆一棵树时,也要通过递归来实现。这里采用先序遍历的方法来克隆一棵树。

(1)首先,根据已知树的根节点来创建新树的根节点

(2)再根据已知树的左子树来创建新树的左子树

(3)最后根据已知树的右子树来创建新树的右子树

(4)子树的创建过程也遵照(1)~(3)的顺序。

(5)当遇到空树时,直接返回,跳出递归函数即可。

        如已知树:


        新树的克隆创建过程与7中创建树的过程图相似。

        代码如下:

//克隆一棵树
TreeNode* TreeClone(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        return NULL;
    }
    
    TreeNode* new_root;
    //克隆根节点
    new_root = CreateNode(root->data);
    //递归克隆左子树
    new_root->lchild = TreeClone(root->lchild);
    //递归克隆右子树
    new_root->rchild = TreeClone(root->rchild);
    return new_root;
}

9. 求二叉树的结点个数

        这里有两种方法来实现该问题。

方法一:

        将先序遍历时的打印操作改为计数器加1操作,当遍历到一个非空节点时,计数器加1。所以,这里也利用递归函数来实现:

(1)首先遍历根节点,此时,计数器加1

(2)再递归遍历左子树。

(3)最后递归遍历右子树

(4)子树的遍历过程也遵循(1)~(3)

(5)当为空树时,直接返回,跳出递归函数即可。

        因为,在递归过程中要一直改变计数器变量的值,所以,该变量不能在递归函数中定义,而是在递归函数外定义,在根据该变量的地址来改变其值。

        代码如下:

void _TreeSize(TreeNode* root,size_t* count)
{
    if(root == NULL)
    {
        //空树
        return;
    }
    if(count == NULL)
    {
        //非法输入
        return;
    }
    (*count)++;
    _TreeSize(root->lchild,count);
    _TreeSize(root->rchild,count);
    return;
}

//求二叉树中的结点个数
size_t TreeSize(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        return 0;
    }
    size_t count = 0;
    _TreeSize(root,&count);
    return count;
}  

方法二:

        也是根据递归函数来实现。如果树不为空,则根结点的个数一定为1

(1)统计根节点左子树的个数

(2)统计根节点右子树的个数

(3)左子树的结点个数加右子树的结点个数再加1即为整棵树的结点个数

(4)统计左右子树结点个数时也是按照(1)~(3)来进行的

(5)当树为空时,根节点的个数为0,此时即到达递归函数的递归出口。

        代码如下:

//求二叉树中的结点个数
size_t TreeSize(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        return 0;
    }
    //统计左子树的节点个数
    size_t lsize = TreeSize(root->lchild);
    //统计右子树的节点个数
    size_t rsize = TreeSize(root->rchild);
    //整棵树的节点个数为左子树加右子树加根节点的个数                                                                                  
    return 1 + lsize + rsize;
}

10. 求二叉树中叶子结点的个数

        与上个问题类似,也有两种方法。

方法一:

       将先序遍历时的打印操作改为:先判断是否为叶子结点(左孩子和右孩子均为空),如果是,计数器加1操作,如果不是,则不作处理。当遍历到一个非空节点时,判断一次。所以,这里也利用递归函数来实现:

(1)先判断根节点

(2)再递归遍历右子树

(3)最后递归遍历左子树

(4)子树的遍历过程遵照(1)~(3)

(5)当为空树时,即到达递归出口

void _TreeLeafCount(TreeNode* root,size_t* count)
{
    if(root == NULL)
    {                                                                                                                                 
        return;
    }
    if(count == NULL)
    {
        //非法输入
        return;
    }
    if(root->lchild == NULL && root->rchild == NULL)
    {
        (*count)++;
    }
    _TreeLeafCount(root->lchild,count);
    _TreeLeafCount(root->rchild,count);
}

//求二叉树中叶子节点个数
size_t TreeLeafCount(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        return 0;
    }
    size_t count = 0;
    _TreeLeafCount(root,&count);
    return count;
}

方法二:

(1)当树为空时,叶子结点个数必为0

(2)当只有一个根节点时,也自己结点个数必为1

(3)不为(1)(2)时,先递归统计左子树的叶子结点个数,再递归统计右子树的叶子结点个数

(4)将左右子树叶子结点个数求和即为整个树的叶子结点个数

        代码如下:

//求二叉树中叶子节点个数
size_t TreeLeafCount(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        return 0;
    }
    if(root->lchild == NULL && root->rchild == NULL)
    {
        return 1;//只有根节点时
    }
    //递归统计左子树的叶子节点个数
    size_t lcount = TreeLeafCount(root->lchild);
    //递归统计右子树的叶子节点个数
    size_t rcount = TreeLeafCount(root->rchild);
    //左右子树的叶子节点个数求和                                                                                                      
    return lcount + rcount;
}

11. 求二叉树第K层结点的个数

        如果一棵树非空,K=1时,结点个数为1。K=2时,结点个数为2,此时,相当于求根节点的左孩子的个数及右孩子的个数之和,即对于左孩子和右孩子来说,是求K=1时的结点个数。

        因此,对于任意的K来说,相当于求根节点的左孩子的第K-1层结点个数和右孩子的第K-1层结点个数之和。依次往下递归,直到最后当K=1时,节点个数为1,此时即到达递归出口。

        代码如下:

//求二叉树第k层节点的个数
size_t TreeKLevelSize(TreeNode* root,int K)
{
    if(K < 1)
    {
        return 0;
    }
    if(root == NULL)
    {
        //空树
        return 0;
    }                                                                                                                                 
    if(K == 1)
    {
        return 1;
    }
    return TreeKLevelSize(root->lchild,K-1) + TreeKLevelSize(root->rchild,K-1);
}

12. 求二叉树的深度或高度

(1)当二叉树为空时,深度为0

(2)当只有根节点时,深度为1

(3)当不为(1)(2)时,二叉树的深度为根节点的左子树与根节点右子树深度的最大值加1

(4)其中,根节点的左右子树的深度按照(1)~(3)来递归求解

(5)上述的二叉树为空时,即为递归出口。

        代码如下:

//求二叉树的深度或高度
size_t TreeHeight(TreeNode* root)
{
    if(root == NULL)
    {
        //空树
        return 0;
    }
    if(root->lchild == NULL && root->rchild == NULL)
    {
        return 1;
    }
    //统计左子树的深度
    size_t lheight = TreeHeight(root->lchild);
    //统计右子树的深度
    size_t rheight = TreeHeight(root->rchild);
    //左右子树的最大值加1                                                                                                             
    return (lheight > rheight)? 1+lheight : 1+rheight;
}

13. 在二叉树中查找指定结点所在的地址

        该问题与先序遍历操作类似,当遍历到一个非空结点时,比较该结点是否与指定值相同,相同,则直接返回地址即可,不相同,则继续遍历。所以,先遍历根节点,在递归遍历左子树,最后递归遍历右子树。当树为空或找到指定值时,即到达递归出口。

        代码如下:

//在二叉树中查找节点(给定数值,求对应节点的指针,假设二叉树中的节点是不重复的)
TreeNode* TreeFind(TreeNode* root,TreeType to_find)
{
    if(root == NULL)
    {
        //空树
        return NULL;
    }
    if(root->data == to_find)
    {
        return root;
    }
    //递归遍历左子树进行查找
    TreeNode* lret = TreeFind(root->lchild,to_find);
    //递归遍历右子树进行查找                                                                                                          
    TreeNode* rret = TreeFind(root->rchild,to_find);
    return (lret == NULL)? rret : lret;
}

14. 求指定节点的父结点

        该问题与遍历操作类似,这里采用先序遍历的方式进行。当遍历到一个结点时,判断该结点的左右孩子是否为指定结点,如果是,则直接返回该结点即可。如果不是,则继续往后遍历,直到遍历完整棵树。所以,根据:先遍历根节点,在递归遍历左子树,最后递归遍历右子树的顺序来遍历整棵树。当树为空或找到指定结点的父节点时,即达到递归出口。

        代码如下:

//求当前节点的父节点
TreeNode* Parent(TreeNode* root,TreeNode* child)
{
    if(root == NULL)
    {
        //空树
        return NULL;
    }
    if(child == NULL)
    {
        //非法输入
        return NULL;
    }                                                                                                                                 
    if(root->lchild == child || root->rchild == child)
    {
        return root;
    }   
    TreeNode* lret = Parent(root->lchild,child);
    TreeNode* rret = Parent(root->rchild,child);
    return (lret != NULL)? lret : rret;
    
}   

15. 查找指定结点的左右孩子

        当指定结点不为空时,直接返回指定结点的左右孩子的指针即可。

        代码如下:

//查找指定节点的左孩子
TreeNode* LChild(TreeNode* node)
{                                                                                                                                     
    if(node == NULL)
    {
        return NULL;
    }
    return node->lchild;
}
//查找指定节点的右孩子
TreeNode* RChild(TreeNode* node)
{
    if(node == NULL)
    {
        return NULL;
    }
    return node->rchild;
}

16. 销毁一棵树

        销毁树时与遍历操作类似,当遍历到一个结点时,将该结点所占有的内存释放即可。上面有介绍过三种遍历方法。当采用先序遍历进行销毁时,先销毁的是根节点,这时根节点的左右孩子就找不到了,所以在销毁根节点前要保存左右孩子的地址。同理,中序遍历进行销毁时也需要保存右孩子的地址。而采取后序遍历进行销毁时,只需按照先左孩子,在右孩子,最后根节点的顺序进行递归销毁即可。此时,并不需要保存任何结点的地址。所以,这里采取后序遍历的方法进行销毁。当结点为空时,即到达递归出口。

        注意:当销毁一个节点后,为防止该结点的指针变为野指针,所以,free一个节点后,再将该指针置为空。所以这里需要改变指针的指向即要改变指针的值,因此在函数传参时,传递的是二级指针。

        代码如下:

void DestroyNode(TreeNode* node)
{
    free(node);
    return;
}

//void TreeDestroy(TreeNode** root);
//二叉树的销毁(后序遍历销毁)
void TreeDestroy(TreeNode** root)
{
    if(root == NULL)
    {   
        //非法输入
        return;
    }   
    if(*root == NULL)
    {   
        //空树
        return;
    }   
    TreeDestroy(&((*root)->lchild));//销毁左子树
    TreeDestroy(&((*root)->rchild));//销毁右子树
    DestroyNode(*root);//销毁根节点
    *root = NULL;//将销毁的结点置为空
    return;
}





        





















猜你喜欢

转载自blog.csdn.net/sandmm112/article/details/80173921
今日推荐