• 【数据结构复习】第五章树和二叉树


    树和二叉树

    一、树

    (一)树的逻辑结构

    1.树的定义
    是n个结点n(>=0)的有限集合,n=0称为空树,任何非空树满足:
    (1)有且只有一个根结点
    (2)n>1时,除根结点外的其他m个有限集合(m>0) T1 T2……Tm,每个集合是一棵树称为这个根结点的子树
    在这里插入图片描述
    图5-3(a)有9个结点T = {A,B,C,D,E,F,G,H,I} A为根结点
    T1 = {B(根),D,E,F,I} T2 = {C(根),G,H} T1、T2为根结点的两个子树

    注:图5-3(b)©两树间存在交集,所以不是树。

    2.基本术语

    (1)结点的度、树的度
    结点所拥有的子树的个数为该结点的度,树中各结点度的最大值为该树的度
    例5-3(a):A的度为2,B的度为3,树的度为3。

    (2)叶子结点、分支结点
    度为0的为叶子结点(终端结点),度不为0为分支结点(非终端结点)。
    例5-3(a):D,I,F,G,H是叶子结点,其余都是分支结点。

    (3)孩子结点、双亲结点、兄弟结点
    某结点子树的根结点为孩子结点,反之该结点为双亲结点,同一个双亲的孩子结点为兄弟结点
    例5-3(a):B为A的孩子,A为B的双亲,B、C互为兄弟结点。

    (4)路径、路径长度
    路径就是上下相连的通路,路径上经过的边数叫路径长度
    例5-3(a):结点A到I的路径为ABEI

    (5)祖先、子孙
    结点x与结点y之间有一条路径,则x为y的祖先,y为x的子孙。

    (6)结点的层数、树的深度(高度)、树的宽度
    规定根结点层数为1,树中所有结点的最大层数叫树的深度(高度),树中每层结点个数最大的叫树的宽度
    5-3(a):结点D的层数为3,树的深度为4,树的宽度为5。

    3.树的遍历
    前序遍历:(1)先访问根结点 (2)再从左->右访问
    后序遍历:(1)先从左->右访问 (2)再访问根结点
    层序遍历:按层次序列依次访问
    例5-3(a)
    在这里插入图片描述
    前序遍历: ABDEIFCGH
    后序遍历: DIEFBGHCA
    层序遍历: ABCDEFGHI

    (二)树的存储结构

    1.双亲表示法
    除根结点外每个结点都有且只有一个双亲结点,因此采用一维数组存储各个结点(层序存储)。
    数组元素包括:树结点的数据信息双亲在数组中的下标
    在这里插入图片描述其中parent域值为-1,表示此结点无双亲,即该结点是根结点。
    数组元素结构体定义

    template <typename DataType>
    struct PNode
    {
        DataType data;
        int parent;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.孩子表示法
    (1)是基于链表的存储法,把每个结点的孩子排列起来,看成一个单链表存储的线性表,称为该结点的孩子链表。
    (2)n个结点n个孩子链表,n个孩子链表n个头指针,头指针构成一个线性表。
    在这里插入图片描述

    struct CTNode           //孩子结点
    {
        int child;
        CTNode*next;
    };
    template <typename DataType>
    struct CBNode           //表头结点
    {
        DataType data;
        CTNode*firstChild;  //在表头结点定义,指向孩子链表的头指针
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    孩子表示法不仅表示了孩子结点的信息,同一孩子链表中的结点还有兄弟关系
    孩子表示法存储示意图(会画):
    在这里插入图片描述

    3.孩子兄弟表示法
    (1)链表中每个结点除数据域外,设置了两个指针分别指向该结点的第一个孩子结点和右兄弟
    (2)data存储该结点信息,firstChild存储该结点的孩子结点的存储地址,rightSib存储该结点的右兄弟的存储地址。
    在这里插入图片描述

    template <typename DataType>
    struct TNode
    {
        DataType data;
        TNode<DataType>*firstChild,*rightSib;		//firstChild存储该结点的第一个孩子结点的存储地址
    											    //rightSib存储存储该结点的右兄弟的存储地址
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    (三)二叉树的逻辑结构

    1.定义:n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两颗互不相交的分别称为左子树右子树的二叉树构成。

    2.二叉树的特点
    (1)每个结点最多有两棵子树,所以二叉树不存在度>2的结点
    (2)二叉树的左右子树不能任意颠倒,若结点只有一棵子树,需指明是左子树还是右子树。

    3.几种特殊的二叉树

    (1)斜树
    左斜树:所有结点都只有左子树的二叉树
    右斜树:所有结点都只有右子树的二叉树
    在这里插入图片描述

    (2)满二叉树
    一棵二叉树中,所有分支结点都存在左右子树,且所有叶子结点在同一层,这样的二叉树叫满二叉树。
    在这里插入图片描述
    特点:叶子结点只出现在最下一层;只有度为0和度为2的结点。

    (3)完全二叉树
    如果编号为i(1<=i<=n)的结点与同样深度满二叉树编号为i的结点在二叉树中位置完全相同,则该树称为完全二叉树。
    在这里插入图片描述
    特点
    a。深度为k的完全二叉树在k-1是满二叉树 。
    b。叶子结点只能出现在最下两层,且最下层叶子结点都集中在左侧连续位置。
    c。若有度为1的点,只可能是该结点的左孩子(若是右孩子则编号不一致了)。

    4.二叉树的基本性质(重点记忆)

    前3条描述对象为二叉树,后2条描述对象为完全二叉树。

    (1)在一棵二叉树中,若叶子结点的个数为n0,度为2的结点个数为n2,则n0 = n2+1。

    (2)二叉树的第i层上最多有2^(i-1)个结点(i>=1)

    (3)在一棵深度为k的二叉树中,最多有2^k-1个结点。

    (4)具有n个结点的完全二叉树的深度为log2n+1(不超过n的整数)

    (5)若对一棵有n个结点的完全二叉树从1开始按层序编号,对于编号为i的(1<=i<=n)的结点(简称为结点i),有以下关系:

    a。如果i>1,则结点i的双亲编号为[i/2] (取整),否则结点i是根结点无双亲。
    b。如果2i<=n,则结点i的左孩子的编号为2i,否则结点i无左孩子。
    c。如果2i+1<=n, 则结点i的右孩子的编号为2i+1,否则结点i无右孩子。

    5.二叉树的遍历操作
    分为:
    (1)前序遍历:a。先根 b。前序遍历根的左子树 c。前序遍历根的右子树 (根左右)
    (2)中序遍历:a。中序遍历根的左子树 b。访问根 c。中序遍历根的右子树 (左根右)
    (3)后序遍历:a。后序遍历根的左子树 b。后序遍历根的右子树 c。访问根结点 (左右根)
    (4)层序遍历: 按层次依次访问结点
    例:
    在这里插入图片描述
    前序遍历:ABDGCEF
    中序遍历:DGBAECF
    后序遍历:GDBEFCA

    前序遍历+中序遍历——>确定一个二叉树
    后序遍历+中序遍历——>确定一个二叉树

    (二)叉树的存储结构

    1.顺序存储结构
    用一维数组存储二叉树结点,用结点的存储位置表示结点之间的逻辑关系。

    一般二叉树按照层序编号,再用一维数组顺序存储:
    (1)将二叉树按照完全二叉树编号,根结点编号为1,若某结点i有左孩子,则左孩子的编号为2i,若某结点i有右孩子,则右孩子编号为2i+1。
    (2)将二叉树结点按照编号顺序存储到一维数组中。
    在这里插入图片描述

    注:此方法浪费存储空间,一般仅适合存储完全二叉树。

    2.二叉链表
    二叉树一般用二叉链表存储,基本思想:
    令二叉树的每个结点对应一个链表结点,链表结点除了存放二叉树信息外还要存放指示左右孩子的指针
    在这里插入图片描述
    data存放该结点的数据信息,lchild存放指向左孩子的指针,rchild存放指向右孩子的指针
    在这里插入图片描述
    定义

    template <typename DataType>
    struct BiNode
    {
        DataType data;
        BiNode<DataType>*lchild,*rchild;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.二叉链表的实现
    二叉链表的类定义
    为了避免类的调用者访问BiTree类的私有变量root,在构造函数、析构函数以及遍历函数中调用了相应的私有函数,例如在PreOrder()中调用了私有函数PreOrder(root)。

    //root为二叉链表的根指针
    template <typename DataType>
    class Bitree
    {
    public:
        BiTree(){root = Creat();}               //构造函数,建立一棵二叉树
        ~BiTree(){Release(root);}               //析构,释放各节点存储空间
        void PreOrder(){PreOrder(root);}        //前序遍历二叉树
        void InOrder(){InOrder(root);}          //中序遍历二叉树
        void PostOrder(){PostOrder(root);}      //后序遍历二叉树
        void LevelOrder();                      //层序遍历
    private:
        BiNode<DataType>*Creat();               //构造函数调用
        void Release(BiNode<DataType>*bt);      //析构函数调用
        void PreOrder(BiNode<DataType>*bt);     //前序遍历函数调用
        void InOrder(BiNode<DataType>*bt);      //中序遍历函数调用
        void PostOrder(BiNode<DataType>*bt);    //后序遍历函数调用
        BiNode<DataType>*root;                  //指向根结点的头指针
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    (1)前序遍历操作(根左右)
    在这里插入图片描述

    template <typename DataType>
    void BiTree<DataType>::PreOrder(BiNode<DataType>*bt)
    {
        if (bt==nullptr) return;            //递归调用结束条件,遍历到空结束
        else{
            cout <<bt->data<<"\t";           //访问根结点bt的数据域
            PreOrder(bt->lchild);            //前序递归遍历bt的左子树
            PreOrder(bt->rchild);            //前序递归遍历bt的右子树
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    前序遍历序列:ABDC
    在这里插入图片描述

    (2)中序遍历操作 (左根右)
    访问结点的操作发生在左子树遍历完成尚未遍历右子树,所以只需将输出操作cout <data放到递归遍历左子树之后。
    中序遍历序列:BDAC

    template <typename DataType>
    void BiTree<DataType>::PreOrder(BiNode<DataType>*bt)
    {
        if (bt==nullptr) return;            //递归调用结束条件,遍历到空结束
        else{
            PreOrder(bt->lchild);            //前序递归遍历bt的左子树
            cout <<bt->data<<"\t";           //访问根结点bt的数据域
            PreOrder(bt->rchild);            //前序递归遍历bt的右子树
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (3)后序遍历操作 (左右根)
    访问结点操做发生在该结点的左子树右子树均遍历完毕,所以只需将输出操作cout <data放到递归遍历右子树之后。
    后序遍历序列:DBCA

    template <typename DataType>
    void BiTree<DataType>::PreOrder(BiNode<DataType>*bt)
    {
        if (bt==nullptr) return;            //递归调用结束条件,遍历到空结束
        else{
            PreOrder(bt->lchild);            //前序递归遍历bt的左子树
            PreOrder(bt->rchild);            //前序递归遍历bt的右子树
            cout <<bt->data<<"\t";           //访问根结点bt的数据域
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (4)层序遍历
    访问一层结点,再对各个结点的左孩子右孩子顺序访问,先访问的结点其左右孩子也要先访问。
    在层序遍历时,设置一个队列存放已访问结点。
    层序遍历序列:ABCD

    基本思想:
    1.队列Q初始化
    2.二叉树非空,根指针入队
    3.循环直到队列Q为空
    (1)q = 队列Q的队头元素出队
    (2)访问结点q的数据域
    (3)若结点存在左孩子,则将左孩子指针入队
    (4)若结点存在右孩子,则将右孩子指针入队
    在这里插入图片描述

    设:队列采用顺序队列,且不会发生假溢出,层序遍历队列变化如下
    在这里插入图片描述

    template <typename DataType>
    void BiTree<DataType>::LevelOrder()
    {
        BiNode<DataType>*Q[100],*q = nullptr;
        int front = -1,rear = -1;               //队列初始化
        if (root==nullptr) return;              //二叉树为空算法结束
        Q[++rear] = root;                        //根指针入队
        while (front!=rear)                     //当队列非空时
        {
            q = Q[++front];                     //出队
            cout <<q->data<<"\t";
            if (q->lchild!=nullptr) Q[++rear] = q->lchild;
            if (q->rchild!=nullptr) Q[++rear] = q->rchild;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    (5)构造函数——建立二叉树
    建立二叉树的一种方法是根据一个结点序列来建立二叉树,由于前序、中序、后序都不能唯一确定一棵二叉树,因此不可直接使用

    处理方法:将二叉树中,每个结点的空指针指向一个虚结点,其值为特定值如(#)以标识为空,处理后的树称为原二叉树的扩展二叉树
    在这里插入图片描述
    扩展二叉树的一个遍历序列就能唯一的确定一棵二叉树。

    扩展二叉树的前序遍历序列由键盘输入,root为指向根结点的指针。
    二叉链表建立过程:
    首先输入根结点,若输入#字符,则表明二叉树为空树,即root=nullptr;否则输入的字符应该赋值给
    bt->data之后依次建立在他的左子树和右子树。

    template <typename DataType>
    BiNode<DataType>*BiTree<DataType>::Creat()
    {
        BiNode<DataType>*bt;
        char ch;
        cin>>ch;                    //输入结点的数据信息,假设为字符
        if (ch=='#') bt=nullptr;    //建立一棵空树
        else{
            bt = new BiNode<DataType>; bt->data=ch;
            bt->lchild=Creat();      //递归建立左子树
            bt->rchild=Creat();      //递归建立右子树
        }
        return bt;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (6)析构函数——销毁二叉树
    二叉链表是动态存储分配,在二叉链表变量退出作用域前,要释放二叉链表存储空间。可以对二叉链表进行后序遍历,在访问结点时进行释放处理。

    template <typename DataType>
    void BiTree<DataType>::Release(BiNode<DataType>*bt)
    {
        if (bt==nullptr) return;
        else{
            Release(bt->lchild);        //释放左子树
            Release(bt->rchlid);        //释放右子树
            delete bt;                  //释放根结点
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4.三叉链表
    在而二叉链表存储方式下,从某结点出发可以直接访问到他的孩子结点,但要找到双亲结点,需要从根结点开始搜索,最坏情况下需要遍历整个二叉链表。
    此时应采用三叉链表存储二叉树
    在这里插入图片描述
    data数据域,lchild存放指向左孩子的指针,rchild存放指向右孩子的指针,parent域为指向该结点的双亲结点的指针。

    template <typename DataType>
    struct TriNode
    {
        DataType data;
        TriNode<DataType>*lchild,*rchild,*parent;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注:三叉链表便于查找双亲结点,但是相对于二叉链表而言,增加了空间开销
    在这里插入图片描述

    (三)森林

    1.森林的逻辑解构
    森林是m(m>=0)棵互不相交的的集合,森林由树构成,而不是二叉树。
    在这里插入图片描述
    任何一棵树去掉根结点就是森林。

    2.遍历森林
    两种方法:前序遍历、后序遍历
    图中前序遍历:ABCDEFGHIJ
    图中后序遍历:BADEFCHJIG

    3.树转换成二叉树
    (1)加线——树中所有相邻兄弟结点之间加一条连线
    (2)去线——对树中的每个结点,只保留它与第一个孩子结点之间的连线,删除它与其他孩子结点之间的连线。
    (3)层次调整——按照二叉树结点之间关系进行层次调整
    a. 树的第一个孩子为左子树
    b.第一个孩子的兄弟为右子树

    注:转换后的二叉树根结点的右子树必为空。
    在这里插入图片描述

    4.森林转化成二叉树
    在这里插入图片描述
    (1)将森林中的每棵树转换为二叉树
    (2)将每棵树的根结点视为兄弟结点,在所有根结点之间加上连线
    (3)将二叉树结点关系进行层次调整。

    5.二叉树转化为树、森林
    在这里插入图片描述
    树和森林都可以转为二叉树,不同:树转换成的二叉树,其根结点无右子树。森林转换成的二叉树,其根结点有右子树。

    (1)加线——若某结点x是双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子…都与结点y连线
    (2)去线——删去原二叉树中所有的双亲结点与孩子结点的连线。
    (3)调整层次

    (四)最优二叉树(哈夫曼树)

    1.带权路径长度

    (1)叶子结点的权值是对叶子结点赋予有意义的数量值。
    (2)从根结点到各个叶子结点的路径长度与相应叶子结点权值乘积之和称为带权路径长度
    在这里插入图片描述
    wk为第k个叶子结点的权值,lk为从根结点到第k个叶子结点的路径长度。

    2.最优二叉树(哈夫曼树)
    给出一组权值对应的叶子结点,可构造出形状不同的多棵二叉树,其中带权路径长度最小的二叉树叫最优二叉树

    图中©的权值最小所以©是最优二叉树
    在这里插入图片描述

    注:一棵二叉树要使其带权路径长度最小,需使权值越大的叶子结点靠近根结点。 哈夫曼树不存在度为1的点

    例:给定集合W={2,3,4,5}构造哈夫曼树过程。
    在这里插入图片描述

    3.HuffmanTree基本思想(伪代码描述):
    输入:n个权值W = {w1,w2……wn}
    输出:哈夫曼树
    1.初始化:由{w1,w2……wn}构造n个只有根结点的二叉树,得到二叉树集合F = {T1,T2……Tn}。
    2.重复以下操作,直至F中只剩下一棵二叉树
    (1)选取合并:选取F中根结点权值最小的两棵二叉树作为左右子树,构造一棵新的二叉树,新根结点为左右子树权值的和。
    (2)删除加入:删除F中作为左右子树的两棵二叉树,并将新建立的二叉树加入F中。

    有n个叶子结点的哈夫曼树中有n-1个分支结点,在n-1次合并中生成,哈夫曼树共有2*n-1个结点。设置数组huffTree[2n-1]保存哈夫曼树各结点的信息。

    哈夫曼树的结点结构定义
    在这里插入图片描述

    struct ElemType
    {
        int weight;                 //确定权值为整数
        int parent,lchild,rchild;   //游标
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    parent = -1表示没有双亲结点(-1就是表示没有)。parent表示该结点的双亲结点在数组的下标
    在这里插入图片描述

    构造过程:
    输入:n个权值w[n]
    输出:哈夫曼树huffTree[2n-1]
    1.数组huffTree初始化,所有数组元素的双亲、左右孩子都都置为-1。
    2.数组huffTree的前n个元素的权值置给定权值。
    3.循环变量k从n~n-2进行n-1次合并
    (1)选取两个权值最小的根结点,其下标分别为i1,i2
    (2)将二叉树i1,i2合并为新的二叉树k

    哈夫曼算法

    //w[n]保存n个叶子结点的权值
    //Select函数用来选取huffTree中两个权值最小的根结点并返回下标i1,i2
    
    void HuffmanTree(ElemType huffTree[],int w[],int n)
    {
        int i,k,i1,i2;
        for (i = 0;i<2*n-1;i++)         //所有结点均没有双亲和孩子
        {
            huffTree[i].parent = -1;
            huffTree[i].lchild = huffTree[i].rchild = -1;
        }
        for (i = 0;i<n;i++)             //存储叶子结点的权值
            huffTree[i].weight = w[i];
        for (k = n;k<2*n-1;k++)         //n-1次合并
        {
            Select(huffTree,i1,i2);9     //权值最小的根结点下标为i1和i2(找parent为-1的结点)
            huffTree[k].weight = huffTree[i1].weight + huffTree[i2].weight;
            huffTree[i1].parent = k; huffTree[i2].parent = k;
            huffTree[k].lchild = i1; huffTree[k].rchild = i2;
        }    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4.哈夫曼编码
    左分支代表0,右分支代表1

    例:对于{A,B,C,D,E}5个字符使用频率为{35,25,15,15,10}
    在这里插入图片描述
    使用频率高编码短,使用频率低编码长

  • 相关阅读:
    数据库系统原理与应用教程(067)—— MySQL 练习题:操作题 82-89(十一):数据的增、删、改操作
    【项目管理】Java使用pdfbox调用打印机打印PDF文件
    docker-compose 启动多个容器 为容器设置自定义网络 互相ping
    C++中的类型转换
    代码随想录算法训练营第二十八天 | LeetCode 491. 递增子序列、46. 全排列、47. 全排列 II
    Linux Socket网络编程UDP、TCP 阻塞与非阻塞 断线重连机制
    客户画像分析无头绪?来试下风险评分与特征的方案与实现
    【DLoopDetector(C++)】DBow2词袋模型&&loop close学习
    Spring Security 自定义授权服务器实践
    传输层——再谈端口号
  • 原文地址:https://blog.csdn.net/qq_45748133/article/details/125857732