首页 > 专题系列 > 经典问题 > 贪心算法(3)-霍夫曼编码
2014
03-26

贪心算法(3)-霍夫曼编码

霍夫曼编码是一种无损数据压缩算法。在计算机数据处理中,霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用霍夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节(byte),即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。

构建霍夫曼编码主要包括两个部分:

1)根据输入的字符串构建霍夫曼树。

2)便利霍夫曼数并给每个字符分配编码。

哈夫曼树(Huffman Tree),又叫最优二叉树,指的是对于一组具有确定权值的叶子结点的具有最小带权路径长度的二叉树。

(1)路劲(Path):从树中的一个结点到另一个结点之间的分支构成两个结点间的路径。
(2)路径长度(Path Length):路径上的分支树。
(3)树的路径长度(Path Length of Tree):从树的根结点到每个结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
(4)结点的权(Weight of  Node):在一些应用中,赋予树中结点的一个有实际意义的树。
(5)结点的带权路径长度(Weight Path Length of Node):从该结点到树的根结点的路径长度与该结点的权的乘积。
(6)树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和

构建霍夫曼树的步骤:

算法:输入是没有相同元素的字符数组(长度n)以及字符出现的频率,输出是哈夫曼树。

即假设有n个字符,则构造出得哈夫曼树有n个叶子结点。n个字符的权值(频率)分别设为w1,w2,…,wn,则哈夫曼树的构造规则为:

(1)将w1,w2,…,wn看成是有n棵树的森林(每棵树仅有一个结点);
(2)在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。

用一个例子来了解该算法:

character   Frequency
    a	        5
    b           9
    c           12
    d           13
    e           16
    f           45

第1步:将每个元素构造成一个节点,即只有一个元素的树。并构建一个最小堆,包含所有的节点,该算法用了最小堆来作为优先队列。

第2步:选取两个权值最小的节点,并添加一个权值为5+9=14的节点,作为他们的父节点。并更新最小堆,现在最小堆包含5个节点,其中4个树还是原来的节点,权值为5和9的节点合并为一个。

character           Frequency
       c               12
       d               13
     内部 节点           14
       e               16
       f               45

重复上面的步骤,直到最小堆只有一个节点。

character      Frequency
 内部节点         100

C语言实现如下:

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

#define MAX_TREE_HT 100

// 一个霍夫曼树节点
struct MinHeapNode
{
    char data;  // 输入的字符数组中的一个字符
    unsigned freq;  // 字符出现的次数
    struct MinHeapNode *left, *right; 
};

// 最小堆: 作为优先队列使用
struct MinHeap
{
    unsigned size;    // 最小堆元素的个数
    unsigned capacity;   //最大容量
    struct MinHeapNode **array;  
};

//初始化一个最小堆节点
struct MinHeapNode* newNode(char data, unsigned freq)
{
    struct MinHeapNode* temp =
          (struct MinHeapNode*) malloc(sizeof(struct MinHeapNode));
    temp->left = temp->right = NULL;
    temp->data = data;
    temp->freq = freq;
    return temp;
}

// 创建一个容量为capacity 的最小堆
struct MinHeap* createMinHeap(unsigned capacity)
{
    struct MinHeap* minHeap =
         (struct MinHeap*) malloc(sizeof(struct MinHeap));
    minHeap->size = 0;  // current size is 0
    minHeap->capacity = capacity;
    minHeap->array =
     (struct MinHeapNode**)malloc(minHeap->capacity * sizeof(struct MinHeapNode*));
    return minHeap;
}

//  swap 两个堆节点
void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b)
{
    struct MinHeapNode* t = *a;
    *a = *b;
    *b = t;
}

// 更新最小堆.
void minHeapify(struct MinHeap* minHeap, int idx)
{
    int smallest = idx;
    int left = 2 * idx + 1;
    int right = 2 * idx + 2;

    if (left < minHeap->size &&
        minHeap->array[left]->freq < minHeap->array[smallest]->freq)
      smallest = left;

    if (right < minHeap->size &&
        minHeap->array[right]->freq < minHeap->array[smallest]->freq)
      smallest = right;

    if (smallest != idx)
    {
        swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]);
        minHeapify(minHeap, smallest);
    }
}

//检测堆的大小是否为1
int isSizeOne(struct MinHeap* minHeap)
{
    return (minHeap->size == 1);
}

//取得堆中最小的节点
struct MinHeapNode* extractMin(struct MinHeap* minHeap)
{
    struct MinHeapNode* temp = minHeap->array[0];
    minHeap->array[0] = minHeap->array[minHeap->size - 1];
    --minHeap->size;
    minHeapify(minHeap, 0);
    return temp;
}

// 想最小堆中插入一个节点
void insertMinHeap(struct MinHeap* minHeap, struct MinHeapNode* minHeapNode)
{
    ++minHeap->size;
    int i = minHeap->size - 1;
    while (i && minHeapNode->freq < minHeap->array[(i - 1)/2]->freq)
    {
        minHeap->array[i] = minHeap->array[(i - 1)/2];
        i = (i - 1)/2;
    }
    minHeap->array[i] = minHeapNode;
}

//构建一个最小堆
void buildMinHeap(struct MinHeap* minHeap)
{
    int n = minHeap->size - 1;
    int i;
    for (i = (n - 1) / 2; i >= 0; --i)
        minHeapify(minHeap, i);
}

void printArr(int arr[], int n)
{
    int i;
    for (i = 0; i < n; ++i)
        printf("%d", arr[i]);
    printf("\n");
}

// 检测是否是叶子节点
int isLeaf(struct MinHeapNode* root)
{
    return !(root->left) && !(root->right) ;
}

// 创建一个容量为 size的最小堆,并插入 data[] 中的元素到最小堆
struct MinHeap* createAndBuildMinHeap(char data[], int freq[], int size)
{
    struct MinHeap* minHeap = createMinHeap(size);
    for (int i = 0; i < size; ++i)
        minHeap->array[i] = newNode(data[i], freq[i]);
    minHeap->size = size;
    buildMinHeap(minHeap);
    return minHeap;
}

// 构建霍夫曼树
struct MinHeapNode* buildHuffmanTree(char data[], int freq[], int size)
{
    struct MinHeapNode *left, *right, *top;

    // 第 1步 : 创建最小堆.  
    struct MinHeap* minHeap = createAndBuildMinHeap(data, freq, size);

    //知道最小堆只有一个元素
    while (!isSizeOne(minHeap))
    {
        // 第二步: 取到最小的两个元素
        left = extractMin(minHeap);
        right = extractMin(minHeap);

        // Step 3: 根据两个最小的节点,来创建一个新的内部节点
        // '$' 只是对内部节点的一个特殊标记,没有使用
        top = newNode('$', left->freq + right->freq);
        top->left = left;
        top->right = right;
        insertMinHeap(minHeap, top);
    }

    // 第4步: 最后剩下的一个节点即为跟节点
    return extractMin(minHeap);
}

// 打印霍夫曼编码
void printCodes(struct MinHeapNode* root, int arr[], int top)
{
    if (root->left)
    {
        arr[top] = 0;
        printCodes(root->left, arr, top + 1);
    }

    if (root->right)
    {
        arr[top] = 1;
        printCodes(root->right, arr, top + 1);
    }

    // 如果是叶子节点就打印
    if (isLeaf(root))
    {
        printf("%c: ", root->data);
        printArr(arr, top);
    }
}

// 构建霍夫曼树,并遍历打印该霍夫曼树
void HuffmanCodes(char data[], int freq[], int size)
{
   //  构建霍夫曼树
   struct MinHeapNode* root = buildHuffmanTree(data, freq, size);

   // 打印构建好的霍夫曼树
   int arr[MAX_TREE_HT], top = 0;
   printCodes(root, arr, top);
}

// 测试
int main()
{
    char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
    int freq[] = {5, 9, 12, 13, 16, 45};
    int size = sizeof(arr)/sizeof(arr[0]);
    HuffmanCodes(arr, freq, size);
    return 0;
}

输出:

f: 0
c: 100
d: 101
a: 1100
b: 1101
e: 111

 时间复杂度

O(nlogn), 其中n是字符的数量。extractMin() 调用了 2*(n-1)次,extractMin()为log(n)的复杂度。

如果输入是已经排序的,其实是有一个线性复杂度的算法,后面再讲解。

参考:http://www.geeksforgeeks.org/greedy-algorithms-set-3-huffman-coding/


  1. 我还有个问题想请教一下,就是感觉对于新手来说,递归理解起来有些困难,不知有没有什么好的方法或者什么好的建议?

  2. 第一句可以忽略不计了吧。从第二句开始分析,说明这个花色下的所有牌都会在其它里面出现,那么还剩下♠️和♦️。第三句,可以排除2和7,因为在两种花色里有。现在是第四句,因为♠️还剩下多个,只有是♦️B才能知道答案。