数据结构与算法-Day11-线索二叉树与哈夫曼树

563 阅读8分钟

线索二叉树

在二叉树中,假设有n个结点,那么一共有2n个链域,其中有n-1一个链域被使用,而空的链域有2n-(n-1)=n+1个。如下图所示:

其中叶子结点D、E、F以及没有左孩子或者右孩子的结点C都有1个或者2个的链域为空。

因此,提出了一种方法,利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。

假如ptr为二叉树的某一个结点,建立线索的规则为(以中序遍历为例):

  1. 假如ptr->lchild为空,则存放指向中序遍历中该结点的前驱结点的指针
  2. 假如ptr->rchild为空,则存放指向中序遍历中该结点的后继结点的指针

显然,什么指向孩子结点,什么时候指向前驱或者后继结点,我们还需要两个标识符,在原有的结点结构中增加ltagrtag。新的结点结构如下:

/* Link==0表示指向左右孩子指针, */
/* Thread==1表示指向前驱或后继的线索 */
typedef enum {Link,Thread} PointerTag;

/* 线索二叉树存储结点结构*/
typedef struct BiThrNode{
    //数据
    CElemType data;
    //左右孩子指针
    struct BiThrNode *lchild,*rchild;
    //左右标记
    PointerTag LTag;
    PointerTag RTag;
}BiThrNode,*BiThrTree;

线索二叉树的实现

  • 二叉树的线索化
    线索二叉树的创建方法和一般的二叉树一样,区别在于二叉树的线索化。由于前驱和后继信息只有在遍历该二叉树的时候才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程,得到的结果如下图所示,其中实心箭头虚线代表前驱空心虚线代表后继

void InThreading(BiTree p, BiTree *pre){
    if(p) {
        //中序遍历左子树
        InThreading(p->lchild, pre);
        //因为有前驱结点,我们需要一个pre指针来保存前驱结点
        //根据左子树是否存在修改对应的LTag和lchild
        if(p->lchild) {
            p->LTag = Link;
        }else {
            p->LTag = Thread;
            p->lchild = *pre;
        }
        //根据前驱结点是否存在右子树来修改RTag和rchild
        if(!(*pre)->rchild) {
            (*pre)->RTag = Thread;
            (*pre)->rchild = p;
        }else {
            (*pre)->RTag = Link;
        }
        //修改pre结点
        *pre = p;
        InThreading(p->rchild, pre);
    }
}

上述代码和二叉树的中序遍历很相似,区别的代码在于将访问结点的操作改成了二叉树的线索化。

  • 增加头结点

我们再为当前二叉树增加一个头结点Thrt,令Thrt->lchild指向根结点Thrt->rchild指向遍历的最后一个结点G,再令遍历的第一个结点H->lchild指向头结点Thrt,这样即形成了一个双向的链表。

/* 中序遍历二叉树T,并将其中序线索化,Thrt指向头结点 */
Status InOrderThreading(BiTree *Thrt , BiTree T, BiTree *pre){
    
    *Thrt = (BiTree)malloc(sizeof(BiTNode));
    if(!*Thrt) return printError("创建结点失败", ERROR);
    
    (*Thrt)->LTag = Link;
    (*Thrt)->RTag = Thread;
    
    //右指针回指向
    (*Thrt)->rchild = (*Thrt);
    
    /* 若二叉树空,则左指针回指 */
    if (!T) {
        (*Thrt)->lchild=*Thrt;
    }else{
        //头结点左指针指向根结点
        (*Thrt)->lchild = T;
        *pre = *Thrt;
        InThreading(T, pre);
        //最后一个结点的右指针指向头结点
        (*pre)->rchild = *Thrt;
        (*pre)->RTag = Thread;
        //头结点右指针指向最后一个结点
        (*Thrt)->rchild = *pre;
        
    }
    return OK;
}

线索二叉树的中序遍历

/*中序遍历二叉线索树T*/
Status InOrderTraverse_Thr(BiTree T){
    //T结点指的是头结点,我们先找到根结点p
    BiTree p = T->lchild;
    while(p != T) {
        //
        while(p->LTag == Link) {
            p = p->lchild;
        }
        while(p->RTag == Thread && p->rchild != T) {
            visit(p->data);
            p = p->rchild;
        }
        visit(p->data);
        p = p->rchild;
    }
    return OK;
}

还是以上图为例:

  1. 根据T(Thrt)找到根结点A
  2. 因为遍历的最后一个结点G的右指针指向T,因此当遍历到Gp == T时,循环停止
  3. while(p->LTag == Link) { p = p->lchild; },这段代码会找到第一个没有左子树的结点,即遍历的起始结点H
  4. H开始,沿着我们已经构建好的后继结点,依次遍历并访问,直到找到一个有右子树的结点D,访问D
  5. 指针p指向D->rchild,重新进行循环遍历,直到循环结束。

由于充分利用了空指针域的空间(等于节省了空间),又保证了创建时的一次遍历就可以终生受用后继的信息(意味着节省了时间)。所以在实际问题中,如果所用的二叉树需要经过遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。

哈夫曼树

路径:在一棵树中,一个结点到另一个结点之间的通道,称为路径。上图中,从根结点a之间的通路就是一条路径。

路径长度:在一条路径上,每经过一个结点,路径的长度就要加1。在一棵树中,规定根结点所在层数为1层,那么根结点到第i层结点的路径长度为i-1

结点的权:给每一个结点赋予一个新的数值,称为结点的权。例如上图中a结点的权为7

结点的带权路径长度:指的是从根结点到每一个结点的路径长度和该结点权值的乘积。上图中c结点的带权路径长度为2*3=6

树的带权路径长度为树中所有叶子结点的带权路径长度之和。记做WPL。图中这棵树的带权路径长度为:

WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3 = 30

什么是哈夫曼树

当用n个结点(都做叶子结点并且各有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为最优二叉树,有时也称为赫夫曼树或者哈夫曼树

构建哈夫曼树

对于给定的有各自权值的n个结点,构建哈夫曼树的方法如下:

  1. n个权值中选出两个最小的值,对应的两个结点组成一个新的二叉树,则新二叉树的根结点的权值为左右两孩子权值之和。
  2. 在原有的n个结点中删除权值最小的两个结点,将新的权值加入到n-2个结点中去
  3. 重复步骤1,2,直到所有所有的结点组成一棵二叉树为止。

我们需要对应的哈夫曼结点结构如下:

typedef struct HaffNode{
    int weight;
    int flag;//标识当前结点是否已经生成子树
    int parent;
    int leftChild;
    int rightChild;
}HaffNode;

生成哈夫曼树:

//根据权重值,构建哈夫曼树;
//{2,4,5,7}
//n = 4;
void Haffman(int weight[],int n,HaffNode *haffTree){
    for(int i = 0; i < 2*n-1; i++) {
        if(i < n) {
            haffTree[i].weight = weight[i];
        }else {
            haffTree[i].weight = 0;
        }
        haffTree[i].flag = 0;
        haffTree[i].parent = -1;
        haffTree[i].leftChild = -1;
        haffTree[i].rightChild = -1;
    }
    //m1,m2分别为第1小和第2小的数h值
    //x1,x2为对应的索引
    int m1,m2,x1,x2;
    
    for(int i = 0; i < n-1; i++) {
        m1 = m2 = MaxValue;
        x1 = x2 = 0;
        //找出最小的两个数
        for(int j = 0; j < n+i; j++) {
            //如果有比最小数m1小的数,则当前m1就变成第2小的数,新的第一小的数变成m1
            if(haffTree[j].weight < m1 && haffTree[j].flag == 0) {
                m2 = m1;
                x2 = x1;
                m1 = haffTree[j].weight;
                x1 = j;
            }else if(haffTree[j].weight < m2 && haffTree[j].flag == 0) {
                //如果有比第2小数m2小的数,则替换m2
                m2 = haffTree[j].weight;
                x2 = j;
            }
        }
        //修改双亲结点的值
        haffTree[n+i].weight = m1 + m2;
        haffTree[n+i].leftChild = x1;
        haffTree[n+i].rightChild = x2;
        haffTree[x1].parent = n+i;
        haffTree[x2].parent = n+i;
        haffTree[x1].flag = 1;
        haffTree[x2].flag = 1;
    }
}

哈夫曼编码

我们将哈夫曼树的左边分支当做0,右边分支当做1,这一样一来从根结点到每一个叶子结点的路径,都可以等价为一段二进制编码。

上图中:
a对应的哈夫曼编码为:0
b对应的哈夫曼编码为:10
c对应的哈夫曼编码为:110
d对应的哈夫曼编码为:111

哈夫曼code的结构如下:

typedef struct Code//存放哈夫曼编码的数据元素结构
{
    int bit[MaxBit];//数组
    int start;  //编码的起始下标
    int weight;//字符的权值
}Code;

生成对应的哈夫曼code

/*
 哈夫曼编码
 由n个结点的哈夫曼树haffTree构造哈夫曼编码haffCode
 //{2,4,5,7}
 */
void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[]) {
    Code *cd = (Code*)malloc(sizeof(Code));
    int child,parent;
    for(int i = 0; i < n; i++) {
        cd->start = 0;
        child = i;
        cd->weight = haffTree[child].weight;
        parent = haffTree[child].parent;
        //从叶子结点开始,寻找对应的哈夫曼code
        while(parent!=-1) {
            if(haffTree[parent].leftChild == child) {
                cd->bit[cd->start++] = 0;
            }else {
                cd->bit[cd->start++] = 1;
            }
            child = parent;
            parent = haffTree[child].parent;
        }
        //得到的code是逆序的,需要转换过来
        for(int j = cd->start-1; j>=0; j--) {
            int tmp = cd->start-1-j;
            haffCode[i].bit[tmp] = cd->bit[j];
        }
        //把cd中的数据赋值到haffCode[i]中.
        //保存好haffCode 的起始位以及权值;
        haffCode[i].start = cd->start;
        //保存编码对应的权值
        haffCode[i].weight = cd->weight;
    }
}

验证:

int main(int argc, const char * argv[]) {
    int weight[4] = {2,4,5,7};
    int n = 4;
    HaffNode haffTree[2*n-1];
    Code code[n];
    Haffman(weight, n, haffTree);
    HaffmanCode(haffTree, n, code);
    
    int m = 0;
    for (int i = 0; i<n; i++)
    {
        printf("Weight = %d\n",haffTree[i].weight);
        for (int j = 0; j<code[i].start; j++)
            printf("%d",code[i].bit[j]);
        m = m + code[i].weight*code[i].start;
         printf("\n");
    }
    printf("Huffman's WPS is:%d\n",m);
    return 0;
}
Weight = 2
110
Weight = 4
111
Weight = 5
10
Weight = 7
0
Huffman's WPS is:35
Program ended with exit code: 0