二叉树实现及相关操作知识梳理

树型结构

树

前言

树型结构在生活中是非常常见的一种结构,应用范围很广,就用一个简单的例子来说。计算机中的文件目录就是一个树型结构,一般创建一个文件,如果文件中没有文件那么就相当于一个空树,如果里面有文件,就相当于这个文件的子树,以此类推,就形成了树型结构的文件目录。
在学习中我们主要学习二叉树的一些特性,把树细化学习。

二叉树的概念

二叉树是结点的有限集合,该集合或者为空,或者由一个
根节点加上两颗对称的左子树和右子树的二叉树组成。
二叉树的每个结点上最多有两个子树
二叉树有五种形态,及只有根结点,空树,只有左子树,只有右子树,左右子树都有。

二叉树的分类

二叉树有完全二叉树和满二叉树两种

  • 满二叉树是所有分支节点都存在左右子树,并且叶子结点在同一层上。
  • 完全二叉树是具有N个结点的二叉树的结构与满二叉树前N个结点的结构相同

    怎么理解呢?
    满二叉树完全二叉树
    左图为满二叉树,有图为完全二叉树。

    二叉树性质

  • 二叉树最大结点数:若规定只有一个根结点的时候,深度为1,那么深度为K的树,最大结点数为 (2^K) -1 (K>=0)

  • 二叉树的第i层上的结点数:若规定只有一个根结点的时候,层数为1,那么一颗非空树的第i层最多有 2^(i-1) (i>0)

  • 叶子结点和非叶子结点的关系:对于任何一颗二叉树,如果叶子结点个数为N0,度为2的非叶子结点个数为N1,则有 N1 = N0 + 1;

  • 关于完全二叉树的深度:具有N结点的二叉树的的深度K为log2(n+1)上取整数

  • 顺序存出中父结点和子结点的关系:子结点要找到对应的父结点,(i - 1)/2;父找到左孩子i*2 + 1;找到右孩子i*2 + 1;

    二叉树的创建

    创建一颗二叉树,以顺序表,通过先序遍历的方式还原一颗二叉树(顺序表中必须有标识NULL的特殊字符存在)
    具体思路是采用递归的方式,用一个index来记录树创建过程中创建到数组中的哪个位置,递归的用先序遍历的方式去创建。
    具体代码:

    // 创建一个结点
    TreeNode* CreateTreeNode(TreeType value)
    {
     TreeNode* new_node = (TreeNode*)malloc(sizeof(TreeNode));
     // 用assert来判断new_node空间是否开辟成功。
     // 在这里我们直接让程序挂掉。(不同场景处理方式不同)
     assert(new_node);
     new_node->data = value;
     new_node->lchild = NULL;
     new_node->rchild = NULL;
     return new_node;
    }
    // 二叉树递归体,递归的建立一颗二叉树
    TreeNode* _CreateTree(TreeType arr[], size_t size, int* index, TreeType nulltype)
    {
     if (index == NULL || size < 1)
     {
         return NULL;
    }
    if (arr[*index] == nulltype)
    {
        return NULL;
    }
    // 先创建根结点
    TreeNode* root = Create Tree Node(TreeType value);
    ++(*index);
    // 递归的创建左子树
    TreeNode* root->lchild = _CreateTree(arr, size, index, nulltype);
    ++(*index);
    // 递归的创建右子树
    TreeNode* root->rchild = _CreatTree(arr,size, index, nulltype);
    return root;
    }
    // 函数主体
    TreeNode* CreateTree(TreeType arr[], size_t size, TreeType nulltype)
    {
    if (size < 1)
    {
        return NULL;
    }
    // 用来记录创建到数组中的哪个元素上
    int index = 0;
    return _CreateTree(arr, size, &index, nulltype);
    }

    二叉树的遍历

    创建好二叉树后,我们访问二叉树中的某个结点,就必须经过遍历。
    我们实现一下,二叉树的递归版的前、中、后序遍历和非递归版的前、中、后遍历。

递归版

  • 先序遍历(前序遍历)

    void PreOrder(TreeNode* root)
    {
        if (root == NULL)
        {
            return;
        }
        // 先打印根结点
        printf("%c  ", root->data);
        // 递归的打印左右子树
        PreOrder(root->lchild);
        PreOrder(root->rchild);
    }
  • 中序遍历

    void InOrder(TreeNode* root)
    {
         if (root == NULL)
         {
            return;
         }
         // 先递归的找到最左边的孩子
         InOrder(root->lchild);
         // 打印最左边的孩子,递归栈出栈打印根结点
         printf("%c  ", root->data);
         // 递归进右子树
         InOrder(root->rchild);
    }
    
  • 后续遍历

    void PostOrder(TreeNode* root)
    {
        if (root == NULL)
        {
            return;
        }
        // 先递归,找最左的孩子,打印,然后递归栈出栈,再进行最左子树的右子树进行递归
        PreOrder(root->lchild);
        PreOrder(root->rchild);
        printf("%c  ", root->data);
    }

    非递归版

    注意:这里我们在写非递归版,直接用顺序栈的函数接口,不做栈的实现。
    如果不知道栈的实现,戳这里栈的实现

  • 前序遍历
    前序遍历非递归采用手动栈来进行操作,栈的特性是先入后出,取栈顶元素,所以我们抓住这两个特性。 前序遍历,是根左右顺序遍历。
    1)先让根结点入栈,取栈顶元素打印,成功进(2),失败退出循环。
    2)进行出栈
    3)再进行入右孩子,入左孩子。
    就这样循环,直到栈为空,也就是取栈顶失败。遍历完成。
    代码如下:

    void PreOrederByLoop(TreeNode* root)
    {
         if (root == NULL)
         {
                 return;
         }
         // 创建一个栈。
         SeqStack stack;
         // 初始化一个栈(用C语言实现的栈)
         InitStack(&stack);
         // 先入根结点
         PushStack(&stack,  root);
         TreeNode* top = NULL;
         while (FindTopStack(&stack, &top)
         {
                 printf("%c ", top->data);
                 PopStack(&stack);
                 // 先入右孩子,后入左孩子
                 if (top->rchild != NULL)
                 {
                         PushStack(&stack, top->rchild);
                 }
                 if (top-lchild != NULL)
                 {
                         PushStack(&stack, top->lchild);
                 }
          }
    }
    
  • 中序遍历
    中序遍历,是采用手动栈。将函数放入一个while(1)的循环中。
    1)先循环的去从根结点到最左孩子的入栈。
    2)进行取栈顶元素,并判断是否取栈顶元素成功,
    3)如果失败break,退出while(1)训话。如果成功,打印栈顶元素,并出栈
    4)进行右孩子的判断是否为空,如果不为空,进行让循环指针指向取栈顶元素的右孩子。
    代码:

void InOrderByLoop(TreeNode* root)
{
        if (root == NULL)
        {
            // 非法输入
            return;
        }
        SeqStack Stack;
        InitStack(&stack);
        TreeNode* cur = root;
        while (1)
        {
            // 从根结点到最左边的孩子,并逐一入栈
            while (cur != NULL)
            {
                PushStack(&stack, cur);
                cur = cur->lchild;
            }
            // 取栈顶元素
            TreeNode* top = NULL;
            FindTopStack(&stack, &top);
            if (top == NULL)
            {
                // 取失败,退出循环。因为因为已经遍历完了。
                break;
            }
            // 打印并出栈。
            printf("%c ", top->data);
            PopStack(&stack);
            // 让cur尝试的去找最左孩子的右孩子。
            cur = top->rchild;
        }
}
  • 后序遍历
    后序遍历,借助手动创建的栈,采用while(1)循环作为大的循环条件。
    1)循环的从根结点到最左孩子,并且逐一入栈。
    2)取栈顶元素。如果取失败就break;
    3)判断是否存在最左孩子是否存在右孩子。还要判断右孩子是否等于上一个取栈顶的元素,这样做是防止重复遍历。
    4)如果不存在右孩子或者右孩子不等于上次取栈顶元素,那么可以打印并且出栈。
    5)否则就让cur 指向栈顶元素的右孩子
    代码:
void PostOrder(TreeNode* root)
{
    if (root == NULL)
    {
        // 非法输入
        return;
    }
    SeqStack satck;
    InitStack(&stack);
    // 用来记录上一个top的元素
    TreeNode* pre = NULL;
    TreeNode* cur = root;
    while (1)
    {
        // 循环从根到最左孩子的入栈
        while (cur != NULL)
        {
            PushStack(&stack, cur);
            cur = cur->lchild;
        }
        // 取栈顶元素
        TreeNode* top = NULL;
        FindTopStack(&stack, &top);
        if (top == NULL)
        {
            // 取失败退出,说明遍历结束
            break;
        }
        // 满足个两个条件中的一个就可以打印并且出栈。
        if (top->rchild == NULL || top->rchild == pre)
        {
            printf("%c ", top->data);
            PopStack(&stack);
            pre = top;
        }
        else
        {
            // 存在右孩子,继续入右孩子。
            cur = top->rchild;
        }
    }
}
  • 层序遍历
    对于层序遍历,也是一个很重要的点,需要掌握。层序遍历就是,从上到下,从左到右依次的进行遍历。那么前面我们说的,前中后序遍历,都是借助栈的先进后出,可以访问栈顶元素的特点来进行的遍历。然而对于层序遍历用队列的先进先出,访问队首的方式遍历。对队列不熟悉的可以戳这里链式队列的实现循环顺序队列的实现
    1)先让根结点入队。
    2)以取队守元素为条件进行循环
    3)打印并出队
    4)判断是否左孩子为空,不为空就让左孩子入队,
    5)再判断右孩子是否为空,不为空就让右孩子入队
    代码:
void LevelOrder(TreeNode* root)
{
    if (root == NULL)
    {
        // 非法判断
        return;
    }
    SeqQueue queue;
    InitQueue(&queue);
    PushQueue(&queue, root);
    // 取栈顶元素,并且取队守元素是否为空。
    TreeNode* head = NULL;
    while (FindHead(&stack, &head))
    {
        // 打印并且出队
        printf("%c ", head->data);
        PopQueue(&queue);
        if (head->lchild != NULL)
        {
            PushQueue(&queue, head->lchild);
        }
        if (head->rchild != NULL)
        {
            PushQueue(&queue, head->rchild);
        }
    }
}
  • 判断是否是完全二叉树
    判断是否是完全二叉树,更多的是在层序遍历的基础上变化而来,那么我们也需要借助队这个数据结构来进行。判断完全二叉树的依据就是在满二叉树的下一层,从左到右,要么只有左孩子,要么左右都有,不能只有右没有左。
    1)我们需要创建队列,进行初始化,将root结点入队
    2)取队首元素,判断是否为NULL,如果为NULL,直接break。
    3)出队。入左孩子和右孩子。
    4)循环判断队列是否大于0
    如果是,就让原来的队列出队,并且取队首元素,判断是否为空,是就返回0。不是就继续
    如果不是就返回1表示是完全二叉树
    代码实现:

    int IsCompleteTree(TreeNode* root)
    {
        if (root == NULL)
        {
             // 非法判断
             return 0;
        }
        SeqQueue queue;
        InitQueue(&queue);
        // 用来取队首元素
        TreeNode* head = NULL;
        PushQueue(&queue, root);
        while (SizeQueue(&queue) > 0)
        {
            FindHeadQueue(&queue, head);
            if (head == NULL)
            {
                break;
            }
            PopQueue(&queue);
            PushQueue(&queue, head->lchild);
            PushQueue(&queue, head->rchild);
        }
        while (SizeQueue(&queue) > 0)
        {
            // 当上面循环跳出,进入本循环,队首元素为NULL先出队后取
            PopQueue(&queue);
            FindHeadQueue(&queue, &head);
            if (head != NULL)
            {
                reuturn 0;
            }
        }
        return 1;
    }
    

    以上为二叉树基础知识整理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值