【数据结构】哈夫曼树的建立、编码与译码(含完整代码)

概述

哈夫曼编码可以有效的压缩数据,通常可以节省20~90%的空间,具体压缩率依赖于数据的特性。我们将待压缩数据看作字符序列,根据每个字符出现的频率(也可以是字符对应的权重),通过哈夫曼贪心算法构造出字符的最优二进制表示。

这里我们只考虑前缀码,即没有任何码字是其他码字的前缀。前缀码可以保证达到最优的数据压缩率,且不会丧失一般性。但这里不做它做出证明。接下来我们介绍一下Huffman算法

算法设计思路

哈夫曼设计了一个贪心算法来构造最优前缀码,被称为Huffman code。它的正确性证明也依赖于贪心选择性质和最优子结构。

首先我们先设计一下存储结构,采用顺序表存储,开辟两个数组,一个用于存储Huffman树的结点,另一个用于存储Huffman树各个数据对应的前缀码,代码如下:

typedef struct huffman_Node //  结点
{
     
    char data;
    int weight;
    int parent, lchild, rchild;
} Node; 

typedef struct huffman_Code //  编码
{
     
    char data;
    string code;
} Code;
bool flag[N] = {
     false};//标志结点是否已经被加入树中
Node huff_tree[N];//Huffman树
Code huff_code[N];//前缀码数组
int n;//数据个数

接着我们根据输入的数据进行Huffman树的建树过程,这里我们采用顺序表结构进行存储,假设C是一个n个字符的集合,每个字符c∈C,c.weight对应它的出现频率(或者说是它的权重),然后先将n个字符存入Huffman树的数组中,其对应下标为0-n-1。接着,采用自底向上的构造出对应的最优编码的二叉树。从C中元素出发,进行|C|-1次合并操作创建出最终的二叉树。算法主要通过找到当前集合中出现频率(权重)最小的两个对象,将其合并,当合并两个对象时,得到的新对象的频率(权重)为原来两个对象的频率(权重)之和。
代码如下:

int find_min(int m)
    /**
     * @description: 该函数意在找到森林中未被找到过的
     * 且权值为最小的那颗树,采用顺序查找来搜索,最终
     * 返回它的下标
     * @param {*int m}
     * @return {*return id}
     */
    {
     
        int mi = INT_MAX, id = -1;
        for (int i = 0; i < m; ++i)
        {
     
            if (!flag[i] && huff_tree[i].weight < mi)
            {
     
                mi = huff_tree[i].weight;
                id = i;
            }
        }
        flag[id] = true;
        return id;
    }

    void build()
    /**
     * @description: 该函数为建立huffman树函数
     * 每次从森林中选取权值最小的两颗树,将其合并
     * 为一颗新树并加入森林中,然后将这两颗树删除
     * 显然每做一次操作减少一颗树,当进行了n-1次
     * 就可以得到最终形成的huffm树
     */
    {
     
        int i, cnt = 0;
        pr("请输入字符个数:\n");
        input(n);
        pr("请输入字符:    \n");
        for (i = 0; i < n; ++i)
        {
     
            cin >> huff_tree[i].data;
            huff_tree[i].weight = (int)huff_tree[i].data;
            huff_tree[i].lchild = huff_tree[i].rchild = -1;
        }
        //  左0右1  左小右大
        i = n;
        while (1)
        {
     
            if (cnt == n - 1)
            {
     
                int ii = find_min(i);
                huff_tree[ii].parent = -1;
                break;
            }
            int i1 = find_min(i);
            int i2 = find_min(i);

            huff_tree[i].weight = huff_tree[i1].weight + huff_tree[i2].weight;
            huff_tree[i].data = ' ';
            huff_tree[i1].parent = huff_tree[i2].parent = i;
            huff_tree[i].lchild = i1;
            huff_tree[i].rchild = i2;

            ++i, ++cnt;
        }
        display_Huffman_Tree(i);
    }

    void build_code()
    /**
     * @description: 该函数为对huffm树的数据进行编码
     * 由于本程序采用顺序表结构进行存储结点,故而对存储
     * huffman树结点的数组进行顺序遍历,显然叶子结点的
     * 左右孩子均为空,且由于叶子结点为存储数据的结点
     * 便从叶子结点往回遍历,按照左0右1的规则进行编码
     * 由于这里采用从叶子遍历到根的思路,最后得出来的
     * 编码将是逆序的,这时采用reverse函数将其逆序回来
     * 然后存入编码数组中
     */
    {
     
        int i = 0;
        for (; huff_tree[i].lchild == -1 && huff_tree[i].rchild == -1; ++i) // 左0右1
        {
     
            if (i == n)
                break;
            int j = i;
            string s = "";
            huff_code[i].data = huff_tree[i].data;
            while (huff_tree[j].parent != -1) //  叶到根
            {
     
                if (huff_tree[huff_tree[j].parent].lchild == j)
                    s += '0';
                else
                    s += '1';
                j = huff_tree[j].parent;
            }
            reverse(all(s));
            huff_code[i].code = s;
        }
        display_Huffman_Code(i);
    }

上面程序采取通过for循环顺序遍历去反复提取两个频率最低的结点下i1,i2,将他们合并为一个新结点i,代替他们,结点i将i1作为左孩子,i2作为右孩子(顺序是任意的,交换左右孩子会生成一个不一样的编码,但代价完全一样).
这里先分析一下Huffman算法的时间复杂度,显然上面主要采用双重for循环进行贪心,故而时间复杂度为O(n^2);当我们假设采用最小二叉堆实现的最小优先队列Q,去进行操作,显然初始化过程花费了O(n),而在合并数据时进行n-1次操作,且每个堆操作需要O(lgn)的时间赋值,故而时间复杂度为O(nlgn);而当我们将最小二叉堆换成van Emde Boas树时,可以将运行时间减少到O(nlglgn)。

完整代码如下

/*
 * @Author: csc
 * @Date: 2020-11-27 14:03:01
 * @LastEditTime: 2020-12-04 14:37:04
 * @LastEditors: Please set LastEditors
 * @Description: In User Settings Edit
 * @FilePath: \code\data structure\huff.cpp
 */
#include <iostream>
#include <algorithm>
#include <string>
#define pr printf
#define all(x) (x).begin(), (x).end()
#define sz(x) x.size()
const int N = 110;

using namespace std;

typedef struct huffman_Node //  结点
{
     
    char data;
    int weight;
    int parent, lchild, rchild;
} Node; 

typedef struct huffman_Code //  编码
{
     
    char data;
    string code;
} Code;

class huffman
{
     
private:
    bool flag[N] = {
     false};
    Node huff_tree[N];
    Code huff_code[N];
    int n;

public:
    void display_Huffman_Tree(int n)
    {
     
        cout << "下标\t"
             << "数据\t"
             << "权重\t"
             << "双亲\t"
             << "左孩子\t"
             << "右孩子" << endl;
        for (int i = 0; i < n; i++)
            cout << i << "\t" << huff_tree[i].data << "\t" << huff_tree[i].weight << "\t" << huff_tree[i].parent << "\t" << huff_tree[i].lchild << "\t" << huff_tree[i].rchild << endl;
    }

    void display_Huffman_Code(int n)
    {
     
        for (int i = 0; i < n; i++)
            cout << huff_code[i].data << ":" << huff_code[i].code << endl;
    }

    int find_min(int m)
    /**
     * @description: 该函数意在找到森林中未被找到过的
     * 且权值为最小的那颗树,采用顺序查找来搜索,最终
     * 返回它的下标
     * @param {*int m}
     * @return {*return id}
     */
    {
     
        int mi = INT_MAX, id = -1;
        for (int i = 0; i < m; ++i)
        {
     
            if (!flag[i] && huff_tree[i].weight < mi)
            {
     
                mi = huff_tree[i].weight;
                id = i;
            }
        }
        flag[id] = true;
        return id;
    }

    void build()
    /**
     * @description: 该函数为建立huffman树函数
     * 每次从森林中选取权值最小的两颗树,将其合并
     * 为一颗新树并加入森林中,然后将这两颗树删除
     * 显然每做一次操作减少一颗树,当进行了n-1次
     * 就可以得到最终形成的huffm树
     */
    {
     
        int i, cnt = 0;
        pr("请输入字符个数:\n");
        cin >> n;
        pr("请输入字符:    \n");
        for (i = 0; i < n; ++i)
        {
     
            cin >> huff_tree[i].data;
            huff_tree[i].weight = (int)huff_tree[i].data;
            huff_tree[i].lchild = huff_tree[i].rchild = -1;
        }
        //  左0右1  左小右大
        i = n;
        while (1)
        {
     
            if (cnt == n - 1)
            {
     
                int ii = find_min(i);
                huff_tree[ii].parent = -1;
                break;
            }
            int i1 = find_min(i);
            int i2 = find_min(i);

            huff_tree[i].weight = huff_tree[i1].weight + huff_tree[i2].weight;
            huff_tree[i].data = ' ';
            huff_tree[i1].parent = huff_tree[i2].parent = i;
            huff_tree[i].lchild = i1;
            huff_tree[i].rchild = i2;

            ++i, ++cnt;
        }
        display_Huffman_Tree(i);
    }

    void build_code()
    /**
     * @description: 该函数为对huffm树的数据进行编码
     * 由于本程序采用顺序表结构进行存储结点,故而对存储
     * huffman树结点的数组进行顺序遍历,显然叶子结点的
     * 左右孩子均为空,且由于叶子结点为存储数据的结点
     * 便从叶子结点往回遍历,按照左0右1的规则进行编码
     * 由于这里采用从叶子遍历到根的思路,最后得出来的
     * 编码将是逆序的,这时采用reverse函数将其逆序回来
     * 然后存入编码数组中
     */
    {
     
        int i = 0;
        for (; huff_tree[i].lchild == -1 && huff_tree[i].rchild == -1; ++i) // 左0右1
        {
     
            if (i == n)
                break;
            int j = i;
            string s = "";
            huff_code[i].data = huff_tree[i].data;
            while (huff_tree[j].parent != -1) //  叶到根
            {
     
                if (huff_tree[huff_tree[j].parent].lchild == j)
                    s += '0';
                else
                    s += '1';
                j = huff_tree[j].parent;
            }
            reverse(all(s));
            huff_code[i].code = s;
        }
        display_Huffman_Code(i);
    }
    
    void build_Code()
    {
     
        cout << "请输入编码字符串:\n";
        string CODE;
        cin >> CODE;
        int length_code = sz(CODE);
        pr("编码结果:\n");
        for(int i = 0; i < length_code;++i)
        {
     
            for(int j = 0; j < n; ++j)
            {
     
                if(huff_code[j].data == CODE[i])
                {
     
                    cout << huff_code[j].code;
                    break;
                }
            }
        }
        pr("\n");
    }

    void decode()
    /**
     * @description: 本函数为对01字符串进行解码的函数
     * 首先进行预处理,找到根节点的存储下标,然后根据
     * 输入的01字符串进行解码,从根节点开始,若为0则往
     * 它的左子树走,若为1则往它的右子树走,直至走到
     * 叶子结点,然后将数据输出。重新从根节点出发进行解码
     */
    {
     
        int root = 0;
        while (huff_tree[root].parent != -1)
            ++root;
        string s;
        pr("请输入解码字符串:\n");
        cin >> s;
        pr("解码结果:\n");
        int i, j = root, len = sz(s);
        for (i = 0; i < len; ++i)
        {
     
            if (s[i] == '0')
                j = huff_tree[j].lchild;
            else
                j = huff_tree[j].rchild;
            if (huff_tree[j].lchild == -1 && huff_tree[j].rchild == -1)
            {
     
                cout << huff_tree[j].data;
                j = root;
            }
        }
        pr("\n");
    }
};

int main()
{
     
    int _ = 1;
    //sf(_);
    while (_--)
    {
     
        huffman t;
        t.build();
        t.build_code();
        t.build_Code();
        t.decode();
    }

    return 0;
}

这里对上面代码的编码过程和解码过程进行解释。在完成Huffman树的建立后,我们可以根据集合C中的数据连成的数据字符串进行编码,通过顺序遍历数据字符串的每个字符,在前缀码数组中找到其对应的编码,最后输出结果,该部分也可采用从根结点出发,一个字符依次查找对应的编码。而解码过程也是如此,可以根据输入的01字符串,从根节点出发,根据左0右1的原则,进行解码,输出数据。

运行结果如下
【数据结构】哈夫曼树的建立、编码与译码(含完整代码)_第1张图片
【数据结构】哈夫曼树的建立、编码与译码(含完整代码)_第2张图片

你可能感兴趣的:(数据结构与算法,数据结构,字符串,算法)