Huffman树与Huffman编码

1 Huffman树

哈夫曼树(Huffman Tree),又称最优二叉树,是一种加权路径长度最短的二叉树。它是由David A. Huffman在其1952年的论文中提出的一种编码方法,主要用于数据压缩和编码领域。

1.1 基本概念

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

  • 权值:每个节点都与一个权重相关联,这个权重可以是字符出现的频率、概率或其他数值。
  • 带权路径长度:从根节点到某一节点的路径长度乘以该节点的权值。
  • 树的带权路径长度:所有叶子节点的带权路径长度之和。

可以用下面公式表示:
WPL=(W1L1+W2L2+W3L3+…+WnLn)

举例说明:
在这里插入图片描述
计算加权:

(Ⅰ)WPL=9*2+4*2+5*2+2*2=40
(Ⅱ)WPL=9*1+5*2+4*3+2*3=37
(Ⅲ) WPL=5*1+9*2+2*3+4*3=41

这张图片展示了三个不同的二叉树结构,其中Ⅱ是Huffman树,加权是最小的。

1.2 构造Huffman树

  1. 初始化:根据给定的n个权值,构造n棵只有根节点的二叉树,形成一个森林集合F。
    在这里插入图片描述
  2. 选取最小两棵树:在F中选取两个根节点权值最小的树作为左右子树来构造一棵新的二叉树,新二叉树的根节点的权值为左右子树根节点权值之和。
    在这里插入图片描述
  3. 更新森林集合:从F中删除这两个根节点权值最小的树,并把新构造的二叉树加入到集合F中。
    在这里插入图片描述
  4. 重复步骤2和3:直到集合F中只有一棵树为止,这棵树即为哈夫曼树。
    在这里插入图片描述

代码示例:

#include <stdio.h>
#include <stdlib.h>
#define MAX_NODES 20  // 最大节点数(根据需求调整)
// 定义哈夫曼树节点结构
typedef struct HuffmanNode {
    int weight;         // 权值
    int parent;         // 父节点索引
    int left_child;     // 左子节点索引
    int right_child;    // 右子节点索引
} HuffmanNode;
// 创建哈夫曼树函数
void buildHuffmanTree(HuffmanNode *tree, int *weights, int n) {
    if (n > MAX_NODES) {
        printf("节点数超过最大值!\n");
        return;
    }
    // 初始化所有节点
    for (int i = 0; i < 2*n-1; i++) {
        if (i < n) {
            tree[i].weight = weights[i];
        } else {
            tree[i].weight = 0;
        }
        tree[i].parent = -1;
        tree[i].left_child = -1;
        tree[i].right_child = -1;
    }
    // 开始合并节点
    for (int i = n; i < 2*n-1; i++) {
        int min1 = -1, min2 = -1;
        // 寻找两个最小权值的未合并节点
        for (int j = 0; j < i; j++) {
            if (tree[j].parent == -1) {
                if (min1 == -1 || tree[j].weight < tree[min1].weight) {
                    min2 = min1;
                    min1 = j;
                } else if (min2 == -1 || tree[j].weight < tree[min2].weight) {
                    min2 = j;
                }
            }
        }
        // 合并节点
        tree[i].weight = tree[min1].weight + tree[min2].weight;
        tree[i].left_child = min1;
        tree[i].right_child = min2;
        tree[min1].parent = i;
        tree[min2].parent = i;
    }
}
// 计算带权路径长度(WPL)
int calculateWPL(HuffmanNode *tree, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        int path_len = 0;
        int current = i;
        while (tree[current].parent != -1) {
            path_len++;
            current = tree[current].parent;
        }
        total += path_len * tree[i].weight;
    }
    return total;
}
// 前序遍历函数
void preorderTraversal(HuffmanNode *tree, int root) {
    if (root == -1) return;
    
    // 先访问当前节点
    if (tree[root].left_child == -1 && tree[root].right_child == -1) {
        printf("[Leaf:%d] ", tree[root].weight);
    } else {
        printf("[Node:%d] ", tree[root].weight);
    }
    
    // 递归遍历左子树
    preorderTraversal(tree, tree[root].left_child);
    
    // 递归遍历右子树
    preorderTraversal(tree, tree[root].right_child);
}
int main() {
    int weights[] = {18, 22, 9, 13, 25, 7};
    int n = sizeof(weights)/sizeof(weights[0]);
    
    HuffmanNode tree[MAX_NODES];
    
    // 构建哈夫曼树
    buildHuffmanTree(tree, weights, n);
    
    // 打印树形结构
    printf("哈夫曼树结构:\n");
    preorderTraversal(tree, 2*n-2);  // 根节点在数组最后位置
    
    // 计算结果验证
    int wpl = calculateWPL(tree, n);
    printf("\n带权路径长度(WPL): %d\n", wpl);
    
    return 0;
}

运行结果:

哈夫曼树结构:
[Node:94] [Node:40] [Leaf:18] [Leaf:22] [Node:54] [Leaf:25] [Node:29] [Leaf:13] [Node:16] [Leaf:7] [Leaf:9] 
带权路径长度(WPL): 233
       94
     /   \
   40     54
  /  \   /  \
18   22 25   29
             / \
            13 16
               / \
              7   9

2 Huffman编码及应用

前面一节介绍了Huffman编码的核心:构建Huffman数,本节将针对Huffman编码及应用来进行下说明。

2.1 Huffman编码的基本原理

哈夫曼编码的核心在于构建一棵哈夫曼树(也称为最优二叉树)。这棵树是根据字符在文本中出现的频率来构造的。以下是构建哈夫曼树和生成编码的主要步骤:

  1. 统计频率:首先计算文本中每个字符出现的次数。
  2. 创建节点:将每个字符及其对应的频率作为叶子节点加入优先队列(通常是一个最小堆)。
  3. 合并节点:从队列中取出两个具有最小频率的节点,创建一个新的内部节点,这个新节点的频率值等于这两个节点频率之和,并将该新节点重新插入队列中。
  4. 重复合并:不断重复上述过程,直到队列中只剩下最后一个节点,这便是哈夫曼树的根节点。
  5. 分配编码:从根节点开始遍历哈夫曼树,向左分支分配’0’,向右分支分配’1’,直到到达叶子节点,由此得到每个字符的哈夫曼编码。

2.2 应用场景

哈夫曼编码广泛应用于各种领域,特别是在需要高效数据压缩的情况下。以下是一些典型的应用场景:

  • 文件压缩:哈夫曼编码常用于文件压缩软件中,如ZIP格式就使用了哈夫曼编码作为其压缩算法的一部分。
  • 图像压缩:JPEG图像格式利用哈夫曼编码对量化后的DCT系数进行熵编码,以达到压缩的目的。
  • 视频压缩:类似地,MPEG视频压缩标准也会用到哈夫曼编码。
  • 通信系统:在网络传输中,为了提高传输效率,减少带宽占用,哈夫曼编码可以用来压缩发送的数据。
  • 其他应用:包括但不限于数据库索引优化、电报文缩短等。

2.3 实例分析

假设有一个字符串“THIS IS AN EXAMPLE FOR HUFFMAN ENCODING”,我们首先统计每个字符的频率,然后按照哈夫曼编码的方法构建哈夫曼树,并最终得到每个字符的编码。例如,如果’e’是出现最频繁的字符,那么它的编码可能会非常短,如’0’;而对于出现频率较低的字符,则会分配较长的编码,比如’1110’。

  1. 统计字符频率

首先,我们需要统计每个字符在该字符串中出现的次数。对于字符串“this is an example for huffman encoding”,其字符频率如下(假设不区分大小写)

字符空格niaefshmotcdglprux
频率6433332222111111111
  1. 创建哈夫曼树
    基于上述频率,我们可以构建哈夫曼树。这个过程涉及到将每个字符视为一个节点,并根据它们的频率进行组合。由于手工构建哈夫曼树较为复杂且容易出错,通常我们会使用编程语言来实现这一过程。不过,下面是一个简化的流程:

    1. 将每个字符及其频率作为独立的节点加入优先队列(最小堆)。
    2. 取出频率最低的两个节点,创建一个新的内部节点,其频率是这两个节点频率之和,并将新节点重新插入队列。
    3. 重复第二步直到只剩下一个节点,即为哈夫曼树的根节点。
      完整树形结构(权重(字符)编码):
      在这里插入图片描述
  2. 生成哈夫曼编码
    一旦哈夫曼树建立完成,就可以从根节点开始遍历树,给每条边分配’0’或’1’,从而为每个字符生成唯一的二进制编码。例如,向左分支可以分配’0’,向右分支可以分配’1’。

实际编码示例:
由于手动计算可能非常繁琐且容易出错,这里根据上面的Huffman树提供一个简化版的例子而不是完整的编码结果。但是,如果用程序来执行,你可能会得到类似以下的结果(注意:实际编码取决于哈夫曼树的具体结构):

字符编码字符编码
“ ”(space)111R00000
N001U00001
S0010P00010
M0101L00011
H0111T01100
O1000X01101
E1010G10010
F1011C100110
I1100D100111
A1101

我们可以根据编码长度和字符表示为数组,其中第一行是不同编码长度的数量,即:1bit、2bit、7bit、8bit的编码为0,3bit的编码有2个,4bit编码有8个,5bit编码有7个,6bit编码有2个;第二行是按照顺序的编码对应的数据。

struct huffman_table = {
    {0, 0, 2, 8, 7, 2, 0, 0},  /*Bits*/
    {' ', 'N', 'S', 'M', 'H', 'O', 'E', 'F', 'I', 'A', 'R', 'U', 'P', 'L', 'T', 'X', 'G', 'C', 'D'} /* huffman_val */
}

3 Huffman编码在JPEG中的应用

通过第二节的例子,大部分人都会可以联想到大概如何用huffman编码去压缩jpg图像了:统计色彩频率、生成huffman数、进行huffman编码 …,实际上还是比普通文本压缩复杂点,感兴趣可以自行学习下。

JPEG 图像压缩标准中使用了霍夫曼编码(Huffman Coding)作为其熵编码的一部分。在JPEG编码过程中,霍夫曼编码主要用于对量化后的DCT系数进行编码,以减少图像数据的冗余度并提高压缩效率。
对于量化、zigzag编码后续在jpeg编解码介绍,这里只对压缩使用到Huffman编码进行展示。

下面代码是将huffman表格,转换为huffman编码的算法(参考了stm jpeg驱动),感兴趣的可以自行运行,并参考Iso10918协议,有对这一部分编码说明:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;

#define JPEG_AC_HUFF_TABLE_SIZE  ((uint32_t)162) /* Huffman AC table size : 162 codes*/
#define JPEG_DC_HUFF_TABLE_SIZE  ((uint32_t)12)  /* Huffman AC table size : 12 codes*/
typedef struct
{
  /* These two fields directly represent the contents of a JPEG DHT marker */
  uint8_t Bits[16];        /*!< bits[k] = # of symbols with codes of length k bits, this parameter corresponds to BITS list in the Annex C */
  uint8_t HuffVal[162];    /*!< The symbols, in order of incremented code length, this parameter corresponds to HUFFVAL list in the Annex C */
} JPEG_ACHuffTableTypeDef;
typedef struct
{
  /* These two fields directly represent the contents of a JPEG DHT marker */
  uint8_t Bits[16];        /*!< bits[k] = # of symbols with codes of length k bits, this parameter corresponds to BITS list in the Annex C */
  uint8_t HuffVal[12];    /*!< The symbols, in order of incremented code length, this parameter corresponds to HUFFVAL list in the Annex C */
} JPEG_DCHuffTableTypeDef;
typedef struct
{
  uint8_t CodeLength[JPEG_AC_HUFF_TABLE_SIZE];      /*!< Code length  */
  uint32_t HuffmanCode[JPEG_AC_HUFF_TABLE_SIZE];    /*!< HuffmanCode */
} JPEG_AC_HuffCodeTableTypeDef;
typedef struct
{
  uint8_t CodeLength[JPEG_DC_HUFF_TABLE_SIZE];        /*!< Code length  */
  uint32_t HuffmanCode[JPEG_DC_HUFF_TABLE_SIZE];    /*!< HuffmanCode */
} JPEG_DC_HuffCodeTableTypeDef;

typedef struct
{
    uint32_t            *HUFFENC_AC0;      /*!< Internal Counter of input data */
    uint32_t            *HUFFENC_AC1;     /*!< Internal Counter of output data */
} JPEG_HandleTypeDef;
static const JPEG_DCHuffTableTypeDef JPEG_DCLUM_HuffTable =
{
  { 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 },   /*Bits*/
  { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb }           /*HUFFVAL */
};
static const JPEG_DCHuffTableTypeDef JPEG_DCCHROM_HuffTable =
{
  { 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 },  /*Bits*/
  { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb }          /*HUFFVAL */
};
static const JPEG_ACHuffTableTypeDef JPEG_ACLUM_HuffTable =
{
  { 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d },  /*Bits*/
  {
    0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,     /*HUFFVAL */
    0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
    0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
    0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
    0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
    0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
    0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
    0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
    0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
    0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
    0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
    0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
    0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
    0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
    0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
    0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
    0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
    0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
    0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
    0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
    0xf9, 0xfa
  }
};
static const JPEG_ACHuffTableTypeDef JPEG_ACCHROM_HuffTable =
{
  { 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77 },   /*Bits*/
  {
    0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,      /*HUFFVAL */
    0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
    0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
    0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
    0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
    0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
    0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
    0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
    0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
    0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
    0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
    0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
    0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
    0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
    0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
    0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
    0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
    0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
    0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
    0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
    0xf9, 0xfa
  }
};
static int JPEG_Bits_To_SizeCodes(uint8_t *Bits, uint8_t *Huffsize, uint32_t *Huffcode, uint32_t *LastK)
{
    uint32_t i;
    uint32_t p;
    uint32_t l;
    uint32_t code;
    uint32_t si;
  
    /* Figure C.1: Generation of table of Huffman code sizes */
    p = 0;
    for (l = 0; l < 16UL; l++) {
        i = (uint32_t)Bits[l];
        if ((p + i) > 256UL) {
            /* check for table overflow */
            return 1;
        }
        while (i != 0UL) {
            Huffsize[p] = (uint8_t) l + 1U;
            p++;
            i--;
        }
    }
    Huffsize[p] = 0;
    *LastK = p;
  
    /* Figure C.2: Generation of table of Huffman codes */
    code = 0;
    si = Huffsize[0];
    p = 0;
    while (Huffsize[p] != 0U) {
        while (((uint32_t) Huffsize[p]) == si) {
            Huffcode[p] = code;
            p++;
            code++;
        }
        /* code must fit in "size" bits (si), no code is allowed to be all ones*/
        if(si > 31UL) {
            return 1;
        }
        if (((uint32_t) code) >= (((uint32_t) 1) << si)) {
            return 1;
        }
        code <<= 1;
        si++;
    }
    /* Return function status */
    return 0;
}
 
static int JPEG_ACHuff_BitsVals_To_SizeCodes(JPEG_ACHuffTableTypeDef *AC_BitsValsTable, JPEG_AC_HuffCodeTableTypeDef *AC_SizeCodesTable)
{
    int error;
    uint8_t huffsize[257];
    uint32_t huffcode[257];
    uint32_t k;
    uint32_t l, lsb, msb;
    uint32_t lastK;
    error = JPEG_Bits_To_SizeCodes(AC_BitsValsTable->Bits, huffsize, huffcode, &lastK);
    if (error != 0) {
        return  error;
    }
    /* Figure C.3: Ordering procedure for encoding procedure code tables */
    k = 0;
    while (k < lastK) {
        l = AC_BitsValsTable->HuffVal[k];
        if (l == 0UL) {
            l = 160; /*l = 0x00 EOB code*/
        } else if (l == 0xF0UL) {/* l = 0xF0 ZRL code*/
            l = 161;
        } else {
            msb = (l & 0xF0UL) >> 4;
            lsb = (l & 0x0FUL);
            l = (msb * 10UL) + lsb - 1UL;
        } if (l >= JPEG_AC_HUFF_TABLE_SIZE) {
            return 1; /* Huffman Table overflow error*/
        } else {
            AC_SizeCodesTable->HuffmanCode[l] = huffcode[k];
            AC_SizeCodesTable->CodeLength[l] = huffsize[k] - 1U;
            k++;
        }
    }
    /* Return function status */
    return 0;
}
static int JPEG_Set_HuffAC_Mem(JPEG_ACHuffTableTypeDef *HuffTableAC)
{
    int error;
    JPEG_AC_HuffCodeTableTypeDef acSizeCodesTable;
    uint32_t i, lsb, msb;
    volatile uint32_t *address, *addressDef;
    error = JPEG_ACHuff_BitsVals_To_SizeCodes(HuffTableAC, &acSizeCodesTable);
    if (error != 0) {
        return  error;
    }
    for(int i = 0; i < JPEG_AC_HUFF_TABLE_SIZE; i++) {
        printf("len:%2d   code:%0#x\n",acSizeCodesTable.CodeLength[i]+1,(acSizeCodesTable.HuffmanCode[i]));
    }
    i = JPEG_AC_HUFF_TABLE_SIZE;
    while (i > 1UL) {
        i--;
        // address--;
        msb = ((uint32_t)(((uint32_t)acSizeCodesTable.CodeLength[i] & 0xFU) << 8)) | ((uint32_t)acSizeCodesTable.HuffmanCode[i] & 0xFFUL);
        i--;
        lsb = ((uint32_t)(((uint32_t)acSizeCodesTable.CodeLength[i] & 0xFU) << 8)) | ((uint32_t)acSizeCodesTable.HuffmanCode[i] & 0xFFUL);
    }
    /* msb | lsb */
    /* Return function status */
    return 0;
}
static int JPEG_Set_HuffEnc_Mem(void)
{
    int error = 0;
    error = JPEG_Set_HuffAC_Mem((JPEG_ACHuffTableTypeDef *)&JPEG_ACLUM_HuffTable);
    return error;
}
void main(void) 
{
    JPEG_Set_HuffEnc_Mem();
    return ;
}

参考:
数据结构——哈夫曼树(Huffman Tree)
数据结构与算法之Huffman tree(赫夫曼树 / 霍夫曼树 / 哈夫曼树 / 最优二叉树)
ISO/IEC 10918-1 : 1993(E)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值