Huffman压缩真正的C++实现

  关于Huffman编码的规则不多说了。
  首先谈一下用C++实现Huffman 压缩 的过程。
  (1)进行字符频数统计,读文件一般按字节读入,每个字节是8bit,可以开一个比2^8 = 256大一些的数组,如freq[260] = {0},然后每读入一个字符,对应的freq[index]++即可。
  (2)根据统计结果构造Huffman树。可以选择二叉树,也可以选择数组存储Huffman树。这里选择了二叉树。构造过程可以使用priority_queue辅助,每次pq.top()都可以取出权值(频数)最小的节点。每取出两个最小权值的节点,就new出一个新的节点,左右孩子分别指向它们。然后把这个新节点push进优先队列。
  (3)根据Huffman树构造Huffman编码表,可以使用map容器存储。
  (4)根据编码表将文件编码,并写入到输出文件。

  只有这些,想要实现Huffman压缩文件并不够,因为压缩的文件需要解压缩。这就要求压缩文件要有特定的格式,如果只是将Huffman编码写入到压缩文件,解压程序并不知道原Huffman树是什么样的,所以无法进行Huffman编码的decode。所以我们不仅要将Huffman编码写入到文件中,也需要将Huffman树也存入到压缩文件中,使得在解压的时候能够首先重建出Huffman树。
  如何存储Huffman树,一种方法是将原字符及频数写入到文件中。如

a 5000
b 20000
c 8000
d 4000
e 27000
......

  意思是字符a在文件中出现了5000次,b出现了20000次。省略号代表对源文件编码后的Huffman编码。根据字符及其频数,跟压缩程序一样,在解压过程中我们就可以重建Huffman树。
  第二种方法是直接将字符及其Huffman编码写入到文件中,如

a 1100
b 111
c 1101
d 0
e 10
......

  因为第二种方法比较直观,可以直接根据huffman编码就左0右1的“生成”一棵Huffman树来,所以选用了第二种方法。然而读者已经发现问题了,在读这些字符及编码的时候,并不知道什么时候结束啊?所以我们可以在头部插入一个节点数,来指明在适当的时候停止读取。即在第一行插入一个数字5,如

5
a 1100
b 111
c 1101
d 0
e 10
......

  如何存储Huffman树的问题解决了,那到底如何存储Huffman编码呢?肯定不是写入01010串啊,这样编码比源文件还长,还谈什么压缩。众所周知,程序在写文件的时候,都是按Byte写入的。Huffman编码是由0和1组成的,我们可以把Huffman编码每一位看成一个bit,所以就我们在写文件的时候,要将每8位bit组成一个Byte,然后写入到文件。深入分析一下,这就带来一个问题,如果源文件的整个Huffman编码的长度并不是8的倍数怎么办?有些同学说,用0或1补齐啊。对,当然要补齐,但是又会出现新的问题。
  举个例子,假设a,b,c,d,e 这5个字符的Huffman编码就如上所示。文件尾部字符为“abcd”,对应编码即为110011111010。共12bit,前8bit为11001111,后4bit为1010,如果我们将后4个bit以0补齐,10100000。那么在解码的时候,很可能就会解码成“abcddddd”,对,就是补齐位的每一个0都解码成字符d。因为我们不知道在尾部的8bit中,哪些是补齐的0,哪些是真正的Huffman编码。
  为了解决这个问题,我们可以在头部再插入一个“补位数”,如刚刚补了4位,我们就存储一个4在压缩文件中。如

5
a 1100
b 111
c 1101
d 0
e 10
4
......

  可是在实际操作过程中,因为不可能在内存中存储大量的Huffman编码,所以我们一般是边读取源文件,边进行编码的,只有到编码的最后才能知道有多少位需要补齐。所以就得先编码一遍,计算补位数,将补位数写入,再重新进行编码,这样过程感觉很繁琐。
  第二种方法是储存一个总字符数量。这样我们就能知道解码多少个字符之后就停止,这样后面补0补1度不影响了。比如上面的例子中,一共有100000个字符,我们就存储一个100000到压缩文件中。如
  

5
a 1100
b 111
c 1101
d 0
e 10
100000
......

  这就比第一种方法好一点。但是我们又可以分析一下,总字符数量是一个不确定的东西,总有可能溢出int、long、long long。所以总感觉很危险,当然有BigInt或者自己实现的大数类可以使用。但是我们可以再思考另一个方法,使用pseudo-eof,伪结束符。
  首先什么是结束符,比如我们在读文件的时候,使用输入流的eof()函数来判断是否读文件结束。这里的eof()实际上会返回一个-1,来说明读取的结束。所以所谓结束符,就是你在正常读取的时候一定不会读到的字符。
  那么我们在正常读文件的时候,在按Byte读取的情况下,什么值不会遇到?没错,比Byte还大的字符。可是比Byte还大,那还怎么用字符表示。这里我们一定不要被字符这个东西给限制了,所谓字符就是一个8bit的二进制流,9bit能不能表示一个字符?当然能,我比你大,还不能表示你?所以我们可以把读入的字符转换成int类型,用int类型来表示所有可能出现的字符,即包括pseudo-eof在内的十进制为0~256的所有字符。比如255的unsigned char是0xFF,转化成int就是0x000000FF,实际值是不变的。那么自然pseudo-eof的int值用16进制表示就为0x00000100
  需要注意的是,这个pseudo-eof也需要Huffman编码,这样我们解码时在遇到这个“字符”时就知道解码可以结束了。既然需要Huffman编码,我们就需要手动向频数数组中插入一个“频数为1,字符id为256”的字符,然后再对所有的字符(包括pseudo-eof)进行Huffman树的构建。并在写入Huffman编码的最后,插入pseudo-eof的Huffman编码。当然,不要忘了补齐8位,此时补0补1都没什么影响了,因为解码遇到pseudo-eof时就已经停止解码了。
  写到这里我又想到一个问题,既然第二个方法中有考虑到总字符数量溢出的问题,那么每个字符的频数会不会也会溢出呢?说不定会,就不能用int freq[20]这样的数组了,这里就先不考虑了……真正投入到工业生产环境中使用肯定是需要改进的。这里就当先介绍一下实现的基本思想就是了。

  附实现的代码及运行结果。
  

/**
 * huffman.h
 */
#ifndef _huffman_h
#define _huffman_h

#include <cstdlib>
#include <string>
#include <string.h>
#include <queue>
#include <map>
#include <fstream>

using namespace std;

#define MAX_SIZE 270
#define WRITE_BUFF_SIZE 10
#define PSEUDO_EOF 256

struct Huffman_node
{
    int id; // 使用int类型,因为要插入值为256的pseudo-EOF
    unsigned int freq;
    string code;
    Huffman_node  *left, 
                  *right,
                  *parent; 
};

typedef Huffman_node* Node_ptr;

class Huffman
{
private:
    Node_ptr node_array[MAX_SIZE]; // 叶子节点数组
    Node_ptr root;  // 根节点
    int size;  // 叶子节点数
    fstream in_file, out_file; // 输入、输出文件流
    map<int, string> table;  // 字符->huffman编码映射表

    class Compare
    {
        public:
            bool operator()(const Node_ptr& c1, const Node_ptr& c2) const
            {
                return (*c1).freq > (*c2).freq;
            }
    };

    // 用于比较优先队列中元素间的顺序
    priority_queue< Node_ptr, vector<Node_ptr>, Compare > pq;

    // 根据输入文件构造包含字符及其频率的数组
    void create_node_array();

    // 根据构造好的Huffman树建立Huffman映射表
    void create_map_table(const Node_ptr node, bool left);

    // 构造优先队列
    void create_pq();

    // 构造Huffman树
    void create_huffman_tree();

    // 计算Huffman编码
    void calculate_huffman_codes();

    // 开始压缩过程
    void do_compress();

    // 从huffman编码文件中重建huffman树
    void rebuid_huffman_tree();

    // 根据重建好的huffman树解码文件
    void decode_huffman();

public:
    // 根据输入和输出流初始化对象
    Huffman(string in_file_name, string out_file_name);

    // 析构函数
    ~Huffman();

    // 压缩文件
    void compress();

    // 解压文件
    void decompress();
};

#endif
/**
 * huffman.cpp
 */
#include "huffman.h"

void Huffman::create_node_array()
{
    int i, count;
    int freq[MAX_SIZE] = {0}; // 频数统计数组
    char in_char;

    // 依次读入字符,统计数据
    while( !in_file.eof() )
    {
        in_file.get(in_char);
        // 消除最后一行的影响
        if( in_file.eof() )
            break;
        // char是有符号的,数组下标是unsigned 所以要换成unsigned char
        freq[(unsigned char)in_char]++; 
    }

    count = 0;
    for(i = 0; i < MAX_SIZE; ++i)
    {
        if(freq[i] <= 0)
            continue;
        Node_ptr node = new Huffman_node();
        node->id = i;
        node->freq = freq[i];
        node->code = "";
        node->left = NULL;
        node->right = NULL;
        node->parent = NULL;

        node_array[count++] = node;
    }
    // 插入频率为1的pseudo-EOF
    Node_ptr node = new Huffman_node();
    node->id = PSEUDO_EOF;
    node->freq = 1;
    node->code = "";
    node->left = NULL;
    node->right = NULL;
    node->parent = NULL;

    node_array[count++] = node;

    size = count;
}

void Huffman::create_map_table(const Node_ptr node, bool left)
{
    if(left)
        node->code = node->parent->code + "0";
    else
        node->code = node->parent->code + "1";

    // 如果是叶子节点,则是一个“有效”节点,加入编码表
    if(node->left == NULL && node->right == NULL)
        table[node->id] = node->code;
    else
    {
        if(node->left != NULL)
            create_map_table(node->left, true);
        if(node->right != NULL)
            create_map_table(node->right, false);
    }
}

void Huffman::create_pq()
{
    int i;

    create_node_array();

    for(i = 0; i < size; ++i)
        pq.push(node_array[i]);
}

void Huffman::create_huffman_tree()
{
    root = NULL;

    while( !pq.empty() )
    {       
        Node_ptr first = pq.top();
        pq.pop();
        if( pq.empty() )
        {
            root = first;
            break;
        }
        Node_ptr second = pq.top();
        pq.pop();
        Node_ptr new_node = new Huffman_node();
        new_node->freq = first->freq + second->freq;

        if(first->freq <= second->freq)
        {
            new_node->left = first;
            new_node->right = second;
        }
        else
        {
            new_node->left = second;
            new_node->right = first;
        }
        first->parent = new_node;
        second->parent = new_node;

        pq.push(new_node);
    }
}

void Huffman::calculate_huffman_codes()
{
    if(root == NULL)
    {
        printf("Build the huffman tree failed or no characters are counted\n");
        exit(1);
    }

    if(root->left != NULL)
        create_map_table(root->left, true);
    if(root->right != NULL)
        create_map_table(root->right, false);
}

void Huffman::do_compress()
{
    int length, i, j, byte_count;
    char in_char;
    unsigned char out_c, tmp_c;
    string code, out_string;
    map<int, string>::iterator table_it;

    // 按节点数(包括pseudo-EOF) + 哈夫曼树 + 哈夫曼编码来写入文件

    // 第1行写入节点数(int)
    out_file << size << endl;

    // 第2~(size+1)行写入huffman树,即每行写入字符+huffman编码,如"43 00100"
    for(table_it = table.begin(); table_it != table.end(); ++table_it)
    {
        out_file << table_it->first<< " "<< table_it->second<<endl;
    }

    // 第size+2行写入huffman编码
    in_file.clear();
    in_file.seekg(ios::beg);
    code.clear();
    while( !in_file.eof() )
    {
        in_file.get(in_char);
        // 消除最后一行回车的影响
        if( in_file.eof() )
            break;
        // 找到每一个字符所对应的huffman编码
        table_it = table.find((unsigned char)in_char);
        if(table_it != table.end() )
            code += table_it->second;
        else
        {
            printf("Can't find the huffman code of character %X\n", in_char);
            exit(1);
        }
        // 当总编码的长度大于预设的WRITE_BUFF_SIZE时再写入文件
        length = code.length();
        if(length > WRITE_BUFF_SIZE)
        {
            out_string.clear();
            //将huffman的01编码以二进制流写入到输出文件
            for(i = 0; i + 7 < length; i += 8)
            {
                // 每八位01转化成一个unsigned char输出
                // 不使用char,如果使用char,在移位操作的时候符号位会影响结果
                // 另外char和unsigned char相互转化二进制位并不变
                out_c = 0;
                for(j = 0; j < 8; j++)
                {
                    if('0' == code[i+j])
                        tmp_c = 0;
                    else
                        tmp_c = 1;
                    out_c += tmp_c<<(7-j);
                }
                out_string += out_c;
            }
            out_file << out_string;
            code = code.substr(i, length-i);
        }
    }

    // 已读完所有文件,先插入pseudo-EOF
    table_it = table.find(PSEUDO_EOF);
    if(table_it != table.end() )
        code += table_it->second;
    else
    {
        printf("Can't find the huffman code of pseudo-EOF\n");
        exit(1);
    }
    // 再处理尾部剩余的huffman编码
    length = code.length();
    out_c = 0;
    for(i = 0; i < length; i++)
    {
        if('0' == code[i])
            tmp_c = 0;
        else
            tmp_c = 1;
        out_c += tmp_c<<(7-(i%8));
        if(0 == (i+1) % 8 || i == length - 1)
        {
            // 每8位写入一次文件
            out_file<<out_c;
            out_c = 0;
        }
    }
}

void Huffman::rebuid_huffman_tree()
{
    int i, j, id, length;
    string code;
    Node_ptr node, tmp, new_node;

    root = new Huffman_node();
    root->left = NULL;
    root->right = NULL;
    root->parent = NULL; //解码的时候parent没什么用了,可以不用赋值,但为了安全,还是赋值为空

    in_file >> size;
    if(size > MAX_SIZE)
    {
        printf("The number of nodes is not valid, maybe the compressed file has been broken.\n");
        exit(1);
    }

    for(i = 0; i < size; ++i)
    {
        in_file >> id;
        in_file >> code;

        length = code.length();
        node = root;
        for(j = 0; j < length; ++j)
        {
            if('0' == code[j])
                tmp = node->left;
            else if('1' == code[j])
                tmp = node->right;
            else
            {
                printf("Decode error, huffman code is not made up with 0 or 1\n");
                exit(1);
            }

            // 如果到了空,则新建一个节点
            if(tmp == NULL)
            {
                new_node = new Huffman_node();
                new_node->left = NULL;
                new_node->right = NULL;
                new_node->parent = node;

                // 如果是最后一个0或1,说明到了叶子节点,给叶子节点赋相关的值
                if(j == length-1)
                {
                    new_node->id = id;
                    new_node->code = code;
                }

                if('0' == code[j])
                    node->left = new_node;
                else
                    node->right = new_node;

                tmp = new_node;
            }
            // 如果不为空,且到了该huffman编码的最后一位,这里却已经存在了一个节点,就说明
            // 原来的huffmaninman是有问题的
            else if(j == length -1)
            {
                printf("Huffman code is not valid, maybe the compressed file has been broken.\n");
                exit(1);
            }
            // 如果不为空,但该节点却已经是叶子节点,说明寻路到了其他字符的编码处,huffman编码也不对
            else if(tmp->left == NULL && tmp->right == NULL)
            {
                printf("Huffman code is not valid, maybe the compressed file has been broken.\n");
                exit(1);
            }
            node = tmp;
        }

    }
}

void Huffman::decode_huffman()
{
    bool pseudo_eof;
    int i, id;
    char in_char;
    string out_string;
    unsigned char u_char, flag;
    Node_ptr node;

    out_string.clear();
    node = root;
    pseudo_eof = false;
    in_file.get(in_char);// 跳过最后一个回车
    while(!in_file.eof() )
    {
        in_file.get(in_char);
        u_char = (unsigned char)in_char;
        flag = 0x80;
        for(i = 0; i < 8; ++i)
        {

            if(u_char & flag)
                node = node->right;
            else
                node = node->left;

            if(node->left == NULL && node->right == NULL)
            {
                id = node->id;
                if(id == PSEUDO_EOF)
                {
                    pseudo_eof = true;
                    break;
                }
                else
                {
                    // int to char是安全的,高位会被截断
                    out_string += (char)node->id;
                    node = root;
                }
            }
            flag = flag >> 1;
        }
        if(pseudo_eof)
            break;


        if(WRITE_BUFF_SIZE < out_string.length())
        {
            out_file << out_string;
            out_string.clear();
        }
    }

    if(!out_string.empty() )
        out_file << out_string;
}

Huffman::Huffman(string in_file_name, string out_file_name)
{
    in_file.open(in_file_name.c_str(), ios::in);
    if(!in_file)
    {
        printf("Open file error, path is: %s\n", in_file_name.c_str());
        exit(1);
    }

    out_file.open(out_file_name.c_str(), ios::out);
    if(!out_file)
    {
        printf("Open file error, path is: %s\n", out_file_name.c_str());
        exit(1);
    }
}

Huffman::~Huffman()
{
    in_file.close();
    out_file.close();
}

void Huffman::compress()
{
    create_pq();
    create_huffman_tree();
    calculate_huffman_codes();
    do_compress();
}

void Huffman::decompress()
{
    rebuid_huffman_tree();
    decode_huffman();
}
/**
 * main.cpp 
 */
#include <iostream>
#include <cstdio>
#include "huffman.h"
using namespace std;

int main(int argc, char *argv[])
{
    if(argc != 4)
    {
        printf("Usage:\n\t huffmancoding inputfile outputfile\n");
        exit(1);
    }

    if(0 == strcmp("-c", argv[1]))
    {
        Huffman h(argv[2], argv[3]);
        h.compress();
    }
    else if(0 == strcmp("-x", argv[1]))
    {
        Huffman h(argv[2], argv[3]);
        h.decompress();
    }
    else
    {
        printf("Usage:\n\t unkonwn command\n");
    }
    return 0;
}

  程序目录及输入文件input.txt内容:
   
Huffman目录

  编译,进行Huffman压缩:

编译运行

  可以看到,pseudo-eof(256)被编码为1000010。然后进行Huffman解压缩:
  
解压缩

  可以发现成功进行了Huffman压缩和解压缩。不过细心的同学可能又发现了,怎么压缩后的文件encode.txt反而比源文件input.txt要大,这是因为源文件本身字符数较少,储存Huffman树本身就会占一定的空间,而Huffman编码是没有多长的,然而两者加起来会使得比源文件要大。Huffman树和Huffman编码的比重将随着字符数增加而减少。当字符数变多时,压缩效果就能体现出来,比如我们随便找了一个perl代码文件input.pl,大概有6000多行。

input.pl

  重复之前的步骤,进行Huffman的压缩和解压。

decode.pl

  可以看到,压缩前的input.pl是205572 Byte,压缩后的encode.pl是128765 Byte。压缩了将近一倍。
  另外在实验过程中,如果压缩pdf、zip等等的其他格式的文件,也会出现压缩后的文件比源文件大的结果,这是因为pdf、zip这些文件不仅仅包含ASCII字符(0~127),也包含其他控制符,这将使得统计“字符”的时候,在一个字节位中,从0x000xFF都会出现,这个时候使用Huffman编码将失去压缩效果。因为这样的话等于没有编码,所有字符都将是8个bit的Huffman编码,另外再加上储存的Huffman树,自然比源文件要大。所以明确Huffman编码的使用场景也很重要。
  另外代码里面存在了内存泄露问题,像exit(1)之前未对new出来的空间进行释放等等,但由于不是常驻进程,运行完之后操作系统会进行回收,所以暂未进行处理。程序只是简单实现了Huffman压缩文件的过程。一些思想、代码结构、实现方式还有不当之处,仅供各位参考。
  

参考文献(一些国外的资料,可能需要翻墙)
[1] http://web.stanford.edu/class/archive/cs/cs106b/cs106b.1126/handouts/220%20Huffman%20Encoding.pdf
[2] http://michael.dipperstein.com/huffman/#codelen
[3] http://www.cs.duke.edu/courses/compsci201/spring14/wordpress/wp-content/uploads/2014/04/walkthrough-huffman.pdf
[4] https://www.cs.duke.edu/csed/poop/huff/info/#pseudo-eof
[5] https://www.cs.ucsb.edu/~franklin/20/assigns/Program6Huff.html
[6] http://stackoverflow.com/questions/8198801/huffman-encoding-header-eof
[7]《数据结构与算法实验实践教程》. 乔海燕,蒋爱军,高集荣,刘晓铭 . 清华大学出版社 [75-77]

  • 18
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值