数据结构(C语言版 严蔚敏著)——树

· (tree)是n(n>=0)个结点的有限集。当n=0时成为空树,在任意一颗非空树中:

//这里只需掌握定义,重点在二叉树 

    -有且仅有一个特定的称为根(Root)的结点;

    -当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、...、Tm,

     其中集合本身又是一棵树,并且称为根的子树(SubTree)。

    - n>0时,根结点是唯一的,坚决不可能存在多个根结点。

    - m>0时,子树的个数是没有限制的,但它们互相是一定不会相交的。

· 结点拥有的子树称为结点的度(Degree),树的度取树内各结点的度的最大值 。

    -度为0的结点称为叶结点(Leaf)或终端结点。

    -度不为0的结点称为分支结点或非终端结点,除根结点外 ,分支结点也称为内部结点。


· 结点的子树的根称为结点的孩子(Child),相应的,该结点称为孩子的双亲(Parent),

  同一双亲的孩子之间互称为兄弟(Sibling)。

· 结点的祖先是从根到该结点所经分支上的所有结点。


· 结点的层次(Level)从根开始定一起,根为第一层,根的孩子为第二层。

· 其双亲在同一层的结点互为堂兄弟。

· 树中结点的最大层次称为树的深度(Depth)或高度。


· 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树

  为有序树,否则称为无序树。


二叉树

定义

· 二叉树是n(n>=0)个结点 的 有限集合,该集合或者为空集(空二叉树),或者由

  一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。

特点

· 每个结点 最多 有两棵子树,所以二叉树中不存在度大于2的结点。

· 左子树和右子树是有顺序的,次序不能颠倒。

· 即使树中某节点只有一棵子树,也要区分它是左子树还是右子树。

五种基本形态

· 空二叉树

· 只有一个根结点

· 根结点只有左子树

· 根结点只有右子树

· 根结点既有左子树又有右子树



满二叉树

    -在一棵二叉树中,如果所有分支点都存在左子树和右子树,并且所有叶子都在同一层上,

     这样的二叉树称为满二叉树。


· 满二叉树的特点有:

    -叶子只能出现在最下一层。

    -非叶子结点的度一定是2。

    -在同样深度的二叉树中,满二叉树的结点个数一定最多,同时叶子也是最多。

完全二叉树

· 对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树

  中编号为i的结点位置完全相同,则这颗 二叉树称为完全二叉树。


· 完全二叉树的特点有:

    -叶子结点只能出现在最下两层。

    -最下层的叶子一定集中在左部连续位置。

    -倒数第二层,若有叶子结点,一定都在右部连续位置。

    -如果结点度为1,则该结点只有左孩子。

    -同样结点数的二叉树,完全二叉树的深度最小。

· 注意:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。


二叉树的性质

· 性质一:在二叉树的第i层上至多有2^(i-1)个结点(i>=1)。

· 性质二:深度为k的二叉树至多有2^k-1个结点(k>=1)。

· 性质三:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。

    -推导过程

    -首先再假设度为1的结点数为n1,则二叉树T的结点总数n=n0+n1+n2

    -其次发现连接树总是等于总结点数n-1,并且等于n1+2*n2

    -所以n-1=n1+2*n2

    -所以n0+n1+n2-1=n1+n2+n2

    -最后n0=n2+1

· 性质四:具有n个结点的完全二叉树的深度为取下整的(log2n)+1

    -由满二叉树的定义结合性质二可得,深度为k的满二叉树的结点树n一定是2^k-1。

    -对于满二叉树可以通过n=2^k-1推得满二叉树的深度为k=log2(n+1)


    -对于倒数第二层的满二叉树我们同样很容易回推出它的结点数为n=2^(k-1)-1

    -所以完全二叉树的结点数的取值范围是:2^(k-1)-1<n<=2^k-1

    -由于n是整数,n<=2^k-1可以看成n<2^k

    -同理2^(k-1)-1<n可以看成2^(k-1)<=n

    -所以2^(k-1)<=n<2^k

    -不等式 两边同时取对数,得到k-1<=log2n<k

    -由于k是深度,必须取整,所以k为取下整的(log2n)+1

· 性质五:如果对一棵有n个结点的完全二叉树(其深度为(log2n)+1)的结点

  按层序编号,对任一结点i(1<=i<=n)有以下性质:

    -如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲结点[i/2]取下整

    -如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子结点是2i

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

二叉链表

存储结构:

typedef char TElemType;
typedef struct BiTNode{
    TElemType data;
    BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;

创建一个二叉树

void CreateBiTree(BiTree &T){
    //按先序遍历输入结点,左孩子或右孩子为空,用空格代替
    char c;
    scanf("%c",&c);
    if(c==' '){
        //如果为空格,则指向的左孩子或者右孩子为空
        T=NULL;
    } else{
        //创建结点,按照先序遍历创建
        T=(BiTree)malloc(sizeof(BiTNode));
        if(!T)
            exit(0);
        T->data=c;
        CreateBiTree(T->lchild);
        CreateBiTree(T->rchild);
    }
}

二叉树的遍历

· 二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得

  每个结点被访问一次且仅被访问一次。

· 二叉树的遍历次序不同于线性结构,线性结构最多也就是分为顺序、循环、双向等

  简单的遍历方式。

· 树的结点之间不存在唯一的前驱和后继这样的关系,在访问一个结点后,下一个被

 访问的结点面临着不同的选择。

· 二叉树的遍历方式可以很多,主要有下面三种:

· 前序遍历

    -若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。

· 中序遍历

    -若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序

      遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。

· 后序遍历

    -若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后根结点。


    先序遍历:ABCDEFGHK

    中序遍历:BDCAEHGKF

    后序遍历:DCBHKGFEA

递归遍历算法代码实现:

void  PrintElement(TElemType e){
    printf("%c",e);
}

void PreOrderTraverse(BiTree T){
    if(T){
        //先序遍历
        //三种遍历方式只不过更换下面三句语句的顺序
        PrintElement(T->data);
        PreOrderTraverse(T->lchild);
        PreOrderTraverse(T->rchild);
    }
}
void InOrderTraverse(BiTree T){
    //中序遍历
    if(T){
        InOrderTraverse(T->lchild);
        PrintElement(T->data);
        InOrderTraverse(T->rchild);
    }
}

void PostOrderTraverse(BiTree T){
    //后序遍历
    if(T){
        PostOrderTraverse(T->lchild);
        PostOrderTraverse(T->rchild);
        PrintElement(T->data);
    }
}

非递归的两种算法:

· 另需定义一个栈,存储结点

#define STACK_INIT_SIZE 100 //存储空间初始分配量
#define STACKINCREMENT 10   //存储空间分配增量
typedef struct {
    BiTree *base;    //在栈构造之前和销毁之后,base值为NULL
    BiTree *top;     //栈顶指针
    int stacksize;      //当前已分配的存储空间,以元素为单位
} SqStack;
int InitStack(SqStack &S) {
    //构造一个空栈S
    S.base = (BiTree *) malloc(STACK_INIT_SIZE * sizeof(BiTree));
    //存储分配失败
    if (!S.base)
        exit(0);
    S.top = S.base;
    S.stacksize = STACK_INIT_SIZE;
    return 1;
}


int Push(SqStack &S, BiTree e) {
    //插入元素e为新的栈顶元素
    if (S.top - S.base >= S.stacksize) {
        //栈满,追加存储空间
        S.base = (BiTree *) realloc(S.base,
                                       (S.stacksize + STACKINCREMENT) * sizeof(BiTree));
        //出错退出
        if (!S.base)
            exit(0);
        //使top指针重新回到栈顶
        S.top = S.base + S.stacksize;
        S.stacksize += STACKINCREMENT;
    }
    *S.top++ = e;//赋值后,指针上移
    return 1;
}

int Pop(SqStack &S, BiTree &e) {
    //若栈不为空,则删除S的栈顶元素,用e返回其值
    //并返回1,否则返回0
    if (S.top == S.base)
        return 0;
    //top指针下移,并赋值给e
    e = *--S.top;
    return 1;
}

int GetTop(SqStack S, BiTree &e) {
    //若栈不空,则用e返回S的栈顶元素,并返回1,否则返回0
    if (S.top == S.base)
        return 0;
    e = *(S.top - 1);
    return 1;
}

int StackEmpty(SqStack S) {
    //判断栈是否为空,空则返回1,否则返回0
    if (S.base == S.top)
        return 1;
    else
        return 0;
}
void unInOrderTraverse1(BiTree T){
    //采用二叉链表存储结构
    //中序遍历二叉树T的非递归 算法
    //方法1
    SqStack S;
    BiTree p;
    InitStack(S);//创建栈
    Push(S,T);//头结点入栈
    while (!StackEmpty(S)){//当栈非空时
        while (GetTop(S,p)&&p)//把栈顶元素给p,且p存在
            Push(S,p->lchild);//一直往左,直到尽头
        Pop(S,p);//空指针出栈
        if(!StackEmpty(S)){//判断是否空栈
            Pop(S,p);//最左边的一个结点出栈
            if(!p->data)//访问结点
                exit(0);
            else
                PrintElement(p->data);
            Push(S,p->rchild);//该结点的右子树进栈
        }
    }
}

void unInOrderTraverse2(BiTree T){
    //采用二叉链表存储结构
    //中序遍历二叉树T的非递归 算法
    //方法2
    SqStack S;
    BiTree p;
    //创建栈
    InitStack(S);
    p=T;
    while (p||!StackEmpty(S)){
        if(p){
            //根指针进栈,遍历左子树
            Push(S,p);
            p=p->lchild;
        } else{
            //根指针退栈,访问根结点 ,遍历右子树
            Pop(S,p);
            if(!p->data)
                exit(0);
            else
                PrintElement(p->data);
            p=p->rchild;
        }
    }
}

线索二叉树

普通二叉树在叶子结点中存在空指针,造成了空间浪费,线索二叉树把这些利用起来

并且能提高遍历的效率。就像链表一样,直接指示下一个结点的位置。

需要增加两个标识域


· LTage 为0  lchild域指示结点的左孩子

· LTage 为1  lchild域指示结点的前驱

· RTage 为0  rchild域指示结点的右孩子

· RTage 为1  rchild域指示结点的后继

结构体代码:
typedef char TElemType;
//Link为0表示左右孩子的指针
//Thread为1表示前驱后继的线索
enum PointerTag {
    Link, Thread
};
typedef struct BiThrNode {
    TElemType data;
    struct BiThrNode *lchild, *rchild;
    PointerTag LTag, RTag;
} BiThrNode, *BiThrTree;

中序遍历线索化以及中序遍历二叉线索树T的非递归算法

//全局变量,始终指向刚刚访问过的结点
BiThrTree pre;

void CreateBiThrTree(BiThrTree &T) {
    //遵循前序遍历约定输入
    char c;
    scanf("%c", &c);
    if (c == ' ')
        T = NULL;
    else {
        T = (BiThrTree) malloc(sizeof(BiThrNode));
        if (!T)
            exit(0);
        T->data = c;
        printf("%c", c);
        //先默认它有左右子树
        T->LTag = Link;
        T->RTag = Link;

        CreateBiThrTree(T->lchild);
        CreateBiThrTree(T->rchild);
    }
}

//中序遍历线索化
void InTreading(BiThrTree p) {
    if (p) {
        //递归左孩子线索化
        InTreading(p->lchild);
        //如果该结点没有左孩子,设置LTag为Thread,
        // 并把lchild指向刚刚访问的结点,设为前驱
        if (!p->lchild) {
            p->LTag = Thread;
            p->lchild = pre;
        }
        //如果该结点没有右孩子,设置RTag为Thread,
        // 并把刚刚访问过结点的rchild指向当前结点,设为后继
        if (!pre->rchild) {
            pre->RTag = Thread;
            pre->rchild = p;
        }
        pre = p;
        InTreading(p->rchild);
    }
}

void InOrderThreading(BiThrTree &Thrt, BiThrTree T) {
    //中序遍历二叉树T,并将其中序线索化,Thrt指向头结点
    if (!(Thrt = (BiThrTree) malloc(sizeof(BiThrNode))))
        exit(0);
    //建立头结点
    Thrt->LTag = Link;
    Thrt->RTag = Thread;
    Thrt->rchild = Thrt;//右指针回指
    //若二叉树空,则左指针回指
    if (!T)
        Thrt->lchild = Thrt;
    else {
        Thrt->lchild = T;
        pre = Thrt;
        InTreading(T);//中序遍历进行中序线索化
        //最后一个结点线索化
        pre->rchild = Thrt;
        pre->RTag = Thread;
        Thrt->rchild = pre;
    }
}

void PrintElement(TElemType e) {
    printf("%c", e);
}

void InOrderTraverse_Thr(BiThrTree T) {
    //T指向头结点,头结点的左链lchild指向根结点
    //中序遍历二叉线索树T的非递归算法
    BiThrTree p;
    p = T->lchild;//p指向根结点
    while (p != T) {//空树或遍历结束时,p==T
        while (p->LTag == Link)
            p = p->lchild;
        if (!p->data)
            exit(0);
        else
            PrintElement(p->data);//访问其左子树为空的结点
        while (p->RTag == Thread && p->rchild != T) {
            p = p->rchild;
            PrintElement(p->data);//访问后继结点
        }
        p = p->rchild;
    }
}

树、森林及二叉树的相互转换

· 树转换成二叉树

    -加线,在所有兄弟结点之间加一条线。

    -去线,对树中每个结点,只保留它与第一孩子结点的连线,

      删除它与其他孩子结点之间的连线。

    -层次调整,以树的根结点为轴心,将整棵树顺时针旋转

      一定角度,使之结构层次分明。


1.第一步,在树中所有兄弟结点之间加一连线


2.第二步,对每个结点,除了保留与其长子的连线外,去掉该结点与其它孩子的连线。



· 森林转换二叉树

    -先将森林中的每棵树变为二叉树。

    -再将各二叉树的根结点视为兄弟从左至右连在一起,就这样形成一个二叉树。

      把第一棵根结点为根结点,其他根结点连起来,作为它的右子树。


1.第一步,先将森林中的每棵树变为二叉树。


2.第二步,将各二叉树的根结点视为兄弟从左至右连在一起。



· 二叉树到树、森林的转换

    -二叉树转换为普通树是刚才的逆过程,步骤也就是反过来而已

    -判断一棵二叉树能够转换成一棵树还是森林,那就是只要看这棵

     二叉树的根结点有没有右子树,有的话就是森林,没有就是一棵树。





树与森林的遍历:(理解)

· 树的遍历分为两种方式:一种是先根遍历,另一种是后根遍历。

· 先根遍历:先访问树的根结点,然后再依次先根遍历根的每棵子树。

· 后根遍历:依次遍历每棵子树,然后再访问根结点。


· 先根遍历结果:ABEFCGDHIJ

· 后根遍历结果:EFBGCHIJDA

·森林的遍历也分为前序遍历和后序遍历,其实就是按照树的先根遍历和

 后根遍历依次访问森林的每棵树。

· 有个 惊人的发现:树、森林前根(序)遍历和二叉树的前序遍历结果相同,

  树、森林的后根(序)遍历和二叉树的中序遍历结果相同

· 于是我们可以找到对树和森林遍历这种复杂问题的简单解决方案。


赫夫曼树

· 结点的路径长度:

    -从根结点到该结点的路径上的连接数

· 树的路径长度:

    -树中每个叶子结点的路径长度之和

· 结点带权路径长度:

    -结点的路径长度与结点权值的乘积

· 树的带权路径长度:

    -WPL是树中所有叶子结点的带权路径长度之和

WPL值越小,说明构造出来的二叉树性能越优

赫夫曼树的构造过程

    -选权值最小的两个结点构成一个二叉树,其双亲结点权值为两个结点权值之和。

    -然后再选取剩下结点的权值最小的,与上一步的二叉树组合,构成新的二叉树,

      如此重复,直到没有结点剩下,这棵二叉树便是赫夫曼树。





构造完成。

注意:为了使得到的哈夫曼树的结构尽量唯一,通常规定生成的哈夫曼树中每个结点的左子树

根结点的权小于等于右子树根结点的权。

赫夫曼编码

· 赫夫曼编码可以有效地压缩数据(通常可以节省20%~90%的空间,具体压缩率依赖于数据的特性)。

· 定长编码、变长编码、前缀码

    -定长编码:类似于ASCII编码。

    -变长编码:单个编码的长度不一致,可以根据整体出现频率来调节。

    -前缀码:没有任何码字是其他码字的前缀。

·  赫夫曼树中没有度为1的结点(这类树又称为严格的二叉树),则一棵有n个叶子结点的赫夫曼树

   共有2n-1个结点,可以存储在一个大小为2n-1的以为数组中。

· 由于在构成赫夫曼树之后,为求编码需从叶子结点出发走一条从叶子到根的路径;而为编码需

  从根到叶子的路径。则对没个结点而言,既需知双亲的信息,又需知孩子结点的信息。

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int weight;
    int parent, lchild, rchild;
} HTNode, *HuffmanTree;   //动态分配数组存储赫夫曼树
typedef char **HuffmanCode;//动态分配数组存储赫夫曼编码表
//这里可以理解成相当于许多字符串组成的数组

void Select(HuffmanTree htree, int end, int &s1, int &s2) {
    //从数集中选取parent=0,weight最小的两个结点,其下标存入s1,s2
    int min1, min2;
    int i = 1;
    //找到第一个没有双亲的结点
    while (htree[i].parent != 0 && i <= end)
        i++;
    //作为一个参考的最小值,存入min1和s1
    min1 = htree[i].weight;
    s1 = i;
    //下个结点开始
    i++;
    //找到第二个没有双亲的结点,与前面那个作对比
    //较小的放min1和s1,较大的放min2和s2
    while (htree[i].parent != 0 && i <= end)
        i++;
    if (htree[i].weight < min1) {
        min2 = min1;
        s2 = s1;
        min1 = htree[i].weight;
        s1 = i;
    } else {
        min2 = htree[i].weight;
        s2 = i;
    }
    //遍历剩下无双亲的结点
    for (int j = i + 1; j <= end; j++) {
        if (htree[j].parent != 0)
            continue;
        //如果比min1小,min1,s1的数据移到min2,s2
        //并把当前结点的值赋给min1,结点序号给s1
        if (htree[j].weight < min1) {
            min2 = min1;
            min1 = htree[j].weight;
            s2 = s1;
            s1 = j;
        } else if (htree[j].weight >= min1 && htree[j].weight < min2) {
            //如果比min1大且比min2小,
            //则把当前结点值赋给min2,序号给s2
            min2 = htree[j].weight;
            s2 = j;
        }
    }
}

void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int *w, int n) {
    //w存放n个字符的权值(均>0),构造赫夫曼树HT,并求出n个字符的赫夫曼树编码HC
    HuffmanTree p;
    int i, s1, s2, start, c, f;
    if (n <= 1)
        return;
    int m = 2 * n - 1;
    HT = (HuffmanTree) malloc((m + 1) * sizeof(HTNode));//0号单元不用
    //注意,第一个结点为空
    //每个结点赋初值
    //n个叶子结点
    for (p = HT+1, i = 1; i <= n; ++i, ++p, ++w)
        *p = {*w, 0, 0, 0};
    //m-n个终端结点
    for (i; i <= m; ++i, ++p)
        *p = {0, 0, 0, 0};
    for (i = n + 1; i <= m; ++i) {
        //在HT[1...i-1]选择parent为0且weight最小的两个结点,其序号分别为 s1和s2
        //组合好后又成为一个新的可选结点,故[1...i-1]
        Select(HT, i - 1, s1, s2);
        //最小两个结点的双亲序号为当前i
        HT[s1].parent = i;
        HT[s2].parent = i;
        //当前i结点左右孩子序号分别是s1,s2
        HT[i].lchild = s1;
        HT[i].rchild = s2;
        //权重为两孩子之和
        HT[i].weight = HT[s1].weight + HT[s2].weight;
    }
    //---从叶子到根逆向求没个字符的赫夫曼编码---
    //分配n+1个字符编码的头指针向量
    //0号位不存放数据
    HC = (HuffmanCode) malloc((n + 1) * sizeof(char *));
    //分配求编码的工作空间
    char *cd = (char *) malloc(n * sizeof(char));
    //最后一个字符为结束符
    cd[n - 1] = '\0';
    //逐个字符求赫夫曼编码
    for (i = 1; i <= n; ++i) {
        start = n - 1;//编码结束符位置
        for (c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent){
            //从叶子到根逆向求编码
            if (HT[f].lchild == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
        }
        //为第i个字符分配空间
        HC[i] = (char *) malloc((n - start) * sizeof(char));
        //将遍历得到的编码串复制到HC[i]
        strcpy(HC[i], &cd[start]);
    }
    free(cd);
}

void print_huffman_tree(HuffmanTree htree, int n) {
    printf("Huffman tree:\n");
    int m = 2 * n - 1;
    for (int i = 1; i < m; ++i) {
        printf("node_%d, weight = %d, parent = %d, left = %d, right = %d\n",
               i, htree[i].weight, htree[i].parent, htree[i].lchild, htree[i].rchild);
    }
}

void print_all_huffman_code(HuffmanCode HC, int n) {
    printf("Huffman code:\n");
    for (int i = 1; i <= n; ++i) {
        printf("%d code = %s\n", i, HC[i]);
    }
}

int main() {
    int w[5] = {2, 8, 7, 6, 5};
    int n = 5;
    HuffmanTree HT;
    HuffmanCode HC;
    HuffmanCoding(HT, HC, w, n);

    print_huffman_tree(HT, n);
    print_all_huffman_code(HC, n);
    return 0;
}



猜你喜欢

转载自blog.csdn.net/super_sloppy/article/details/79724288
今日推荐