数据结构复习五:树和二叉树

树和二叉树的定义

树的定义

是n个结点的有限集,n=0时为空树,对于非空树有:

1、有且仅有一个称之为根的结点;

2、除根结点以外的其余节点可分为m个互不相交的有限集T1、T2、…、Tm,其中每一个集合本身又是一棵树,并且成为根的子树。

树的基本术语

1、结点:树中的一个独立单元,包含一个数据元素以及若干指向其子树的分支;

2、结点的度:结点拥有的子树数;

3、树的度:树内个结点度的最大值;

4、叶子:度为0的结点(终端结点);

5、非终端结点:度不为0的结点(分支结点);

6:双亲和孩子:结点的子树的根称为该节点的孩子,相应地,该节点成为孩子的双亲;

7、兄弟:同一个双亲的孩子之间互称兄弟;

8、祖先:从根到该结点所经分支上的所有结点;

9、子孙:以某一结点为根的子树中任一结点都成为该结点的子孙;

10、层次:从根开始,根为第一层,树中任意结点的层次等于其双亲结点的层次加一;

11、堂兄弟:双亲在同一层的结点互为堂兄弟;

12、树的深度:树中结点的最大层次称为树的深度或高度;

13、有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树的最左边的子树的根称为第一个孩子,最右边的称为第二个孩子;

14、森林:是m棵互不相交树的集合;对于树中每个结点而言,其子树的集合即为森林。

二叉树的定义

二叉树是n个结点的所构成的集合,n=0时为空树,对于非空树T有:

1、有且仅有一个称之为根的结点;

2、除根结点以外的其余节点可分为2个互不相交的有限集T1、T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树;

二叉树

二叉树的性质

1、在二叉树的第i层上至多有2^{i-1}个结点(i>=1);

2、深度为k的二叉树至多有2^{k}-1个结点(k>=1);

3、任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1;

简单证明一下:n=n0+n1+n2(各度数结点之和)又n=n1+2*n2+1(子树之和加一个根),联立得n0=n2+1;

4、具有n个结点的完全二叉树的深度为\left \lfloor log_{2} n\right \rfloor+1

5、如果对一棵又n个结点的完全二叉树的结点按层序的编号,则对任一结点,有:

(1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点 \left \lfloor i/2\right \rfloor

(2)如果2i大于n,则结点i无左孩子,否则其左孩子是结点2i;

(3)如果2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1;

满二叉树:深度为k且含有2^{k}-1个结点的二叉树;

完全二叉树:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树;

二叉树的存储结构

两种存储方式:顺序存储、链式存储;

顺序存储时,对于完全二叉树,只需从根开始按层序存储即可;对于一般二叉树,须将其与完全二叉树上的结点对照,存储在一维数组中的相应位置中,不存在的结点标为0;

链式存储:

typedef struct BiTNode{
    char data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

遍历二叉树

根据对根遍历的顺序可分为中序遍历先序遍历后序遍历三种遍历方法

先序遍历操作如下:

若二叉树为空,则空操作,否则:

1、访问根结点;

2、先序遍历左子树;

3、先序遍历右子树;

其他的以此类推;

先序遍历:

先序遍历的递归算法

void PreOrderTraverse(BiTree T){
    if(T){
        cout<data;
        PreOrderTraverse(T->lchild);
        PreOrderTraverse(T->rchild);
    }
    return;
} 

中序遍历

中序遍历的递归算法

void InOrderTraverse(BiTree T){
    if(T){
        PreOrderTraverse(T->lchild);
        cout<data;
        PreOrderTraverse(T->rchild);
    }
    return;
} 

后序遍历

后序遍历的递归算法

void PostOrderTraverse(BiTree T){
    if(T){
        PreOrderTraverse(T->lchild);
        PreOrderTraverse(T->rchild);
        cout<data;
    }
    return;
} 

二叉树遍历的应用

建立二叉链表(以先序遍历为例)

算法步骤:

1、扫描字符序列,读入字符ch;

2、如果ch是一个’/‘字符,说明该二叉树为空树,即T为NULL,否则执行一下操作:

(1)申请一个结点空间T;

(2)将ch赋给T->data;

(3)递归创建T的左子树;

(4)递归创建T的右子树;

上代码:

void CreateTree(BiTree &T){//建树
    char elem;
    cin>>elem;
    if(elem=='/') T=NULL;
    else{
        T=new BiTNode;
        T->data=elem;
        CreateTree(T->lchild);
        CreateTree(T->rchild);
    }
    return;
}

还有复制二叉树、计算二叉树深度等算法,思想差不多,不多说了。

线索二叉树

线索二叉树是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序序列、中序序列和后序序列。

改变二叉树结点结构,增加LTag与RTag两个标志域,LTag为0时,lchild指向结点的左孩子,LTag为1时,lchild指向结点的前驱;RTag为0时,rchild指向结点的右孩子,RTag为1时,rchild指向结点的后继;

树和森林

常用的树的存储结构:

1、双亲表示法

以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置;

这种存储结构下,便于求结点的双亲以及树的根,但求结点的孩子时需要遍历整个结构

数据结构复习五:树和二叉树_第1张图片

2、孩子表示法

由于树中每个结点可能有多棵子树,可用多重链表,每个指针域指向一棵子树的根结点

数据结构复习五:树和二叉树_第2张图片

结合孩子和双亲表示法如下图:

数据结构复习五:树和二叉树_第3张图片

3、孩子兄弟表示法

以二叉链表做树的存储结构,链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为firstchild域和nextsibling域

存储结果如下图:

数据结构复习五:树和二叉树_第4张图片

森林与二叉树的转换

1、森林转换成二叉树

如果F={T1,T2,…,Tm}是森林,可按如下规则转换成一棵二叉树B=(root,LB,RB)

(1)若F为空,即m=0,则B为空树;

(2)若F非空,即m!=0,则B的根root即为森林中第一颗树的根ROOT(T1);B的左子树LB是从T1中根结点的子树森林F1={T11,T12,…,T1m}转换而成的二叉树;右子树RB是从森林F’={T2,T3,…,Tm}转换而成的二叉树;

2、二叉树转换成森林

如果B=(root,LB,RB)是一棵二叉树,可按如下规则转换成F={T1,T2,…,Tm}:

(1)若B为空,则F为空;

(2)若B非空,则F中第一棵树T1的根ROOT(T1)即为二叉树B的根root;T1中根结点的子树森林F1是由B的左子树LB转换成的森林;F中除T1之外其余树组成的森林F‘’={T2,T3,…,Tm}是由B的右子树RB转换而成。

树和森林的遍历

1、树的遍历

以上面的图为例

先根遍历:R A D E B C F G H K

后根遍历:D E A B G H K F C R

2、森林的遍历

先序遍历森林(非空):

(1)访问森林中第一棵树的根结点

(2)先序遍历第一棵树的根结点的子树森林

(3)先序遍历除去第一棵树之后剩余的树构成的森林

中序遍历森林(非空):

(1)中序遍历森林中第一棵树的根结点的子树森林

(2)访问第一棵树的根结点

(3)中序遍历除去第一棵树之后剩余的树构成的森林

哈夫曼树及其应用

哈夫曼树又称最优树,是一种带权路径长度最短的树。

结点的带权路径长度:从该结点到树根之间的路径长度与节点上权的乘积;

树的带权路径长度:树中所有叶子节点的带全路径长度之和。

构造过程:

1、根据给定的n个权值{w1,w2,…,wn},构造n棵只有根结点的二叉树,这n棵二叉树构成一个森林F;

2、在森林中选区两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为左右子树上根结点的权值之和;

3、在森林F中删除这两棵树,同时将新得到的二叉树加入F中;

4、重复(2)和(3),直到F只含一棵树为止,这棵树便是哈夫曼树。

哈夫曼树存储结构:

typedef struct HTNode{
    int weight;
    int lchild,rchild,parent;
}HTNode,*HTree;

构造哈夫曼树算法:

1、初始化:首先动态申请2n个单元;然后循环2n-1次,从1号单元开始,依次将1至2n-1所有单元中的双亲、左孩子、右孩子的下表都初始化为0;最后再循环n次,输入前n个单元中叶子节点的权值;

2、创建树:循环n-1次,通过n-1次的选择、删除与合并来创建哈夫曼树。选择是从当前森林中选择双亲为0且权值最小的两个数根节点s1和s2,删除是将s1和s2的双亲改为非0;合并就是将s1和s2的权值和作为一个新结点的权值依次存入到数组的第n+1之后的单元中,同时记录着个新结点左孩子的下标为s1,右孩子的下标为s2。

void CreateHMT(HTree &HT,int n){
    int m=2*n-1;
    HT=new HTNode[m+1];
    for(int i=1;i<=m;++i){
        HT[i].lchild=HT[i].rchild=HT[i].parent=0;
    }
    for(int i=1;i<=n;i++){
        cin>>HT[i].weight;
    }
    for(int i=n+1;i<=m;++i){
        int s1,s2;
        SelectMin(HT,i,s1,s2);
        HT[s1].parent=HT[s2].parent=i;
        HT[i].weight=HT[s1].weight+HT[s2].weight;
        HT[i].lchild=s1;HT[i].rchild=s2;
    }
    return;
}

哈夫曼编码

前缀编码:如果一个编码方案中人一个编码都不是其他任何编码的前缀,则称编码是前缀码;

哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对树中的每个做分支赋予0,右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码

哈夫曼编码是最优前缀编码

哈夫曼编码存储表示:

typedef char **HuffmanCode; 

算法步骤:

1、分配n个字符编码的编码表空间HC,长度为n-1;分配临时存储每个字符编码的动态数组空间cd,cd[n-1]置为’\0‘;

2、逐个求解n个字符的编码,循环n次,执行以下操作:

(1)设置变量start用于记录编码在cd中存放的位置,start初始时指向最后,即编码结束符位置n-1;

(2)设置变量c用于记录从叶子结点向上回溯至根结点所经过的结点下标,c初始时为当前待编码字符的下标i,f用于记录双亲结点的下标;

(3)从叶子节点向上回溯至根结点,求得字符i的编码,当f没有到达根结点时,循环执行以下操作:

回溯依次start向前指一个位置,即--start;

若结点c是f的左孩子,则生成编码0,否则生成编码1,保存在cd[start]中;

继续向上回溯,改变c和f的值。

(4)根据数组cd的字符串长度为第i个字符编码分配空间HC[i],然后将数组cd中的编码复制到HC[i]中;

3、释放临时空间cd。

代码实现;

void CreatHFC(HTree HT,HFMCode &hc,int n){
    hc=new char*[n+1];//开n+1个1维数组空间
    char* cd=new char[n];//临时数组
    cd[n-1]='\0';
    for(int i=1;i<=n;++i){
        int start=n-1;  
        int c=i;
        int f=HT[i].parent;
        while(f!=0){
            start--;
            if(HT[f].lchild==c)
            cd[start]='0';
            else cd[start]='1';
            c=f;
            f=HT[f].parent;
        }
        hc[i]=new char[n-start];
        strcpy(hc[i],&cd[start]);
    }
    delete cd;
    return;
}

附上一条龙完整代码:

#include
using namespace std;
typedef struct HTNode{
    char name;
    int weight;
    int lchild,rchild,parent;
}HTNode,*HTree;
typedef char** HFMCode;
void SelectMin(HTree HT,int n,int &s1,int &s2){
    s1=s2=0;
    int i;
    for(i=0;iHT[s2].weight){//把s1作为较小的元素
        int t=s1;
        s1=s2;
        s2=t;
    }
    for(;i>HT[i].name>>HT[i].weight;
    }
    for(int i=n+1;i<=m;++i){
        int s1,s2;
        SelectMin(HT,i,s1,s2);
        HT[s1].parent=HT[s2].parent=i;
        HT[i].weight=HT[s1].weight+HT[s2].weight;
        HT[i].lchild=s1;HT[i].rchild=s2;
    }
    return;
}
void CreatHFC(HTree HT,HFMCode &hc,int n){
    hc=new char*[n+1];//开n+1个1维数组空间
    char* cd=new char[n];//临时数组
    cd[n-1]='\0';
    for(int i=1;i<=n;++i){
        int start=n-1;  
        int c=i;
        int f=HT[i].parent;
        while(f!=0){
            start--;
            if(HT[f].lchild==c)
            cd[start]='0';
            else cd[start]='1';
            c=f;
            f=HT[f].parent;
        }
        hc[i]=new char[n-start];
        strcpy(hc[i],&cd[start]);
    }
    delete cd;
    return;
}
void Print(HTree HT,HFMCode HC,int n){
    for(int i=1;i<=n;++i){
        cout<>n;
    CreateHMT(HT,n);
    HFMCode HC;
    CreatHFC(HT,HC,n);
    Print(HT,HC,n);
    return 0;
}

测试样例:

数据结构复习五:树和二叉树_第5张图片

————————————————————————————————————————————————————————

图片来源:http://data.biancheng.net/view/30.html

https://www.cnblogs.com/ciyeer/p/9045746.html

https://blog.csdn.net/qq_25775935/article/details/88647758

注:本文所有内容均来源于《数据结构(C语言第二版)》(严蔚敏老师著)

你可能感兴趣的:(数据结构期末复习)