哈夫曼树

151 阅读4分钟

哈夫曼树的基本概念

路径:从树中一个结点到另一个结点之间的分支构成这两个结点的路径
结点的路径长度:两结点路径上的分支数
树的路径长度(TL):从树根到每个结点的路径长度之和[结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树] 权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
结点的带权路径长度:从根结点到该结点的路径长度与该结点的权的乘积
树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和

哈夫曼树也叫最优二叉树(带权路径长度最短的[二叉树])

哈夫曼树的构造算法

哈夫曼树中权越大的叶子离根越近,用贪心算法,首先选择权值小的作叶子结点
编写了哈夫曼口诀来帮助记忆:1.构造森林全是根 2.选用量小造新树 3.删除量小小新人 4.重复2,3剩单根

  1. 根据n个给定的权值{w1,w2,w3...wn}构成n棵二叉树的森林F={T1,T2...Tn},其中Ti只有一个带权为Wi的根节点
  2. 在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根节点的权值为其左右子树上根结点的权值指和
  3. 在F中删除这两棵树,同时将新得到的二叉树加入森林中
  4. 重复(2)(3),知道森林中只有一棵树为止,这棵树即为哈夫曼树

构造哈夫曼树中需要注意:

  • 在哈夫曼树中,初始有n棵二叉树,要经过n-1次合并最终形成二叉树
  • 经过n-1次合并产生n-1个新结点,且这n-1个新阶段都是具有两个孩子的分支结点
  • 哈夫曼树共有2n-1个结点,且所有分支结点的度均不为1

构造算法的实现

采用顺序存储结构——一维结构数组
结点类型定义:

typedef struct {
    int weight;
    int parent, lch, rch;
}HTNode,*HuffmanTree;

算法实现:

void CreatHuffmanTree(HuffmanTree HT,int n){
    //初始化
    if(n <= 1) return;
    int m = 2 * n - 1;
    HT = new HTNode[m + 1];//0号元素未用,HT[m]表示根结点
    for(int i = 1; i <= m; i++){
        HT[i].lch = 0;HT[i].rch = 0;HT[i].parent = 0;
    }
    for(int i = 1; i <= n; i++) cin >> HT[i].weight;//输入weigth值
    //初始化结束,开始构建
    for(int i = n + 1; i <= m; i++){//合并产生n-1个结点
        int s1,s2;
        Select(HT,i - 1,s1,s2);
        //在HT[k](1<= k <= i - 1)中选择两个其双亲与为0,且权值最小的结点,并返回他们在HT中的序号
        HT[i].lch = s1,HT[i].rch = s2;
        HT[i].weigth = HT[s1].weigth + HT[s2].weigth;
    }
}

上述的查找两个最小值算法Select:

void Select(HuffmanTree HT, int end, int *s1, int *s2){
    int min1, min2;
    //遍历数组初始下标为 1
    int i = 1;
    //找到还没构建树的结点
    while (HT[i].parent != 0 && i <= end) {
        i++;
    }
    min1 = HT[i].weight;
    *s1 = i;

    i++;
    while (HT[i].parent != 0 && i <= end) {
        i++;
    }
    //对找到的两个结点比较大小,min2为大的,min1为小的
    if (HT[i].weight < min1) {
        min2 = min1;
        *s2 = *s1;
        min1 = HT[i].weight;
        *s1 = i;
    }
    else {
        min2 = HT[i].weight;
        *s2 = i;
    }
    //两个结点和后续的所有未构建成树的结点做比较
    for (int j = i + 1; j <= end; j++) {
        //如果有父结点,直接跳过,进行下一个
        if (HT[j].parent != 0) {
            continue;
        }
        //如果比最小的还小,将min2=min1,min1赋值新的结点的下标
        if (HT[j].weight < min1) {
            min2 = min1;
            min1 = HT[j].weight;
            *s2 = *s1;
            *s1 = j;
        }
        //如果介于两者之间,min2赋值为新的结点的位置下标
        else if (HT[j].weight >= min1 && HT[j].weight < min2) {
            min2 = HT[j].weight;
            *s2 = j;
        }
    }
}

哈夫曼编码

哈夫曼编码是可变字长编码(VLC)的一种。该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码,在计算机中,霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。

我们要从哈夫曼树中构建哈夫曼编码,可以从叶子结点往根结点遍历,这样比较方便:

void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){
    /*从叶子结点开始向上回溯求每个字符的哈夫曼编码,存储在编码表HC中*/
    HC = new char *[n + 1];//分配n个字符串编码的头指针矢量
    cd = new char[n];//分配临时存放编码的动态数组空间
    //*HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
    //char *cd = (char *)malloc(n*sizeof(char)); //存放结点哈夫曼编码的字符[串]数组
    cd[n - 1] = '\0';//结束符
    for(int i = 0; i <= n; i++){
        int start = n - 1 , c = i , f = HT[i].parent;//start:从叶子结点出发,得到的哈夫曼编码是逆序的,需要在字符串数组中逆序存放,c:当前结点在数组中的位置
        while(f != 0){//从叶子结点向上回溯,直到根结点
            start--;
            if(HT[f].lch == c) cd[start] = ‘0’;
            else cd[start] = '1';
            c = f;
            f = HT[f].parent;
        }
        HC[i] = new char[n - start];为第i个字符编码分配空间
        //(*HC)[i] = (char *)malloc((n-start)*sizeof(char));
        strcpy(HC[i],&cd[start]);
    ]
    delete cd;
}

当然你也可以从根结点往叶子结点找,这样比较麻烦,不推荐这样做。