ハフマン解凍コーデック実装ファイル-C ++

序文

ハフマン符号化は、貪欲な文字符号化アルゴリズムとバイナリの組み合わせで、幅広いアプリケーションのバックグラウンドを持っている、最も直感的なファイル圧縮です。本論文では、ハフマン圧縮と解凍のコーデックのファイルを実装する方法について説明し、コードを提供します。

ハフマン符号化の概念

また、最適ハフマン木木として知られているハフマン符号化と呼ばれるハフマン符号化方法によって構築最短経路長加重ツリーです。

ハフマン符号化は、通常の状況下では、ハフマンツリーによって文字「0」と「1」を実行するエンコーディングです。実装プロセスをコーディングする場合は次のノードが左の子であるならば、我々はルートノードから下方横断ハフマン木を、横断することによって、長いハフマン木などとして、非常に単純で、後の文字列を追加し、「0」その右の子は、その文字列の後に「1」に追加されます 現在のノードの終了条件がリーフノードである、文字列は文字に対応するリーフノードを符号化することによって得られます。

ハフマン木の実装

達成するためのアイデア貪欲アルゴリズムによると、文字と文字の複数の周波数は、コーディング若干短く表示され、小さな文字が少し長いコードするあまり頻繁に表示されます。ハフマン木は、この考え方に基づいて達成され、次の例では、特定のプロセス分析ハフマンツリーを作成します。文字と出現頻度に対応し、下記の表の各行は、その情報に基づいてハフマンツリーを作成することができるであろう。

文字 発生頻度 コーディング ビットの総数
A 500 1 500
B 250 01 500
C 120 001 360
D 60 0001 240
E 30 00001 150
F 20 00000 100

 

以下、文字、優先キューに周波数を持つすべての文字として各ノードは、待ち行列が各撮影の二つは、左サブツリーとして最小周波数及びB(最小周波数ノード)、新しい親ノードのノードBとノードの周波数と二つのノード、およびノー​​ドRを設定し、新しいノードRを作成します 最後に、Rは、プライオリティキューに入れました。キューの中の唯一の要素は、ハフマンツリーのルートノードがあるまで、このプロセスが繰り返されます。

 

由上分析可得,哈夫曼编码的需要的总二进制位数为 500 + 500 + 360 + 240 + 150 + 100 = 1850。上面的例子如果用等长的编码对字符进行压缩,实现起来更简单,6 个字符必须要 3 位二进制位表示,解压缩的时候每次从文本中读取 3 位二进制码就能翻译成对应的字符,如 000,001,010,011,100,101 分别表示 a,b,c,d,e,f。则需要总的二进制位数为 (500 + 250 + 120 + 60 + 30 + 20)* 3 = 2940。对比非常明显哈夫曼编码需要的总二进制位数比等长编码需要的要少很很多,这里的压缩率为 1850 / 2940 = 62%。哈夫曼编码的压缩率通常在 20% ~90% 之间。

下面代码是借助标准库的优先队列 std::priority_queque 实现哈夫曼树的代码简单实现,构造函数需要接受 afMap 入参,huffmanCode 函数是对象的唯一对外方法,哈夫曼编码的结果会写在 codeMap 里面。这部分是创建哈夫曼树的核心代码,为方便调试,我还实现了打印二叉树树形结构的功能,这里就补贴代码,有兴趣的同学可以到文末给出的 github 仓库中下载。

using uchar = unsigned char;

struct Node {
    uchar c;        
    int freq;
    Node *left;
    Node *right;
    Node(uchar _c, int f, Node *l = nullptr, Node *r = nullptr)
        : c(_c), freq(f), left(l), right(r) {}
    bool operator<(const Node &node) const { //重载,优先队列的底层数据结构std::heap是最大堆
        return freq > node.freq;
    }
};

class huffTree {
public:
    huffTree(const std::map<uchar, int>& afMap) {
        for (auto i : afMap) {
            Node n(i.first, i.second);
            q.push(n);
        }
        _makehuffTree();
    }
    ~huffTree() {
        Node node = q.top();
        _deleteTree(node.left);
        _deleteTree(node.right);
    }
    void huffmanCode(std::map<uchar, std::string>& codeMap) {
        Node node = q.top(); 
        std::string prefix;
        _huffmanCode(&node, prefix, codeMap);
    }
private:    
    static bool _isLeaf(Node* n) {
        return n->left == nullptr && n->right == nullptr;
    }
    void _deleteTree(Node* n) {
        if (!n) return ;
        _deleteTree(n->left);
        _deleteTree(n->right);
        delete n;
    }
    void _makehuffTree() {
        while (q.size() != 1) {
            Node *left = new Node(q.top()); q.pop();
            Node *right = new Node(q.top()); q.pop();
            Node node('R', left->freq + right->freq, left, right);
            q.push(node);
        }
    }
    void _huffmanCode(Node *root, std::string& prefix, 
                      std::map<uchar, std::string>& codeMap) {
        std::string tmp = prefix;
        if (root->left != nullptr) {
            prefix += '0';
            if (_isLeaf(root->left)) {
                codeMap[root->left->c] = prefix;
            } else {
                _huffmanCode(root->left, prefix, codeMap);
            }
        }
        if (root->right != nullptr) {
            prefix = tmp;
            prefix += '1';
            if (_isLeaf(root->right)) {
                codeMap[root->right->c] = prefix;
            } else {
                 _huffmanCode(root->right, prefix, codeMap);
            }
        }
    }
private:
    std::priority_queue<Node> q;
};

文件压缩实现

首先需要给出文件压缩和下面将要提到的文件解压缩的公共头文件,如下:

//得到index位的值,若index位为0,则GET_BYTE值为假,否则为真
#define GET_BYTE(vbyte, index) (((vbyte) & (1 << ((index) ^ 7))) != 0)
//index位置1
#define SET_BYTE(vbyte, index) ((vbyte) |= (1 << ((index) ^ 7)))
//index位置0
#define CLR_BYTE(vbyte, index) ((vbyte) &= (~(1 << ((index) ^ 7))))

using uchar = unsigned char;

struct fileHead {
    char flag[4];                //压缩二进制文件头部标志 ycy
    uchar alphaVariety;            //字符种类
    uchar lastValidBit;            //最后一个字节的有效位数
    char unused[10];            //待用空间
};                                //这个结构体总共占用16个字节的空间

struct alphaFreq {
    uchar alpha;                //字符,考虑到文件中有汉字,所以定义成uchar
    int freq;                    //字符出现的频度
    alphaFreq() {}
    alphaFreq(const std::pair<char, int>& x) 
      : alpha(x.first), freq(x.second) {}    
};

下面是文件压缩的代码具体实现。过程其实相对简单,理解起来不难。首先需要读取文件信息,统计每一个字符出现的次数,这里实现是从 std::map 容器以字符为 key 累加统计字符出现的次数。然后,用统计的结果 _afMap 创建哈夫曼树,得到相应的每个字符的哈夫曼编码 _codeMap。最后,就是将数据写入压缩文件,该过程需要先写入文件头部信息, 即结构体 fileHead 的内容,这部分解压缩的时候进行格式校验等需要用到。接着将 _afMap 的字符及频率数据依次写入文件中,这部分是解压缩时重新创建哈夫曼树用来译码。到这一步就依次读取源文件的每一个字符,将其对应的哈夫曼编码写进文件中去。至此压缩文件的过程结束。下面的代码不是很难,我就不加注释了。

class huffEncode {
public:
    bool encode(const char* srcFilename, const char* destFilename) {
        if (!_getAlphaFreq(srcFilename)) return false;
        huffTree htree(_afMap);
        htree.huffmanCode(_codeMap);
        return _encode(srcFilename, destFilename);
    }
private:
    int _getLastValidBit() {
        int sum = 0;
        for (auto it : _codeMap) {
            sum += it.second.size() * _afMap.at(it.first);
            sum &= 0xFF;
        }
        sum &= 0x7;
        return sum == 0 ? 8 : sum;
    }
    bool _getAlphaFreq(const char* filename) {
        uchar ch;
        std::ifstream is(filename, std::ios::binary);
        if (!is.is_open()) {
            printf("read file failed! filename: %s", filename);
            return false;
        }
        is.read((char*)&ch, sizeof(uchar));
        while (!is.eof()) {
            _afMap[ch]++;
            is.read((char*)&ch, sizeof(uchar));
        }
        is.close();
        return true;
    }
    bool _encode(const char* srcFilename, const char* destFilename) {
        uchar ch;
        uchar value;
        int bitIndex = 0;
        fileHead filehead = {'e', 'v', 'e', 'n'};
        filehead.alphaVariety = (uchar) _afMap.size();
        filehead.lastValidBit = _getLastValidBit();

        std::ifstream is(srcFilename, std::ios::binary);
        if (!is.is_open()) {
            printf("read file failed! filename: %s", srcFilename);
            return false;
        }
        std::ofstream io(destFilename, std::ios::binary);
        if (!io.is_open()) {
            printf("read file failed! filename: %s", destFilename);
            return false;
        }

        io.write((char*)&filehead, sizeof(fileHead));
        for (auto i : _afMap) {
            alphaFreq af(i);
            io.write((char*)&af, sizeof(alphaFreq));
        }

        is.read((char*)&ch, sizeof(uchar));
        while (!is.eof()) {
            std::string code = _codeMap.at(ch);
            for (auto c : code) {
                if ('0' == c) {
                    CLR_BYTE(value, bitIndex);
                } else {
                    SET_BYTE(value, bitIndex);
                }
                ++bitIndex;
                if (bitIndex >= 8) {
                    bitIndex = 0;
                    io.write((char*)&value, sizeof(uchar));
                }
            } 
            is.read((char*)&ch, sizeof(uchar));
        }

        if (bitIndex) {
            io.write((char*)&value, sizeof(uchar));
        }
        is.close();
        io.close();
        return true;
    }
private:
    std::map<uchar, int> _afMap;
    std::map<uchar, std::string> _codeMap;
};

文件解压缩实现

文件解压缩其实就是哈夫曼编码的译码过程,处理过程相对于压缩过程来说相对复杂一点,但其实就是将文件编码按照哈夫曼编码的既定规则翻译出原来对应的字符,并将字符写到文件中的过程。较为详细的过程是先读取文件头部信息,校验文件格式是否是上面压缩文件的格式(这里是flag的四个字符为even),不是则返回错误。然后根据头部信息字符种类 alphaVariety(即字符的个数)依次读取字符及其频率,并将读取的内容放到  _afMap 中,然后创建哈夫曼树,得到相应的每个字符的哈夫曼编码 _codeMap,并遍历 _codeMap 创建以字符编码为 key 的译码器 _decodeMap,主要方便是后面译码的时候根据编码获取其对应的字符。然后读取压缩文件剩余的内容,每次读取一个字节即 8 个二进制位,获取哈夫曼树根节点,用一个树节点指针pNode指向根节点,然后逐个读取二进制,每次根据二进制位的值,当值为 0 指针走左子树,当值为 1 指针走右子树,并将值添加到 std::string 类型的字符串 code 后面,直到走到叶子结点位置为止。用 code 作为 key 可在译码器 _decodeMap 中取得对应的字符,将字符写到新文件中去。然后清空 code,pNode重新指向根节点,继续走上面的流程,直到读完文件内容。文件最后一个字节的处理和描述有点不一样,需根据文件头信息的最后一位有效位 lastValidBit 进行特殊处理,这里特别提醒一下。

class huffDecode {
public:
    huffDecode() : _fileHead(nullptr), _htree(nullptr) {
        _fileHead = new fileHead();
    }
    ~huffDecode() {
        if (!_fileHead) delete _fileHead;
        if (!_htree) delete _htree;
    }
private:
    static bool _isLeaf(Node* n) {
        return n->left == nullptr && n->right == nullptr;
    }
    long _getFileSize(const char* strFileName) {
        std::ifstream in(strFileName);
        if (!in.is_open()) return 0;

        in.seekg(0, std::ios_base::end);
        std::streampos sp = in.tellg();
        in.close();
        return sp;
    }
    bool _getAlphaFreq(const char* filename) {
        std::ifstream is(filename, std::ios::binary);
        if (!is) {
            printf("read file failed! filename: %s", filename);
            return false;
        }
        
        is.read((char*)_fileHead, sizeof(fileHead));
        if (!(_fileHead->flag[0] == 'e' && 
              _fileHead->flag[1] == 'v' &&
              _fileHead->flag[2] == 'e' &&
              _fileHead->flag[3] == 'n')) {
            printf("not support this file format! filename: %s\n", filename);
            return false;
        }
        for (int i = 0; i < static_cast<int>(_fileHead->alphaVariety); ++i) {
            alphaFreq af;
            is.read((char*)&af, sizeof(af));
            _afMap.insert(std::pair<char, int>(af.alpha, af.freq));
        }
        is.close();
        return true;
    }
    bool _decode(const char* srcFilename, 
                 const char* destFilename) {
        long fileSize = _getFileSize(srcFilename);
        
        std::ifstream is(srcFilename, std::ios::binary);
        if (!is) {
            printf("read file failed! filename: %s", srcFilename);
            return false;
        }
        is.seekg(sizeof(fileHead) + sizeof(alphaFreq) * _fileHead->alphaVariety);

        Node node = _htree->getHuffTree();
        Node* pNode = &node;

        std::ofstream io(destFilename, std::ios::binary);
        if (!io) {
            printf("create file failed! filename: %s", destFilename);
            return false;
        }
        
        uchar value;
        std::string code;
        int index = 0;
        long curLocation = is.tellg();
        is.read((char*)&value, sizeof(uchar));
        while (1) {
            if (_isLeaf(pNode)) {
                uchar alpha = _decodeMap[code];
                io.write((char*)&alpha, sizeof(uchar));
                if (curLocation >= fileSize && index >= _fileHead->lastValidBit) {
                    break;
                }
                code.clear();
                pNode = &node;
            }

            if (GET_BYTE(value, index)) {
                code += '1';
                pNode = pNode->right;
            } else {
                pNode = pNode->left;
                code += '0';
            }
            if (++index >= 8) {
                index = 0;
                is.read((char*)&value, sizeof(uchar));
                curLocation = is.tellg();
            }
        }
        
        is.close();
        io.close();
        return true;
    }
public:
    bool decode(const char* srcFilename, const char* destFilename) {
        if (!_getAlphaFreq(srcFilename)) return false;
        long fileSize = _getFileSize(srcFilename);
        _htree = new huffTree(_afMap);
        _htree->watch();  
        _htree->huffmanCode(_codeMap);

        for (auto it : _codeMap) {
            _decodeMap.insert(std::pair<std::string, uchar>(it.second, it.first));
        }

        return _decode(srcFilename, destFilename);
    }
private:
    fileHead *_fileHead;
    huffTree *_htree;
    std::map<uchar, int> _afMap;
    std::map<uchar, std::string> _codeMap;
    std::map<std::string, uchar> _decodeMap;
};

总结

利用哈夫曼编解码实现文件的解压缩其实原理不是很难,但其需要用的编程知识其实相对较多,有优先队列、位运算、满二叉树、容器及文件操作等,想要实现的优雅其实不是很容易。而我在网上查到的 C++ 实现都不甚满意,所以决定自己实现,个人觉得还算比较满意,但因个人水平有限肯定会存在某些问题,请发现的朋友留言探讨。我觉得这个过程还是比较非常能锻炼自己的编程能力,作为一个小项目来练手再合适不过,不仅能够加深自己对位运算、C++标准库、二叉树及文件操作的理解,而且能够锻炼面向对象的编程思维。对了,不能忘记了,我代码实现的主要思想主要参考这位兄弟的文章,他是用 C 语言实现的,其实已经非常优雅,文章链接:https://blog.csdn.net/weixin_38214171/article/details/81626498

最后给出实现的源码链接:https://github.com/evenleo/huffman

 

おすすめ

転載: www.cnblogs.com/evenleee/p/11932122.html