数据结构(14)-哈夫曼树&哈夫曼编码

前言

首先先来看四个和树相关的概念:

  • 路径:从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径。
  • 路径长度:从一个结点到另一个结点所经过的“边”的数量,被我们称为两个结点之间的路径长度。
  • 树的路径长度:从根结点到每一个结点的路径长度之和。
  • 结点的带权路径长度:树的每一个结点,都可以拥有自己的“权重”(Weight),权重在不同的算法当中可以起到不同的作用。结点的带权路径长度,是指树的根结点到该结点的路径长度,和该结点权重的乘积。
  • 树的带权路径长度:所有叶子结点的带权路径长度之和,被称为树的带权路径长度,也被简称为WPL
WPL01.png

如上图所示,二叉树a中,结点A到结点B之间的路径长度为3,树的路径长度为1+1+2+2+3+3+4+4=20,树的带权路径长度为5*1+15*2+40*3+30*4+10*4=315。二叉树b中,结点A到结点B之间的路径长度为2,树的路径长度为1+2+2+3+3+1+2+2=16,树的带权路径长度为5*3+15*3+40*2+30*2+10*2=220

哈夫曼树

给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(WPL)达到最小,称这样的二叉树为哈夫曼树(Huffman Tree),也称为最优二叉树。从上面的例子可以看出,具有同样的结点和权值,二叉树b比二叉树a的树的路径长度和WPL都要小,如果在数据量比较大的计算中,其性能是更高的。那么,二叉树b是如何构造出来的呢?它是不是就是哈夫曼树呢?下面我们就来看看构建哈夫曼树的步骤:

  1. 首先把有权值的叶子结点按照从小到大的顺序排成一个有序序列:A5、E10、B15、D30、C40
  2. 取两个最小权值的结点作为新结点N1的两个子结点,需要注意的是相对较小的为左子树,此时新结点的权值为5+10=15。
  3. 比较新结点N1B15,再生成新的父结点,以此类推,最终生成新的树。
哈夫曼树1.png

计算我们构造的新二叉树的WPL40+30*2+15*3+4*5+4*10=205,比二叉树b还要小15。

图中红色字的结点即为原来的结点,黑色字的结点是新生成的结点。总结步骤如下:

  1. 根据给定的个权值{}构成棵二叉树的集合,其中每二叉棵树中只有一个带权值为的结点,其左右子树均为空。
  2. 在中选取两棵根结点权值最小的树作为左右子树构造一棵新的树,且新树的根结点的权值为左右子树根结点的权值之和。相同权值的结点再开一个子树。
  3. 在中删除这两棵树,同时将新生成的树加入中。
  4. 重复2和3步骤,一直到只含一棵树为止。

哈夫曼编码

哈夫曼树被发明出来的主要目的是解决当年远距离通信的数据传输最优化的问题。比如需传送的电报为BADCADFEED,它只用到6种字符,我们可以使用对应的二进制数来进行表示:

字母 A B C D E F
二进制字符 000 001 010 011 100 101

传输后的编码就是001 000 011 010 000 011 101 100 100 011。这种等长的编码虽然使用起来方便,但是编码结果太长,会占用过多的内存资源。如果我们对上述字母进制做一些修改:

字母 A B C D E F
二进制字符 01 1001 101 00 11 1000

此时,新生成的编码001 01 00 101 01 00 1001 11 11 00就比等长编码短了,节约了存储和传输成本。但是这种方式也有缺陷,比如一个字符的编码恰好是另一个字符编码的前缀,就会产生歧义。这时,哈夫曼编码(Huffman Coding)就登场了。它实现了两个重要的目标:

  • 任何一个字符编码,都不是其他字符编码的前缀
  • 信息编码的总长度最小

哈夫曼编码不是一套固定的编码,而是通过哈夫曼树,根据给定信息中各个字符出现的频次,动态生成最优的编码。假设需要编码的字符集为{},每个字符出现的次数为{},我们以为叶子结点,以为对应叶子结点的权值来构造一棵哈夫曼树,规定左分支为0,右分支为1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列即为该结点的字符编码,这个编码就是哈夫曼编码。

哈夫曼编码.png

代码实现

下面我们就使用顺序存储结构来实现哈夫曼树及哈夫曼编码。由于结点存在权值,且我们使用的是顺序存储结构,可以通过下标来获取到左右孩子、双亲结点。

typedef struct HaffNode {
    int weight;
    int parentIndex;        // 双亲结点下标
    int leftChildIndex;     // 左孩子下标
    int rightChildIndex;    // 右孩子下标
    int flag;               // 0: 表示该结点未合并到哈夫曼树中 1: 表示该结点已经合并到哈夫曼树中
} HaffNode;

个叶子结点的二叉树会有个结点,构建哈夫曼树的时候,由于我们使用的是顺序存储结构,我们可以将叶子结点存放在前个位置,而非叶子结点,存放在后面,使用下标来标记。

// 构建哈夫曼树
void initHaffmanTree(int weight[], int n, HaffTreeNode *haffmanTree) {
    
    // 赋初值 将前n个位置存储叶子结点
    for (int i = 0; i < 2*n-1; i++) {
        if (i < n) {
            haffmanTree[i].weight = weight[i];
        } else {
            haffmanTree[i].weight = 0;
        }
        haffmanTree[i].parentIndex = 0;
        haffmanTree[i].flag = 0;
        haffmanTree[i].leftChildIndex = -1;
        haffmanTree[i].rightChildIndex = -1;
    }
    
    // 记录两个最小权重值及其下标
    int minValue1, minValue2;
    int minValueIndex1, minValueIndex2;
    
    // 构建非叶子结点
    for (int i = 0; i < n-1; i++) {
        // 将非叶子结点的权值设为INT_MAX
        minValue1 = minValue2 = INT_MAX;
        minValueIndex1 = minValueIndex2 = 0;
        
        // 遍历 找出两个权值最小的结点
        for (int j = 0; j < n+i; j++) {
            if (haffmanTree[j].weight < minValue1 && haffmanTree[j].flag == 0) {
                minValue2 = minValue1;
                minValueIndex2 = minValueIndex1;
                minValue1 = haffmanTree[j].weight;
                minValueIndex1 = j;
            } else if (haffmanTree[j].weight < minValue2 && haffmanTree[j].flag == 0) {
                minValue2 = haffmanTree[j].weight;
                minValueIndex2 = j;
            }
        }
        
        // 将两个权值最小的结点合并
        // 将n+i的位置储存新的结点
        haffmanTree[n+i].weight = minValue1 + minValue2;
        haffmanTree[minValueIndex1].parentIndex = n + i;
        haffmanTree[minValueIndex2].parentIndex = n + i;
        haffmanTree[n+i].leftChildIndex = minValueIndex1;
        haffmanTree[n+i].rightChildIndex = minValueIndex2;
        
        // 表示该结点已经合并到哈夫曼树中
        haffmanTree[minValueIndex1].flag = 1;
        haffmanTree[minValueIndex2].flag = 1;
    }
}

生成哈夫曼编码时候,左孩子的编码记为0,右孩子的编码记为1。编码结构中首先要保存的是编码,由于编码可能存在多位,我们需要把读到第几位记录下来,另外还需要保存该字符的权值。

typedef struct HaffCode {
    int bit[4]; // 暂定编码为4位
    int end;    // 编码位置结束的下标
    int weight; // 字符的权值
} HaffCode;

// 生成哈夫曼编码
void generateHaffmanCode(HaffTreeNode *haffmanTree, int n, HaffCode codes[]) {
    HaffCode *code = (HaffCode *)malloc(sizeof(HaffCode));
    
    // 求n个叶子结点的编码
    int child, parent;
    for (int i = 0; i < n; i++) {
        code->end = 0;
        code->weight = haffmanTree[i].weight;
        
        child = i;
        parent = haffmanTree[i].parentIndex;
        
        while (parent != 0) {
            if (haffmanTree[parent].leftChildIndex == child) {
                // 左孩子编码为0
                code->bit[code->end] = 0;
            } else {
                // 右孩子编码为1
                code->bit[code->end] = 1;
            }
            
            // 进行下一轮遍历
            code->end++;
            child = parent;
            parent = haffmanTree[parent].parentIndex;
        }
        
        // 得到的结果是从叶子结点到根结点 所以需要将bit逆序
        for (int j = code->end - 1; j >= 0; j--) {
            codes[i].bit[code->end-1-j] = code->bit[j];
        }
        
        codes[i].end = code->end;
        codes[i].weight = code->weight;
    }
}

验证如下:

int weight[] = {27, 8, 15, 15, 30, 5};
int len = 6;
    
HaffTreeNode *hfTree = malloc(sizeof(HaffTreeNode) * (2*len - 1));
initHaffmanTree(weight, len, hfTree);
    
HaffCode *hfCodes = malloc(sizeof(HaffCode)*len);
generateHaffmanCode(hfTree, len, hfCodes);
for (int i = 0; i < len; i++) {
    for (int j = 0; j < hfCodes[i].end; j++) {
        printf("%d", hfCodes[i].bit[j]);
    }
    printf(" ");
}
   
// 输出
// 01 1001 101 00 11 1000

你可能感兴趣的:(数据结构(14)-哈夫曼树&哈夫曼编码)