数据结构笔记 | 第六章 | 树

Chapter 6 树

  • 是什么:n个结点的优先级。仅有一个根节点,其他结点可分为m个互不相交的有限集,其中每一个集合本身又是一棵树,并称为子树。(递归定义)
  • 节点分类:按度分,度是结点拥有的子树数。 树的度是树内各结点度的最大值。
    度为0:叶结点或终端结点。 度不为0的结点:非终端节点或分支结点。除根结点外,分支结点也叫内部结点。
  • 节点间关系: 结点的子树的根称为该结点的孩子,该结点称为孩子的双亲。同一双亲的孩子之间互称兄弟。结点的祖先是从根到该结点的所经分支上的所有结点。以某结点为根的子树中的任一结点都称为该结点的子孙
  • 树的其他相关概念: ①结点的层次,根为第一层。②双亲在同一层的结点互称为堂兄弟。③树中结点的最大层次称为树的深度或高度。④有序树:子树左右有顺序。⑤森林:m棵互不相交的树的集合。

树的抽象数据类型

ADT 树(tree)
Data 
    树是由一个根结点和若干棵子树构成。树中结点具有相同数据类型及层次关系。
Operation 
    InitTree(*T): 构造空树T。
    DestroyTree(*T): 销毁树T。
    CreateTree(*T,definition):按definition中给出树的定义来构造树。
    ClearTree(*T): 若树T存在,则将树T清为空树。
    TreeEmpty(T): 若T为空树,返回true,否则返回false。
    TreeDepth(T): 返回T的深度。
    Root(T):返回T的根结点。
    Value(T,cur_e): cur_e是树T中一个结点,返回此结点的值。
    Assign(T,cur_e,value): 给树T的结点cur_e赋值为value。
    Parent(T,cur_e):若cur_e是树T的非根结点,则返回它的双亲,否则返回空。
    LeftChild(T,cur_e): 若cur_e是树T的非叶结点,则返回它的左孩子,否则返回空。
    RightSibling(T,cur_e):若cur_e有有兄弟,则返回它的右兄弟,否则返回空。
    InsertChild(*T,*p,i,c):其中p指向树T的某个结点,i为所指结点p的度加上1。非
                           空树c与T不相交,操作结果为插入c为树T中p指结点的第i棵子树,
    DeleteChild(*T,*p,i): 其中p指向树T的某个结点,i为所指结点p的度,操作结果为
                          删除T中p所指结点的第i棵子树。
endADT 

树的存储结构

  • 简单顺序结构直接反映孩子双亲的逻辑关系,因此充分利用顺序结构和链式结构的特点。
双亲表示法
  • 每个结点不一定有孩子,但一定有且仅有一个双亲。**以一组连续空间存储树的结点,在每个结点中,附设一个指示器指示其双亲结点在数组中的位置。**也就是说,每个结点除了直到自己是谁外,还知道它的双亲在哪里。
    在这里插入图片描述
    data是数据域,存储结点的数据信息。parent是指针域,存储该结点双亲在数组中的下标。根结点没有双亲,约定根结点的parent值为-1。
/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTNode /*结点结构*/
{
    TElemType data;   /*结点数据*/
    int parent;       /*双亲位置*/
}PTNode;
typedef struct
{
    PTNode nodes[MAX_TREE_SIZE]; /*结点数组*/
    int r,n;    /*根的位置和结点数*/
}PTree;

在这里插入图片描述在这里插入图片描述

  • 这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所用时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。但找结点的孩子就要遍历整个结构。
  • 可加一个结点的长子域(最左边孩子的域),这样可以很容易得到结点的孩子。如果结点没有孩子,就设置其为-1。对有2个孩子的结点,知道了长子是谁,另一个就是次子了。
    在这里插入图片描述
  • 体现兄弟关系可以增加一个右兄弟域。若有兄弟不存在则赋值为-1。在这里插入图片描述
孩子表示法
  • 是什么:每个结点设多个指针域,每个指针指向一棵子树的根结点(多重链表表示法
  • 方案一:指针域的个数等于树的度。树中各结点的度相差很大时,浪费空间。
  • 方案二:每个结点的指针域的个数等于该结点的度。但由于各个结点链表不同,加上要维护节点的度的数值,在运算上会带来时间上的损耗。
  • 综上:把每个结点放到一个顺序存储结构的数组中,再对每个结点的孩子建立一个单链表体现它们的关系。如果是叶子结点则此单链表为空。设计两种结点结构:
    ①孩子链表的孩子结点:
    在这里插入图片描述
    child是数据域,存储某个结点再表头数组中的下标。next是指针域,用来存储指向某个结点的下一个孩子结点的指针。
    ②表头数组的表头结点:
    在这里插入图片描述
    data是数据域,存储某结点的数据信息。firsrchild是头指针域,存储该结点的孩子链表的头指针。
/*树的孩子表示法结构定义*/
#define MAX_TREE_SIZE 100
typedef int TElemType
typedef struct CTNode /*孩子结点*/
{
    int child;
    struct  CTNode *next;
}*ChildPtr;

typedef sturct    /*表头结构*/
{
    TElemType data;
    ChildPtr firstchild;    
}CTBox;
typedef struct    /*树结构*/   
{
    CTBox nodes[MAX_TREE_SIZE]; /*结点数组*/
    int r,n;      /*根的位置和结点数*/
}CTree;
  • 但这种方法找双亲比较麻烦,所以可以把双亲表示法和孩子表示法综合一下:在这里插入图片描述
孩子兄弟表示法
  • 因结点的第一个孩子和右兄弟都是如果存在就是唯一的,因此我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
    在这里插入图片描述
/*孩子兄弟表示法结构定义*/
typedef struct CSNode
{
    TElemType data;
    struct CSNode *fisrtchild, *rightsib;
}CSNode,*CSTree;
  • 如果需要,可以增加一个parent指针域来解决快速查找双亲的问题。
  • 这种表示方法最大好处是它把一棵复杂树变成了一棵二叉树。

二叉树的定义

  • 是什么:每个结点最多有两棵子树的有序树。
  • 特殊二叉树:
    斜树:所有的结点都只有左子树的二叉树叫左斜树。
    满二叉树:所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上。特点:叶子系欸但只能出现在最下一层;非叶子结点的度一定是2;在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
    完全二叉树:对二叉树按层序编号,如果编号为i的结点与同样深度的满二叉树中编号的结点在二叉树中位置完全相同。特点:叶子结点只能出现在最下两层;最下层的叶子一定集中在左部连续位置;倒数二层,若有叶子结点,一定在右部连续;如果系欸但的度为1,则该结点只有左孩子;同样结点数的二叉树,完全二叉树的深度最小。

二叉树的性质

  • 在二叉树上第i层上至多有2i-1个结点。(i>=1)
  • 深度为k的二叉树至多有2k-1个结点。(k>=1)
  • 对任何一棵二叉树T,若其终端结点数为n0,度为2的结点数位n2,则n0=n2+1:
    总结点数n,总分支数 n-1=2n2+n1,而n=n0+n1+n2
  • 具有n个结点的完全二叉树的深度位 floor(log2n)+1:
    其结点数一定少于同样深度的满二叉树的结点数2k-1,但一定多余2k-1-1,即2k-1-1<n<=2k-1,由于n是整数,意味着2k-1<=n<2k,两边取对数,得到k-1<=log2n<k,而k也是整数,则k=floor(log2n)+1。
  • 如果对一棵有n个结点的完全二叉树按层序编号,对任一结点i:①若i=1是根,若i>1,则其双亲是结点floor(i/2)。②若2i>n,则结点i无左孩子(结点i为叶子节点),否则其左孩子是结点2i。③若2i+1>n,则结点i无有孩子,否则其右孩子是结点2i+1。

二叉树的存储结构

二叉树的顺序存储结构
  • 是什么:按层序编号作为下标存入一维数组,不存在的结点设为NULL。但不能反映逻辑关系。如果是斜树会造成极大浪费,因此顺序存储结构一般只用于完全二叉树。
二叉链表

/*二叉树的二叉链表结点的结构定义*/
typedef struct BiTNode /*结点结构*/
{
    TElemType data;    /*结点数据*/
    struct BiTNode *lchild,*rchild;  /*左右孩子指针*/
}BiTNode,*BiTree;

遍历二叉树

  • 原理:从根结点除法,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
  • 遍历就是在把树中的结点编程某种意义的线性序列。
  • 遍历方法:
    ①前序:ABDGHCEIF
    在这里插入图片描述
/*二叉树的前序遍历递归算法*/
void PreOrderTraverse(BiTree T)
{
    if(T==NULL)
        return;
    printf("%c",T->data);
    PreOrderTraverse(T->lchild);
    PreOrderTraverse(T->rchild);
}

②中序:CDHBAEICF
在这里插入图片描述

/*二叉树的中序遍历递归算法*/
void PreOrderTraverse(BiTree T)
{
    if(T==NULL)
        return;
    PreOrderTraverse(T->lchild);
    printf("%c",T->data);
    PreOrderTraverse(T->rchild);
}

③后续:GHDBIEFCA
在这里插入图片描述

/*二叉树的后序遍历递归算法*/
void PreOrderTraverse(BiTree T)
{
    if(T==NULL)
        return;
    PreOrderTraverse(T->lchild);
    PreOrderTraverse(T->rchild);
    printf("%c",T->data);
}

④层序:ABCDEFGHI
在这里插入图片描述

  • 推导遍历结果:中+前 ; 中+后

二叉树的建立

  • 扩展二叉树:将二叉树的每个结点的空指针设置为“#”。遍历扩展二叉树就可以确定一棵二叉树。
/*按前序输入二叉树中的结点的值(一个字符)*/
/* #表示空树,构造二叉链表表示二叉树T */
void CreateBiTree(BiTree *T)
{
    TElemType ch;
    scanf("%c",&ch);
    if(ch=='#')
        *T=NULL;
    else
    {
        *T=(BiTree)malloc(sizeof(BiTNode));
        if(!*T)
            exit(OVERFLOW);
        (*T)->data=ch; /*生成根结点*/
        CreateBiTree(&(*T)->lchild);   /*构造左子树*/
        CreateBiTree(&(*T)->rchild);   /*构造右子树*/
    }
}

线索二叉树

  • 是什么:加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。线索是指向前驱和后继的指针。
  • 原理:n个结点的二叉链表,一共2n个指针域,而n个结点的二叉树一共有n-1条分支。存在2n-(n-1)=n+1个空指针域,可以用空地址存放指向结点在某种遍历次序下的前驱和后继结点的地址。
  • 线索化:对二叉树以某种次序遍历使其变为线索二叉树的过程。就是在遍历的过程中修改空指针的过程。
  • 线索二叉树结构实现
    在这里插入图片描述
    ltag为0时指向该孩子的左孩子,为1时指向该结点的前驱。
    rtag为0时指向该孩子的右孩子,为1时指向该结点的后继。

/*二叉树的二叉线索存储结构定义*/
typedef enum {Link,Thread} PointerTag; /*Link==0表示指向左右孩子指针*/
                                       /*Thread==1表示指向前驱或者后继的线索*/
typedef struct BiThrNode   /*二叉线索存储结点结构*/
{
    TElemType data;        /*结点数据*/
    struct BiThrNode *lchild,*richild; /*左右孩子指针*/
    PointerTag LTag;
    PointerTag RTag;     
}BiThrNode,*BiThrTree;

BiThrTree pre; /*全局变量,始终指向刚刚访问过的结点*/
/*中序遍历进行中序线索化*/
void InThreading(BiThrTree p)
{
    if(p)
    {
        InThreading(p->lchild);  /*递归左子树线索化*/
        if(!p->lchild)          /*左指针域为空*/
        {
            p->LTag=Thread;     /*前驱结点刚访问过,赋值给了pre*/
            p->lchild=pre;      /*所以将pre赋值给p->lchild,并修改p->LTag*/
        }
        if(!pre->richild)       /*后继还未访问到,因此只能判断前驱pre的右指针*/
        {
            pre->RTag=Thread;   /*若为空那么p就是pre的后继*/
            pre->richild=p;     /*前驱有孩子指针指向后继(当前结点p)*/
        }
        pre=p;                  /*完成前驱后继判断后,别忘记将当前结点p赋值给pre*/
        InThreading(p->richild); /*递归右子树线索化*/
    }
}
  • 有了线索二叉树后,我们对它进行遍历,其实就等于操作一个双向链表结构。
  • ThrHead->lchild=根结点,ThrHead->rchild=last. 二叉树中序序列中的第一个结点的lchild指向 头结点,last的richild指向头结点,这样的好处是我们既可以从第一个结点起顺序后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
/*T指向头结点,ThrHead->lchild=root,ThrHead->rchild=last*/
/*中序遍历二叉线索链表表示的二叉树,时间复杂度O(n)*/
Status InOrderTraverse_Thr(BiThrTree T)
{
    BiThrTree p;
    p = T->lchild;      /*让p指向根结点开始遍历*/
    while(p!=T)         /*空树或遍历结束时p==T*/
    {
        /*先到最左端,然后不是一直找后继就是直接右孩子*/
        while ((p->LTag==Link))  /*当LTag==0时循环到中序序列第一个结点*/
            p = p->lchild;
        printf("c",p->data); /*显示结点数据,可更改为其他对结点操作*/
        while(p->RTag==Thread && p->richild!=T)
        {
            p=p->richild;
            printf("%c",p->data);   
        }
        p = p->richild;      /*p进至右子树根*/
    }
    return OK;
}

树、森林与二叉树的转换

  • 树转换为二叉树:
    ①兄弟结点直接连线②数中每个结点指保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线③层次调整,第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是右孩子。
    在这里插入图片描述
  • 森林转换为二叉树:
    ①把每个树转换为二叉树。②从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的有孩子,用线连起来。在这里插入图片描述
  • 二叉树转换为树:
    ①将每个结点左孩子的n个右孩子结点都作为此结点的孩子,连起来②删除原二叉树中多有结点与其右孩子结点的来你先。③层次调整
    在这里插入图片描述
  • 二叉树转换为森林
    ①从根结点开始,有右孩子就删连线,分离后的二叉树,有右孩子继续删,直到所有右孩子连线都删完。②再将每棵分离后的二叉树转换为树即可。在这里插入图片描述
  • 树与森林的遍历
    树的遍历有两种:①先根遍历:先访问树的根结点,然后依次先根遍历根的每棵子树。ABEFCDG②后根遍历:依次后根遍历每个子函数,然后再访问根结点。EFBCGDA
    在这里插入图片描述
    树的遍历也有两种:
    ①前序遍历:每棵树依次先根遍历。ABEFCDGHJI ②每棵树依次后根遍历。BCFAEFJHIG在这里插入图片描述
  • 森林的前序遍历和二叉树的前序遍历相同,森林的后序遍历和二叉树的中序遍历结果相同。这告诉我们,当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。

赫夫曼树及其应用

  • 结点路径长度:树中两个结点之间的分支数目。树的路径长度:树根到每一结点的路径长度之和。结点的带权路径长度:该结点到树根之间的路径长度与权的乘积。树的 带权路径长度:树中所有叶子节点的带权路径长度之和。
  • 赫夫曼树:带权路径长度WPL最小的二叉树。
  • 构造二叉树:每次选两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且新置的二叉树的根结点的权值为其左右子树上根结点的权值之和。删除这两棵树,将新得到的树加入F中。直到只含一棵树为止。
  • 设计长短不等的编码,则必须任一字符的编码都不是另一个字符的编码的前缀,这种编码称为前缀编码
  • 赫夫曼编码:字符集作为叶子结点,频率作为权值,构造赫夫曼树。规定赫夫曼树左支代表0,右支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这便是赫夫曼编码。

总结

  • 开头提到树的递归定义。提到如子树、结点、度、叶子、分支结点、双亲、孩子、层次、深度、森林等诸多概念。
  • 树的存储结构:双亲表示法、孩子表示法、孩子兄弟表示法(并引出二叉树)等。
  • 二叉树提到了斜树、满二叉树、完全二叉树等特殊二叉树的概念。
  • 二叉树的存储结构由于其特殊性使得既可以用顺序存储结构又可以用链式存储结构表示。
  • 遍历时二叉树中最重要的一门学问。
  • 二叉树的建立可用递归实现。
  • 利用二叉树中的空指针引出了线索二叉树。
  • 树、森林看似复杂棵转化为简单的二叉树来处理。
  • 二叉树的一个应用:赫夫曼树和赫夫曼编码。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值