09 树和二叉树 【实现代码及图解】简单易上手

相关概念

  1. 树的定义: 树是n(n>=0)个结点的有限集,一对多的关系。
  2. 当n=0时,称为空树。
  3. 在任意一棵非空树中应满足:
    1. 有且仅有一个特定的称为根的结点。
    2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,并且称为根的子树。 显然,树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。
  4. 树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
    1. 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
    2. 树中所有结点可以有零个或多个后继。

树的表现形式

在这里插入图片描述

树的基本术语

在这里插入图片描述

  1. 根结点:非空树中无前驱结点的结点
  2. 子结点(孩子): 结点若干分支下的结点为该结点的子结点。
  3. 父结点(双亲): 子结点上一个的结点称为子结点的父节点
  4. 子孙结点:某结点下的任意结点都为该结点的子孙结点
  5. 祖先结点:从根结点到某一结点所经过的所有结点都为该结点的祖先结点
  6. 兄弟结点,堂兄弟结点(不赘述)
  7. 结点的度:父结点拥有的的子节点数
    1). 度=0: 终端节点(叶子)
    2). 度!=0: 分支节点,根结点以外的分支结点为内部结点
  8. 树的度: 树内各结点的最大值
  9. 树的深度(高度): 树中结点的最大层次
  10. 有序树: 树中结点的各子树有次序
  11. 无序树: 树中结点的各子树无次序
  12. 森林: m(m>=0)棵互不相交树的集合

二叉树

二叉树相关概念:

1. 定义: 二叉树是n个结点的有限集合,该集合或者为空,或者是由一个根结点及两棵互不相交分别称为左子树和右子树的二叉树组成。

2. 两种特殊形式:

  1. 满二叉树:深度为k的二叉树的结点数为2**k-1则为满二叉树。
    在这里插入图片描述

  2. 完全二叉树:当且仅当每一个结点都与深度相同的满二叉树中编号一一对应,称之为完全二叉树。
    在这里插入图片描述

3. 特点:

  1. 每个结点最多有两个子结点,即二叉树不存在度大于2的结点。
  2. 二叉树的子树有左右之分,其子树的次序不能颠倒。
  3. 二叉树可以是空集合,根可以有空的左子树或空的右子树

注意:二叉树不是特殊的树,它们是两个概念,区分如下图
在这里插入图片描述

4. 二叉树的5种表示形态
在这里插入图片描述

注意:关于树的基本术语对二叉树都适用

二叉树的性质

  1. 在二叉树的第i层上至多有2**(i-1)个结点(i>=1),至少有1个结点。

  2. 深度为k的二叉树至多有2**k-1个结点(k>=1),至少有k个结点。

  3. 对于任意一棵二叉树T,若叶子树为n0,度为2的结点数为n2,则n0=n2+1

    **边数计算的两种方式:在这里插入图片描述

  4. 具有n个结点的完全二叉树的深度k=(log2n)+1 [ log2n向下取整 ]

  5. 完全二叉树中双亲结点与孩子结点的编号关系

    1. 双亲结点编号: i/2(向下取整)
    2. 左孩子结点编号: 2i
    3. 右孩子结点编号: 2i+1

二叉树的存储结构

1. 顺序存储结构: 按满二叉树的结点层次编号,依次存放二叉树中的数据元素。(如图)

在这里插入图片描述

特点:结点间的关系蕴含在存储的位置中,但比较浪费空间,适合满二叉树或完全二叉树。

二叉树的顺序存储表示:

#define MAXTSIZE 100
typedef int SqBiTree[MAXTSIZE]; //Binary Tree二叉树
SqBiTree bt;

2. 链式存储结构

  1. 二叉链表

    • 二叉树结点的特点:
      在这里插入图片描述

    • 二叉链表结点表示:

      //二叉链表结点表示:
      typedef struct BiNode{
      int data;
      struct BiNode *lchild, *rchild;//左右孩子指针
      }BiNode,*BiTree;
      
    • 练习题:
      在这里插入图片描述

  2. 三叉链表

    • 三叉链表结点表示:
    typedef struct TriTNode{
    int data;
    struct TriTNode *lchild, *rchild;//左右孩子指针
    }BTriTNode,*TriTree;
    

遍历二叉树

  • 遍历定义: 顺着某一条搜索路径访问二叉树中的结点,使每个结点仅被访问一次。
  • 遍历目的: 得到树中所有结点的线性排列
  • 遍历用途: 它是树结构插入,删除,修改,查找和排序运算的前提,是二叉树一切运算的基础和核心
  • 遍历方法:依次访问二叉树中的根节点,左子树,右子树,便是遍历整个二叉树。
  • 遍历方案:DLR,LDR,LRD,DRL,RDL,RLD(D:根节点,L:左子树,R:右子树)

详解DLR,LDR,LRD

1.先序遍历DLR:

在这里插入图片描述

  • 算法思路:

    1. 判断是否为空二叉树
    2. 访问根结点,这里为输出
    3. 用同样的方法访问左子树中的结点
    4. 用同样的方法访问右子树中的结点
  • 算法实现(递归):

    //二叉树的先序遍历
    Status PreOrderTraverse(BiTree T){
        if(T==NULL)return ERROR;
        else{
            printf("%d",T->data);   // 输出根结点
            PreOrderTraverse(T->lchild); //  递归遍历左子树
            PreOrderTraverse(T->rchild); //  递归遍历右子树
        }
    }
    
  • 算法图解:
    在这里插入图片描述

2.中序遍历LDR:

在这里插入图片描述

  • 递归方法:
    • 算法思路:

      1. 判断是否为空二叉树
      2. 用同样的方法访问左子树中的结点
      3. 访问根结点,这里为输出
      4. 用同样的方法访问右子树中的结点
    • 算法实现:

      //二叉树的中序遍历
      Status InOrderTraverse(BiTree T){
          if(T==NULL)return ERROR;     //  判断是否为空二叉树树
          else{
              InOrderTraverse(T->lchild); //  递归遍历左子树
              printf("%d\n",T->data);   // 输出根结点
              InOrderTraverse(T->rchild); //  递归遍历右子树
          }
      }
      
  • 非递归方法:
    • 算法思路:
      1. 建立一个栈,将根结点(要访问的结点)入栈
      2. 根结点进栈,遍历左子树
      3. 根结点出栈,输出根结点,遍历右子树
      • 算法实现:
//非递归法

//顺序栈的表示
#define MAXSIZE 100
typedef struct {
    BiNode **base;   // 栈底指针
    BiNode **top;    // 栈顶指针,指向栈顶元素上一个元素
    int stacksize;  // 栈可用最大容量
}SqStack;

// 顺序栈的初始化(即构造一个空栈)
Status InitStack(SqStack &S){
    S.base= reinterpret_cast<BiNode **>((int *) malloc(MAXSIZE * sizeof(int)));  // 为栈底指针开辟地址(malloc不再赘述)
    if(!S.base)exit(OVERFLOW);  // 如果base地址为0则表示没有分配成功
    S.top=S.base;   //栈顶指针等于栈底指针则为空栈
    S.stacksize=MAXSIZE; //栈的最大容量
    return OK;
}
//  顺序栈的入栈
Status Push(SqStack &S, BiNode *e){
    if(S.top-S.base==S.stacksize)
        return ERROR;   //判读栈满
    *S.top=e;   //将元素e填入栈此时的顶部
    S.top++;    //让指针加1表示指向下一空间
    return OK;
}
// 顺序栈的出栈
Status Pop(SqStack &S, BiNode &e){
    if(S.top==S.base) return ERROR; //判断是否为空栈
    S.top--;    //令top指向栈顶元素
    e=**S.top;
    return OK;
}
Status InOrderTraverseF(BiTree T){
    SqStack S;  //定义栈
    InitStack(S);   //初始化一个栈用于放被访问结点
    BiTree p=T;     //指针p用于指向根结点
    BiNode q;   //用于存放出栈结点
    while (p||!S.base){ //只要二叉树不为空或者栈不为空都执行循环
        if(p){  //如果p不为空二叉树则将根结点入栈,p指向左子树。
            Push(S,p);
            p=p->lchild;
        }
        else{
            Pop(S,q);
            printf("%d",q.data);//输出访问的根结点
            p=q.rchild;
        }
    }
    return OK;
}


3.后序遍历LRD:

在这里插入图片描述

  • 算法思路:

    1. 判断是否为空二叉树
    2. 用同样的方法访问左子树中的结点
    3. 用同样的方法访问右子树中的结点
    4. 访问根结点,这里为输出
  • 算法实现(递归):

    //二叉树的后序遍历
    Status PostOrderTraverse(BiTree T){
        if(T==NULL)return ERROR;     //  判断是否为空二叉树树
        else{
            PostOrderTraverse(T->lchild); //  递归遍历左子树
            PostOrderTraverse(T->rchild); //  递归遍历右子树
            printf("%d\n",T->data);   // 输出根结点
        }
    }
    
三种遍历算法分析:

在这里插入图片描述

  • 时间复杂度: O(n)
  • 空间复杂度: O(n)
联系:
  • 若二叉树中各结点的值均不同,则二叉树的先序遍历,中序遍历,后序遍历都是唯一的。

  • 由二叉树的先序遍历序列和中序遍历序列可以确定唯一的二叉树。

  • 由二叉树的中序遍历序列和后续遍历序列也可以确定唯一的二叉树。

  • 但先序遍历序列和后续遍历序列不能确定唯一的二叉树

  • 解题思路:在前序或后序中找到根结点,在中序中根据根结点划分左右子树。

层次遍历:

在这里插入图片描述

  • 算法思路:
    1. 创建一个队列
    2. 将根结点入队
    3. 判断队是否为空若不空进行循环
      1. 将队列的队首结点*p出队
      2. 若p有左子树,将左子树结点入队
      3. 若p有右子树,将右子树结点入队
    //二叉树的层次遍历
    // 队列类型:
    #define MAXQSIZE 100    //队列的最大长度
    typedef struct {
        BiNode *base;
        int front,rear;
    }SqQueue;
    // 循环顺序队初始化(即构造一个空队列)
    Status InitQueue(SqQueue &Q){
        Q.base=(BiNode*)malloc(MAXQSIZE*sizeof(BiNode));  // 为队列开辟地址(malloc不再赘述)
        if(!Q.base)exit(OVERFLOW);  // 如果base地址为0则表示没有分配成功
        Q.front=Q.rear=0;   //头指针等于尾指针都则为空栈
        return OK;
    }
    // 循环顺序队入队
    Status EnQueue(SqQueue &Q, BiNode e){
        if((Q.rear-1)%MAXQSIZE==Q.front)    // 判断队满
            return ERROR;
        Q.base[Q.rear]=e;   //在队尾插入数
        Q.rear=(Q.rear+1)%MAXQSIZE; //队尾指针+1指向下一元素
        return OK;
    }
    // 循环顺序队出队
    Status DeQueue(SqQueue &Q, BiNode e){
        if(Q.rear==Q.front)     // 判断队空
            return ERROR;
        e=Q.base[Q.front];  //获取头指针所指元素,用e返回其指
        Q.front=(Q.front+1)%MAXQSIZE;  //队尾指针+1指向下一元素
        return OK;
    }
    //遍历算法:
    void LevelOrder(BiTree T){
        SqQueue Q;
        InitQueue(Q);//初始化队列
        EnQueue(Q,*T);
        BiNode *p; //指向结点的指针p
        while(Q.front!=Q.rear){     // 判断队是否为空若不空进行循环
            DeQueue(Q,*p);
            printf("%d",p->data);
            if(p->lchild!=NULL){        //若p有左子树,将左子树结点入队
                EnQueue(Q,*p->lchild);
            }
            if(p->rchild!=NULL){        //若p有右子树,将右子树结点入队
                EnQueue(Q,*p->rchild);
            }
        }
    }
    

遍历二叉树的应用

1. 二叉树的建立(先序遍历法)

  • 算法思路:
    1. 输入键盘的数
    2. 判断输入的数是否为空字符标识#,
      1. 若为#,则为空。
      2. 若不为#则赋值:
        1. 在内存中开辟新结点,并判断是否开辟成功,用指针T指向它
        2. 给根结点T的data域赋值
        3. 利用递归方法建立左子树和右子树
  • 算法实现:
    // 二叉树的建立(先序遍历法)
    Status CreateBiTree(BiTree &T){
        char a;
        scanf("%c",&a);
        if(a=='#'){
            T=NULL;
        }
        else{
            T=(BiNode*) malloc(sizeof(BiNode));//在内存中开辟新结点,并判断是否开辟成功,用指针T指向它
            if(!T) return ERROR;
            T->data=a;
            CreateBiTree(T->lchild);
            CreateBiTree(T->rchild);
        }
        return OK;
    }
    

2. 复制二叉树(先序遍历法)

  • 算法思路:
    1. 判断是否为空树
      1. 若为空则return 0;结束
      2. 若不为空则复制该树的值:,
        1. 在内存中开辟新结点,并判断是否开辟成功,用指针NEWT指向它
        2. 复制根结点T的data给NEWT的根结点的data域
        3. 利用递归方法复制左子树和右子树
  • 算法实现:
    //复制二叉树
    int Copy(BiTree T,BiTree &NEWT){
        if(T==NULL) return 0;
        else{
            NEWT=(BiNode*) malloc(sizeof (BiNode));
            NEWT->data=T->data;
            Copy(T->lchild,NEWT->lchild);
            Copy(T->rchild,NEWT->rchild);
        }
    }
    

3. 计算二叉树的深度(先序遍历法)

  • 算法思路:

    1. 判断是否为空树
      1. 若为空则深度为0,return 0;
      2. 若不为空:
        1. 则递归计算它的左子树和右子树
        2. 比较左右子树的深度,将大的值+1则为整棵树的深度。
  • 算法实现:

    //计算二叉树的深度
    int Depth(BiTree T){
        if(T==NULL)return 0;
        else{
            int m,n;
            m=Depth(T->lchild);
            n=Depth(T->rchild);
            if(m>n) return m+1;
            else return n+1;
        }
    
    }
    

4. 计算二叉树的结点总数(先序遍历法)

  • 算法思路:

    1. 判断是否为空树
      1. 若为空则结点数为0,return 0;
      2. 若不为空:
        1. 则递归计算它的左子树和右子树的结点数
        2. 总结点数=左子树结点数+右子树的结点数+1
  • 算法实现:

    //计算二叉树的结点总数
    int NodeCount(BiTree T){
        if(T==NULL) return 0;
        else{
            int m,n;
            m=NodeCount(T->lchild);
            n=NodeCount(T->rchild);
            return m+n+1;
        }
    }
    

5. 计算二叉树的叶子结点数(先序遍历法)

  • 算法思路:

    1. 判断是否为空树
      1. 若为空则叶子结点数为0,return 0;
      2. 若不为空:
        1. 判断是否为叶子结点,是则返回1
        2. 不是叶子结点则:
          1. 则递归计算它的左子树和右子树的叶子结点数
          2. 总叶子结点数=左子树的叶子结点数+右子树的叶子结点数
  • 算法实现:

    //计算二叉树的叶子结点数(先序遍历法)
    int LeadCount(BiTree T){
        if(T==NULL)return 0;
        else{
            if(T->lchild==NULL&&T->rchild==NULL) return 1;
            else {
                int m,n;
                m= LeadCount(T->lchild);
                n= LeadCount(T->rchild);
                return m+n;
            }
        }
    }
    

线索二叉树

线索二叉树的引入:

遍历二叉树的其实就是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序序列、中序序列或后序序列。这些线性序列中的每一个元素都有且仅有一个前驱结点和后继结点。

但是当我们希望得到二叉树中某一个结点的前驱或者后继结点时,普通的二叉树是无法直接得到的,只能通过遍历一次二叉树得到。每当涉及到求解前驱或者后继就需要将二叉树遍历一次,非常浪费时间,不方便。
如果再增设前驱,后驱指针域,则增加了存储负担。
于是是否能够利用二叉链表中的空指针域,将结点的前驱和后继的信息存储进来。

相关概念

  • 线索: 如果某个结点的左孩子为空,则将空的左孩子指针域改为前驱地址,如果某个结点的右孩子为空,则将空的右孩子的指针域改为后继地址。
  • 线索二叉树: 加上线索的二叉树为线索二叉树
  • 区分指针域指向的目标: 二叉链表增设两个标志域ltag和rtag(如图)
    在这里插入图片描述

图解线索二叉树

1. 先序线索二叉树

在这里插入图片描述

2. 中序线索二叉树

在这里插入图片描述

3. 后序线索二叉树

在这里插入图片描述

注意: 为了避免指针悬空状态,增设头结点
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿明同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值