哈夫曼树与哈夫曼编码

1.从文件压缩到哈夫曼树

        哈夫曼树的一个典型应用是文件压缩,尤其在ZIP文件、JPEG图片以及MP3音乐文件等多种压缩格式中都有广泛应用。为了说明它的具体应用,我们可以以文本文件压缩为例。

具体应用:文本文件压缩

假设我们有一个包含大量文本的文件,其中某些字符出现频率较高,而其他字符出现频率较低。我们可以使用哈夫曼树对该文件进行压缩,具体步骤如下:

  1. 统计字符频率:扫描文本文件,统计每个字符出现的频率。例如,假设文件内容为 "this is an example of a huffman tree",统计结果如下:

    • 空格: 7
    • 'a': 4
    • 'e': 4
    • 'h': 2
    • 'f': 3
    • 'i': 2
    • 'l': 1
    • 'm': 2
    • 'n': 2
    • 'o': 1
    • 'p': 1
    • 'r': 1
    • 's': 2
    • 't': 3
    • 'u': 1
    • 'x': 1
  2. 构建哈夫曼树:根据字符频率构建哈夫曼树。例如:

    • 初始化优先队列,将每个字符和其频率作为叶节点加入队列。
    • 从优先队列中取出频率最小的两个节点,合并成一个新节点,并将新节点插回队列。
    • 重复上述步骤,直到优先队列中只剩下一个节点,即哈夫曼树的根节点。
  3. 生成哈夫曼编码:从哈夫曼树的根节点出发,遍历每个叶节点,记录从根节点到叶节点的路径,路径上的0和1构成了对应字符的哈夫曼编码。例如:

    • 空格: 00
    • 'a': 01
    • 'e': 101
    • 'h': 1100
    • 'f': 1101
    • 'i': 1110
    • 'l': 11110
    • 'm': 11111
    • 'n': 1000
    • 'o': 1001
    • 'p': 1010
    • 'r': 1011
    • 's': 11000
    • 't': 11001
    • 'u': 11010
    • 'x': 11011
  4. 压缩文件:将文本文件中的每个字符替换为对应的哈夫曼编码。例如,原始文本 "this is an example of a huffman tree" 经过编码后变为:

    1100101100 1110110110 0100 1101101 000 101 1010010111011 111010 011 0111100 0001010 1001110100 01011110011110 1100111 0001100 001110
    
  5. 存储压缩文件:将压缩后的二进制编码存储起来,同时保存哈夫曼树的结构,以便解压缩时能恢复原始文件。

通过哈夫曼编码,可以显著减少文件的大小,提高存储和传输效率。这在存储容量有限或传输带宽有限的场景中尤为重要。  

2.什么是哈夫曼树?

如上,这种带权重的处理方式会使得我们的划分效率提高。

如同这种通过权重来决定树的结点出现的顺序且能提高我们效率的树就是哈夫曼树。

WPL的定义:

哈夫曼树的定义: 

3.哈夫曼树的构造

假设我们有以下字符及其出现频率:

A: 5 B: 9 C: 12 D: 13 E: 16 F: 45

构建哈夫曼树的步骤:

1.初始化优先队列:将每个字符和其频率作为叶节点加入队列。

初始队列: [(5, 'A'), (9, 'B'), (12, 'C'), (13, 'D'), (16, 'E'), (45, 'F')]

   (5, 'A')   (9, 'B')   (12, 'C')   (13, 'D')   (16, 'E')   (45, 'F')

2.取出频率最小的两个节点:合并成一个新节点,并将新节点插回队列。

取出: (5, 'A') 和 (9, 'B') 新节点: (14, 'AB') 更新队列: [(12, 'C'), (13, 'D'), (14, 'AB'), (16, 'E'), (45, 'F')]

       (14, 'AB')
      /         \
 (5, 'A')    (9, 'B')

   (12, 'C')   (13, 'D')   (16, 'E')   (45, 'F')

3.重复步骤2:直到优先队列中只剩下一个节点。

取出: (12, 'C') 和 (13, 'D') 新节点: (25, 'CD') 更新队列: [(14, 'AB'), (16, 'E'), (25, 'CD'), (45, 'F')]

       (14, 'AB')          (25, 'CD')
      /         \        /         \
 (5, 'A')    (9, 'B') (12, 'C')   (13, 'D')

   (16, 'E')   (45, 'F')

取出: (14, 'AB') 和 (16, 'E') 新节点: (30, 'ABE') 更新队列: [(25, 'CD'), (30, 'ABE'), (45, 'F')]

           (30, 'ABE')
          /          \
       (14, 'AB')    (16, 'E')
      /         \
 (5, 'A')    (9, 'B')

       (25, 'CD')
      /         \
 (12, 'C')   (13, 'D')

   (45, 'F')

取出: (25, 'CD') 和 (30, 'ABE') 新节点: (55, 'CDABE') 更新队列: [(45, 'F'), (55, 'CDABE')]

       (55, 'CDABE')
      /             \
 (25, 'CD')        (30, 'ABE')
 /         \       /         \
(12, 'C')  (13, 'D') (14, 'AB') (16, 'E')
            /       \
        (5, 'A')   (9, 'B')

   (45, 'F')

取出: (45, 'F') 和 (55, 'CDABE') 新节点: (100, 'FCDABE') 更新队列: [(100, 'FCDABE')]

最终,优先队列中只剩下一个节点,即哈夫曼树的根节点:

根节点: (100, 'FCDABE')

               (100, 'FCDABE')
              /                \
          (45, 'F')          (55, 'CDABE')
                           /            \
                     (25, 'CD')        (30, 'ABE')
                    /        \        /        \
              (12, 'C')   (13, 'D') (14, 'AB')  (16, 'E')
                                        /   \
                                 (5, 'A')  (9, 'B')

哈夫曼编码

要从构建好的哈夫曼树中得出每个字符的哈夫曼编码,可以按照以下步骤进行:

  1. 从根节点开始遍历树:给左边的分支赋值0,右边的分支赋值1。
  2. 记录路径上的编码:每当到达一个叶节点时,路径上经过的0和1的序列就是对应字符的哈夫曼编码。

让我们基于前面构建的哈夫曼树,来得出每个字符的哈夫曼编码:

哈夫曼树的最终结构:

               (100, 'FCDABE')
              /                \
          (45, 'F')          (55, 'CDABE')
                           /            \
                     (25, 'CD')        (30, 'ABE')
                    /        \        /        \
              (12, 'C')   (13, 'D') (14, 'AB')  (16, 'E')
                                        /   \
                                 (5, 'A')  (9, 'B')

编码过程:

  • 从根节点到叶节点的路径:
    • 分支标记为0
    • 分支标记为1

得出每个字符的编码:

  1. F

    • 路径:根节点 -> 左子节点
    • 编码:0
  2. C

    • 路径:根节点 -> 右子节点 -> 左子节点 -> 左子节点
    • 编码:100
  3. D

    • 路径:根节点 -> 右子节点 -> 左子节点 -> 右子节点
    • 编码:101
  4. A

    • 路径:根节点 -> 右子节点 -> 右子节点 -> 左子节点 -> 左子节点
    • 编码:1100
  5. B

    • 路径:根节点 -> 右子节点 -> 右子节点 -> 左子节点 -> 右子节点
    • 编码:1101
  6. E

    • 路径:根节点 -> 右子节点 -> 右子节点 -> 右子节点
    • 编码:111

最终哈夫曼编码:

F: 0 C: 100 D: 101 A: 1100 B: 1101 E: 111

编码示例:

假设需要编码字符串 "FACE":

F -> 0 A -> 1100 C -> 100 E -> 111 编码结果:01100100111

通过这种方式,每个字符都得到了一个唯一的哈夫曼编码,可以用于高效的数据压缩。

代码

typedef struct TreeNode *HuffmanTree;
struct TreeNode{
    int Weight;
    HuffmanTree Left, Right;
}
HuffmanTree Huffman( MinHeap H )
{ /* 假设H->Size个权值已经存在H->Elements[]->Weight里 */
 int i; HuffmanTree T;
 BuildMinHeap(H); /*将H->Elements[]按权值调整为最小堆*/
 for (i = 1; i < H->Size; i++) { /*做H->Size-1次合并*/
     T = malloc( sizeof( struct TreeNode) ); /*建立新结点*/
     T->Left = DeleteMin(H);
     /*从最小堆中删除一个结点,作为新T的左子结点*/
     T->Right = DeleteMin(H);
     /*从最小堆中删除一个结点,作为新T的右子结点*/
     T->Weight = T->Left->Weight+T->Right->Weight;
     /*计算新权值*/
     Insert( H, T ); /*将新T插入最小堆*/
    }
 /*从最小堆中删除并返回堆中的最后一个元素,该元素即为最终构建完成的哈夫曼树的根节点*/
 /*这个根节点是由之前的合并操作生成的,包含了整个哈夫曼树的结构。返回这个根节点后,我们就可以使用它来进行哈夫曼编码或其他相关操作。*/
 T = DeleteMin(H);
 return T;
}

 

4.哈夫曼树的特点

1.没有度为一的结点

2.n个叶子结点的哈夫曼树共有2n-1个结点

3.哈夫曼树的任意非叶节点的左右子树交换后还是哈夫曼树

4.对一组权重{w1,w2,w3…},存在不同构的两颗哈夫曼树

        【例】对一组权值{ 1, 2 , 3, 3 },不同构的两棵哈夫曼树: 

 5.C语言实现哈夫曼编码示例

步骤概述

  1. 定义数据结构

    • 树节点结构体 TreeNode
    • 最小堆结构体 MinHeap
  2. 实现最小堆操作

    • 初始化堆
    • 插入节点
    • 删除最小节点
    • 构建最小堆
  3. 构建哈夫曼树

    • 使用最小堆进行合并操作,直到只剩下一个节点
  4. 生成哈夫曼编码

    • 递归遍历哈夫曼树,生成每个字符的编码
  5. 读取文件和处理输入

    • 读取字符频率数据,构建最小堆
    • 构建哈夫曼树,生成编码
    • 处理用户输入,输出对应的哈夫曼编码

代码实现

下面是完整的代码实现:

 

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

// 定义树节点结构
typedef struct TreeNode {
    char ch;
    int weight;
    struct TreeNode *left, *right;
} TreeNode;

// 定义最小堆结构
typedef struct MinHeap {
    int size;
    int capacity;
    TreeNode **elements;
} MinHeap;

// 函数声明
MinHeap *CreateMinHeap(int capacity);
void Insert(MinHeap *heap, TreeNode *node);
TreeNode *DeleteMin(MinHeap *heap);
void BuildMinHeap(MinHeap *heap);
void BuildHuffmanTree(MinHeap *heap);
void GenerateHuffmanCodes(TreeNode *root, char *code, int length, char codes[256][256]);
void FreeTree(TreeNode *root);
void PrintCodes(char codes[256][256]);

// 主函数
int main() {
    char filePath[256];
    printf("Enter the path to the frequency file: ");
    scanf("%s", filePath);

    FILE *file = fopen(filePath, "r");
    if (!file) {
        perror("Unable to open file");
        return EXIT_FAILURE;
    }

    MinHeap *heap = CreateMinHeap(256);
    char ch;
    int freq;
    while (fscanf(file, "%c %d\n", &ch, &freq) != EOF) {
        TreeNode *node = malloc(sizeof(TreeNode));
        node->ch = ch;
        node->weight = freq;
        node->left = node->right = NULL;
        Insert(heap, node);
    }
    fclose(file);

    BuildMinHeap(heap);
    BuildHuffmanTree(heap);

    char codes[256][256] = {0};
    char code[256];
    GenerateHuffmanCodes(heap->elements[0], code, 0, codes);

    PrintCodes(codes);

    char word[256];
    printf("Enter a word to encode: ");
    scanf("%s", word);
    printf("Encoded word: ");
    int i;
    for (i = 0; word[i] != '\0'; ++i) {
        printf("%s", codes[(unsigned char)word[i]]);
    }
    printf("\n");

    FreeTree(heap->elements[0]);
    free(heap->elements);
    free(heap);

    return 0;
}

// 创建最小堆
MinHeap *CreateMinHeap(int capacity) {
    MinHeap *heap = malloc(sizeof(MinHeap));
    heap->size = 0;
    heap->capacity = capacity;
    heap->elements = malloc(capacity * sizeof(TreeNode *));
    return heap;
}

// 插入节点到最小堆
void Insert(MinHeap *heap, TreeNode *node) {
    heap->elements[heap->size++] = node;
}

// 删除最小节点
TreeNode *DeleteMin(MinHeap *heap) {
    int minIndex = 0;
    int i;
    for (i = 1; i < heap->size; ++i) {
        if (heap->elements[i]->weight < heap->elements[minIndex]->weight) {
            minIndex = i;
        }
    }
    TreeNode *minNode = heap->elements[minIndex];
    heap->elements[minIndex] = heap->elements[--heap->size];
    return minNode;
}

// 调整堆
void Heapify(MinHeap *heap, int index) {
    int smallest = index;
    int left = 2 * index + 1;
    int right = 2 * index + 2;

    if (left < heap->size && heap->elements[left]->weight < heap->elements[smallest]->weight) {
        smallest = left;
    }
    if (right < heap->size && heap->elements[right]->weight < heap->elements[smallest]->weight) {
        smallest = right;
    }

    if (smallest != index) {
        TreeNode *temp = heap->elements[index];
        heap->elements[index] = heap->elements[smallest];
        heap->elements[smallest] = temp;
        Heapify(heap, smallest);
    }
}

// 建立最小堆(通过调整元素)
void BuildMinHeap(MinHeap *heap) {
	int i;
    for (i = heap->size / 2 - 1; i >= 0; --i) {
         Heapify(heap, i);
    }
}

// 构建哈夫曼树
void BuildHuffmanTree(MinHeap *heap) {
    while (heap->size > 1) {
        TreeNode *left = DeleteMin(heap);
        TreeNode *right = DeleteMin(heap);
        TreeNode *node = malloc(sizeof(TreeNode));
        node->weight = left->weight + right->weight;
        node->left = left;
        node->right = right;
        Insert(heap, node);
    }
}

// 生成哈夫曼编码
void GenerateHuffmanCodes(TreeNode *root, char *code, int length, char codes[256][256]) {
    if (!root->left && !root->right) {
        code[length] = '\0';
        strcpy(codes[(unsigned char)root->ch], code);
        return;
    }
    if (root->left) {
        code[length] = '0';
        GenerateHuffmanCodes(root->left, code, length + 1, codes);
    }
    if (root->right) {
        code[length] = '1';
        GenerateHuffmanCodes(root->right, code, length + 1, codes);
    }
}

// 释放树的内存
void FreeTree(TreeNode *root) {
    if (root) {
        FreeTree(root->left);
        FreeTree(root->right);
        free(root);
    }
}

// 打印哈夫曼编码
void PrintCodes(char codes[256][256]) {
	int i;
    for (i = 0; i < 256; ++i) {
        if (codes[i][0] != '\0') {
            printf("%c: %s\n", i, codes[i]);
        }
    }
}

 注意字母“o”我只输了一个,其哈夫曼码应该是最长的,可以验证一下。

 

 

ok! 

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值