树(tree)

本文详细介绍了树的基本术语,包括结点、度、叶子结点、非终端结点等,并探讨了树的层次、高度、深度等概念。还深入讲解了二叉树的性质、存储结构、遍历算法以及赫夫曼树和赫夫曼编码的相关知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本术语:

结点:不仅包含数据元素,而且包含指向子树的分支。

结点的度:结点拥有子树个数或者分支的个数。

树的度:树中各结点的度的最大值。

叶子结点(终端结点):指度为0的结点。

非终端结点(分支结点):指度不为0的结点。

孩子:结点的子树的根。

双亲:与孩子结点定义对应。

兄弟:同一个双亲的孩子之间互为兄弟。

祖先:从根到某结点的路径上的所有结点,都是这个结点的祖先。

子孙:以某结点为根的子树中的所有结点,都是这个结点的子孙。

层次:从根开始,根为第一层,根的孩子为第二层,根的孩子的孩子为第三层,以此类推。

树的高度(或者深度):树中结点的最大层次。

结点的深度和高度:

    1.结点的深度是从根结点到该结点路径上的结点个数。

    2.从某结点往下走可能到达多个叶子结点,其中最长的那条路径的长度即为该结点在树中的高度。

    3.根结点的高度为树的高度。

堂兄弟(sibling):双亲在同一层的结点互为堂兄弟。

有序树:树中结点的子树从左到右是有次序的,不能交换,这样的树叫做有序树。

无序树:树中结点的子树没有顺序,可以任意交换,这样的数叫做无序树。

丰满树:即理想平衡树,要求除最底层外,其他层都是满的。

森林:有若干棵互不相交的树的集合。

树的顺序存储结构中最简单直观的是双亲存储结构,用一维数组即可实现。

数组下标表示树中的结点,数组元素的内容表示该结点的双亲结点,这样就有了结点(下标)以及结点之间的关系(内容),也就可以表示一棵树了。

树的链式存储结构最常用的有一下两种:

    1.孩子存储结构

    2.孩子兄弟存储结构

 

二叉树

1)每个结点最多只有两棵子树

2)子树有左右顺序之分

满二叉树:所有的分支结点都有左孩子和右孩子结点,并且叶子结点都集中在二叉树的最下一层

完全二叉树:如果对一棵深度为k、有n个结点的二叉树进行编号后,各结点的编号与深度为k的满二叉树中相同位置上的结点的编号均相同,那么这个二叉树就是一棵完全二叉树。

(通俗的说,一棵完全二叉树一定是由一棵满二叉树从右至左从上至下,挨个删除结点所得到的)

二叉树的重要性质

性质1:非空二叉树上叶子结点树等于双分支结点数加1.(推而广之:在一棵度为m的树中也有相似推论)

子结点数为 n_{0}, 单分支结点数为 n_{1},双分支结点数 n_{2}

则在一棵二叉树中,总结点数为 n_{0}+n_{1}+n_{2} ;所有结点的分支数等于单分支结点数加上双分支结点数的两倍,即总分支数为 n_{1}+2n_{2}

由于二叉树中除根结点外,每个结点都有唯一的一个分支指向它,因此二叉树中  总分支数=总结点数-1  (这条结论对任何树都适用)

由上述得: n_{0}+n_{1}+n_{2}-1=n_{1}+2n_{2}          化简得   n_{0}=n_{2}+1 

性质2:二叉树的第 i 层上最多有  2^{i-1} (i≥1)个结点。

性质3:高度(或深度)为 k 的二叉树最多有 2^{k}-1 (k≥1)个结点。

性质4:有n个结点的完全二叉树,对各结点从上到下、从左到右依次编号(编号范围为1~n),则结点 之间有以下关系:

如果i≠1,则a的双亲结点为 \left \lfloor i/2 \right \rfloor

如果 2i≤n,则a的左孩子的编号为2i;如果2i>n,则a无左孩子。

如果2i+1≤n,则a的右孩子的编号为2i+1;如果2i+1>n,则a无右孩子。

性质5:函数Catalan():给定n个结点,能构成h(n)种不同的二叉树, h(n)=\frac{C_{2n}^{n}}{n+1}

性质6:具有n(n≥1)个结点的完全二叉树的高度(或深度)为  \left \lfloor log_{2}n \right \rfloor+1

二叉树的存储结构

1.顺序存储结构:用一个数组来存储一棵二叉树(最适合存储完全二叉树,用于存储一般二叉树会浪费大量的存储空间)

2.链式存储结构:含有一个数据域和两个指针域的链式结点

typedef struct BTnode
{
    char data;
    strcut BTNode *lchild;
    strcut BTNode *rchild;
}BTnode;

二叉树的遍历算法

//先序遍历
void preorder(BTNode *p)
{
    if(p*=NULL)
    {
        Visit(p);  //假设访问函数Visit()已经定义,
                   //其中包含对结点p的各种访问操作
        preorder(p->lchild);
        preorder(p->rchild);
    }
}

//中序遍历
void inorder(BTNode *p)
{
    if(p*=NULL)
    {
        inorder(p->lchild);

        Visit(p);  //假设访问函数Visit()已经定义,
                   //其中包含对结点p的各种访问操作

        preorder(p->rchild);
    }
}

//后序遍历
void postorder(BTNode *p)
{
    if(p*=NULL)
    {
        postorder(p->lchild);
        postorder(p->rchild);


        Visit(p);  //假设访问函数Visit()已经定义,
                   //其中包含对结点p的各种访问操作
    }
}

根据二叉树的前、中、后三种遍历序列中的前和中、中和后两对遍历序列都可以唯一确定这棵二叉树,而 根据前和后这对遍历序列则不能 确定这棵二叉树。

层次遍历

需要建立一个循环队列,先将二叉树的头结点入队列,然后出队列,访问该结点,如果它有左子树,则将左子树的根结点入队;如果它有右子树则将右子树的根结点入队。然后出队,对出队结点访问,如此反复,知道队列为空为止。

void level(BTNode *p)
{
    int front,rear;
    BTNode *que[maxSize];  //定义一个循环队列,用来记录将要访问的层次上的结点
    front=rear=0;  //循环队列初始化
    BTNode *q;  //辅助指针
    if (p!=NULL)
    {
        rear=(rear+1)%maxSize;  //根结点入队
        que[rear]=p;
        while(front!=rear)  //当循环队列不空时进行循环
        {
            front=(front+1)%maxSize;  //队头结点出队
            q=que[front];
            Visit(q);  //访问队头结点
            if(q->lchild!=NULL)  //如果左子树不空,则左子树的根结点入队
            {
                rear=(rear+1)%maxSize;
                que[rear]=q->lchild;
            }
            if(q->rchild!=NULL)  //如果右子树不空,则右子树的根结点入队
            {
                rear=(rear+1)%maxSize;
                que[rear]=q->rchild;
            }
        }
    }
}

求二叉树的宽度

/*以下定义的结构型为顺序肺循环队列的队列元素,可存储结点指针以及结点所在的层次*/
typedef struct
{
    BTNode *p;    //结点指针
    int lno;      //结点所在层次号
}St;

void maxNode(BTNode *b)
{
    St que[maxSize];     //定义顺序非循环队列
    int front,rear;
    int Lno,i,j,n,max;
    front=rear=0;        //将队列置空
    BTNode *q;           //辅助指针
    if (b!=NULL)
    {
        ++rear;  
        que[rear].p=b;
        que[rear].lno=1;
        while(front!=rear)  
        {
            ++front;  
            q=que[front].p;
            Lno=que[front].lno;         //Lno用来存取当前结点的层次号

            if(q->lchild!=NULL)
            {
                ++rear;
                que[rear].p=q->lchild;
                que[rear].lno=Lno+1;    //根据当前结点的层次号推知其孩子结点的层次号
            }
            if(q->rchild!=NULL)
            {
                ++rear;
                que[rear].p=q->rchild;
                que[rear].lno=Lno+1;    //根据当前结点的层次号推知其孩子结点的层次号
            }
        }                               //循环结束的时候,Lno中保存的是这棵二叉树的最大层数
        /*以下为找出含有结点最多的层中的结点数*/
        max=0;
        for(i=1;i<=Lno;++i)        //循环Lno次
        {
            n=0;
            for(j=1;j<=rear;++j)   //遍历数组
            {
                if(que[j].lno==i)
                    ++n;
                if(max<n)
                    max=n;
            }
        }
        return max;
    }
    else return 0;  //空树直接返回0
}

二叉树遍历算法的改进

1.二叉树深度优先遍历算法的非递归实现

先序遍历非递归算法

中序遍历非递归算法

后序遍历非递归算法(逆后序遍历序列不过是先序遍历过程中对左右子树遍历顺序交换所得到的结果)

2.线索二叉树

二叉树非递归遍历算法避免了系统栈的调用,提高了一定的执行效率,而线索二叉树则可将用户栈也省掉,将二叉树的遍历线性化,进一步提高效率。

(二叉树被线索化后近似于一个线性结构,分支结构的遍历操作就转化为了近似于线性结构的遍历操作,通过线索的辅助使得寻找当前结点前驱或者后继的平均效率大大提高)

(对于二叉链表存储结构,n个结点的二叉树有n+1个空链域)

中序线索二叉树的构造

typedef struct TBTNode
{
    char data;
    int ltag,rtag;  //线索标记
    struct TBTNode *lchild;
    struct TBTNode *rchild;
}TBTNode;

对一棵二叉树中所有结点的空指针域按照魔种遍历方式家线索的过程叫做线索化,被线索化了的二叉树叫做线索二叉树。

线索化的规则是,左线索指针指向当前结点在中序遍历序列中的前驱结点,右线索指针指向后继节点。因此需要一个指针p指向当前正在访问的结点,pre指向p的前驱结点。p的左线索如果存在则让其指向pre,pre的右线索如果存在则让其指向p,因为p是pre的后继结点,这样就完成了一对线索的连接。

树转换为二叉树后,树的先序遍历对应二叉树的先序遍历,树的后序遍历对应二叉树的中序遍历。

赫夫曼树和赫夫曼编码

1.路径:指从树中的一个结点到另一个结点的分支所构成的路线

2.路径长度:指路径上的分支数目

3.树的路径长度:从根到每个结点的路径长度之和

4.带权路径长度:结点具有权值,从该结点到根之间的路径长度乘以结点的权值,就是该结点的带权路径长度。

5.树的带权路径长度(WPL):树的 带权路径长度是指树中所有叶子结点的带权路径长度之和。

赫夫曼树的特点:

1.权值越大的结点,距离根结点越近

2.树中没有度为1的结点。这类树又叫做正则(严格)二叉树

3.树的带权路径长度最短

赫夫曼编码过程中,每个字符的权值是在字符串中出现的次数,路径长度即为每个字符编码的长度,出现次数越多的字符编码长度越短,因此就使得整个字符串被编码后的前缀码长度最短

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值