哈夫曼(霍夫曼)压缩的实现

本文详细介绍了哈夫曼(霍夫曼)压缩的实现过程,包括哈夫曼树的构造、编码、文件的读写及解压。通过构建哈夫曼树,使用先序遍历来生成编码字典,对文件进行压缩。在读写文件时,通过特定格式存储压缩信息,以实现高效压缩和解压。

哈夫曼压缩

哈夫曼树的构造

总体思路

将输入的字符串中出现的不同字符连同其频数作为一个整体,该整体即哈夫曼树的节点(HuffNode),其中“频数”即节点的权重(weight)。随后使用优先队列(priority_queue)存储这些节点,实现按节点权重的大小将各节点排序。

构造哈夫曼树时,先新建一个空节点作为新树的根节点,随后弹出该队列的前两个元素,分别作为该根节点的左子树和右子树,这样就新建了一棵树;随后把新建的树并入优先队列。重复该过程,当队列中仅含一个元素时,该剩余元素即最终构造好的哈夫曼树。

数据类型定义

出于学习和代码完备性的考虑,以下几种数据类型的定义中都包含有较多并不会在该应用实现过程中用到的函数。

  1. 哈夫曼树节点(HuffNode):

    template <typename T>
    class HuffNode
    {
    private:
        HuffNode<T>* left;
        HuffNode<T>* right;
        int weight;
        T elem;
    
        void DeleteHuffNode(HuffNode<T>* curr) // 递归地删除该节点所连接的所有子节点。
        {
            if (curr->IsLeaf())
            {
                delete curr;
                return;
            }
            DeleteHuffNode(curr->Left());
            curr->SetLeft(nullptr);
            DeleteHuffNode(curr->Right());
            curr->SetRight(nullptr);
            delete curr;
            return;
        }
    
    public:
        HuffNode() { left = right = nullptr; weight = 0; }
        HuffNode(const T& e, int w, HuffNode<T>* l = nullptr, HuffNode<T>* r = nullptr) : elem(e), weight(w), left(l), right(r) { }
    
        int Weight() { return weight; }
        T& Element() { return elem; }
        HuffNode<T>* Left() { return left; }
        HuffNode<T>* Right() { return right; }
    
        bool IsLeaf() { return left == nullptr && right == nullptr; }
        void SetLeft(HuffNode<T>* v) { left = v; }
        void SetRight(HuffNode<T>* v) { right = v; }
    
        void DeleteThisNode()
        {
            DeleteHuffNode(this);
        }
    };
    
  2. 哈夫曼树(HuffTree

    template <typename T>
    class HuffTree
    {
    private:
        HuffNode<T>* root;
    
    public:
        HuffTree(HuffNode<T>* r = nullptr) : root(r) { }
        HuffTree(int weight, HuffTree<T>* left, HuffTree<T>* right) { root = new HuffNode<T>(false, weight, left->Root(), right->Root()); }
        HuffTree(HuffItem<T>& item) { root = new HuffNode<T>(item.elem, item.weight); }
        ~HuffTree() { DeleteTree(); }
    
        void DeleteTree()
        {
            if (root == nullptr) return;
            root->DeleteThisNode();
            root = nullptr;
        }
    
        HuffNode<T>* Root() { return root; }
        int Weight() { return root->Weight(); }
    };
    
  3. 哈夫曼树节点的权值比较类(HuffCmp,用于构造priority_queue):

    template <typename T>
    class HuffCmp
    {
    public:
        bool operator()(HuffTree<T>* h1, HuffTree<T>* h2)
        {
            return h1->Weight() > h2->Weight();
        }
    };
    
  4. 为了更方便地构造哈夫曼树节点,这里使用如下结构体(HuffItem):

    template <typename T>
    struct HuffItem
    {
    T elem;
    int weight;
    };
    

重要功能实现

  • 根据所给的元素及其权重列表,构建哈夫曼树:

    “元素及其权重”使用定义的结构体 HuffItem 存储;“列表”使用 vector<HuffItem>

    template <typename T>
    HuffTree<T>* BuildHuffTree(vector<HuffItem<T>>& items)
    {
    	priority_queue<HuffTree<T>*, vector<HuffTree<T>* >, HuffCmp<T>> ascendingTrees;
    	for (ULL i = 0; i < items.size(); i++)
    		ascendingTrees.push(new HuffTree<T>(items[i]));
    	while (ascendingTrees.size() > 1)
    	{
    		HuffTree<T>* huffTree_newLeft = ascendingTrees.top();
    		ascendingTrees.pop();
    		HuffTree<T>* huffTree_newRight = ascendingTrees.top();
    		ascendingTrees.pop();
    		HuffTree<T>* huffTree_newTop = new HuffTree<T>(huffTree_newLeft->Weight() + huffTree_newRight->Weight(), huffTree_newLeft, huffTree_newRight);
    		ascendingTrees.push(huffTree_newTop);
    	}
    	return ascendingTrees.top();
    }
    

    其中,ascendingTrees 即存储各哈夫曼树/节点的优先队列。函数结束时(ascendingTrees 仅剩余一个元素),返回优先队列的队首元素,即构造好的哈夫曼树。

哈夫曼树编码

总体思路

使用递归的先序遍历,可以获得每一个待编码元素(叶节点)的二进制码。此处二进制码使用 vector<bool> 存储(也可以使用足够长的 char*,或者使用 string),编码字典使用哈希表 map<T, vector<bool>> 存储。

递归地考虑,将哈夫曼树的左右子树分别看成一个整体(看成一个哈弗曼树节点),那么现在编码的流程即:

  1. 先判断当前节点是不是叶节点。如果是叶节点,则将已获得的二进制码(vector<bool>)和该叶节点代表的数据值一并存到字典中。如果不是叶节点,那么:

  2. 先前往左子树,顺便向二进制码表中推入一个“0”,即false

    随后将该节点作为新的根节点,从步骤 1. 递归进行下一步。

  3. 再前往右子树,顺便向二进制码表中推入一个“1”,即true

    随后将该节点作为新的根节点,从步骤 1. 递归进行下一步。

在“向二进制码表中推入”的过程中,注意每次递归的时候传递的二进制表参数不可以是指针或引用;因为在哈夫曼树中,其实每个节点都相当于有自己的编码;所以在递归进行下一层遍历时,应将当前节点的编码“复制”到下一层继续使用。

重要功能实现

  • 根据哈夫曼树获取编码字典:

    typedef vector<bool> HuffCode;
    template <typename T>
    bool GetHuffCode(HuffNode<T>* node, HuffCode code, map<T, HuffCode>& huffMap)
    {
        if (node == nullptr) return false;
    
        if (node->IsLeaf())
        {
            huffMap[node->Element()] = code;
            return true;
        }
    
        if (node->Left() != nullptr)
        {
            HuffCode left_code = code;
            left_code.push_back(false);
            GetHuffCode(node->Left(), left_code, huffMap);
        }
        if (node->Right() != nullptr)
        {
            HuffCode right_code = code;
            right_code.push_back(true);
            GetHuffCode(node->Right(), right_code, huffMap);
        }
        return true;
    }
    

    此处使用 typedef vector<bool> HuffCode,方便使用。

    另外,此处还可以添加如下函数:

    template <typename T>
    map<T, HuffCode> HuffTree2Code(HuffTree<T>* tree)
    {
        HuffCode huff_code;
        map<T, HuffCode> huff_map;
    
        if (!GetHuffCode(tree->Root(), huff_code, huff_map)) return huff_map;
    
        return huff_map;
    }
    

    这样哈夫曼树的编码在日后使用起来将更加方便。

文件的读写

总体思路

  1. 读(读待压缩的原文件):

    使用二进制方式(ios::binary)按字节读入目标文件,并将读入的字节当做字符串,根据其中“字符”出现的种类和频数新建 vector<HuffItem> 表,再使用定义过的 BuildHuffTree(vector<HuffItem<T>>& ) 构造哈夫曼树。

  2. 写(写压缩后的文件):

    将文件分为“文件头”和“内容”两部分。其中,文件头结构如下(括号中的数字代表占用的字节数):

    [ 字符种类数(4) | 最后一字节有效位数(1) | 字符(1)&权重(4)交错串 | 压缩后的“字符”串 ]
    
    • 其中,“字符种类数”和“权重”所占的字节长度可以根据文件大小适当调整,避免文件“越压越大”的情况出现。

    • 规定好文件头中每个元素的长度,在后续的文件读取工作中就会变得更简单。

    • 存储压缩后的二进制表,可将每 8 位当做一个字符输出。

    说明:

    • “字符&权重交错串”存储的结果示例如下:

      A 4 B 1 C 25 D 3
      

      显然,上述串需要 1+4+1+4+1+4+1+4 = 20 个字节来存储。故可通过调整权重的字节长度来优化压缩后的文件大小(视被压缩文件的大小决定)。

    • “最后一字节有效位数”可作如下解释:

      因为压缩后原文件中每个“字符”的编码长度是不同的,因此最后只把这些编码堆起来,很有可能凑不成 8 的整倍数,也就是凑不成完整的字节。因此,可以将最后不足 8 位的部分用“0”(false)补全,并记录最后这一部分的原长度,也就是“最后一字节有效位数”。

      示例如下:

      假定文件内容为

      hello
      

      其对应编码如下(不唯一):

      l = 0
      o = 10
      h = 110
      e = 111
      

      那么原文件便由

      0110 1000  0110 0101  0110 1100  0110 1100  0110 1111
      

      压缩为:

      1101 1100  10
      

      明显这凑不够整字节。因此将上述编码补全为:

      1101 1100  1000 0000
      

      同时要记录原编码中最后一部分的有效长度,为 2。

    • 完整的一个压缩后的文件编码结构示例:

      仍以上述“hello”文件作为例子,结合前两点说明,可得出最后压缩好的文件应该是如下结构:

      [ 4 | 2 | e 1 h 1 l 2 o 1 | 1101 1100  1000 0000 ]
      

      可作如下解释:

      [ 文件有4种字符 | 读文件时最后一个字节只读前2位 | 4种字符及其权重是什么 | 压缩后的文件编码 ]
      

      这个文件便可将“字符种类数”和“权重”所占的字节长度设为 1,压缩率更大。

  3. 读 / 写(读 / 写压缩后 / 解压后的文件):

    仍是结合“hello”的例子,根据写压缩文件的规则,可以按如下规律读:

    读四个字节 → 读一个字节 → (读一个字节 → 读四个字节) × 4 遍 → (读一个字节) 重复直到文件最后一个字节
    

    由此便分别得到了存储的不同信息。

    说明:

    • 在读“字符&权重交错串”的时候,每读到一个字符和一个整数,就构造一个新的 HuffItem,并将其存入 vector<HuffItem<T>> 中。读取结束后,便可使用 BuildHuffTree(vector<HuffItem<T>>& ) 构造用于解码的哈夫曼树。

    • 在读“压缩后的‘字符’串”区域时,上述例子获取的是一个长度为 2 的 char 数组:{ -36, -128 }

      现将其转为 unsigned 类型,得到 { 220, 128 },随后将其转为二进制类型,同时将两个结果连续地按位存入 vector<bool>

      { 220, 128 } → { 1101 1100, 1000 0000 } → { T, T, F, T, T, T, F, F, T, F, F, F, F, F, F, F }
      

      这样以后,再读取 vector<bool> 的前 size() - (8 - 2) 位即可。在读取该 vector<bool> 的时候,遇到 true 则走向右子树,遇到 false 则走向左子树。当遇到叶节点时,输出叶节点存储的数据值(字节)。最终则得到了原来完整的文件。

重要功能实现

  1. 读原文件:

    bool ReadInFile(vector<char>& container, const char* path)
    {
    	ifstream file(path, ios::in | ios::binary);
    	if (!file.is_open())
    	{
    		cout << "  * 文件打开失败!\n";
    		return false;
    	}
    
    	ULL file_size = 0;
    	file_size = GetFileSize(file);
    
    	cout << "  * 读取文件中...\n";
    	for (ULL i = 0; i < file_size; i++)
    	{
    		char value;
    		file.seekg(i);
    		file.read(&value, 1);
    		container.push_back(value);
    	}
    	file.close();
    	return true;
    }
    
  2. 根据读入的原文件的字节构建哈夫曼节点表(将字符及其频数存储进 vector<HuffItem<T>>):

    template <typename T>
    void InsertHuffItem(vector<HuffItem<T>>& items, const T& value)
    {
    	bool have_item = false;
    	for (ULL i = 0; i < items.size(); i++)
    		if (items[i].elem == value)
    		{
    			have_item = true;
    			items[i].weight++;
    			break;
    		}
    	if (!have_item)
    	{
    		HuffItem<char> item_new;
    		item_new.elem = value;
    		item_new.weight = 1;
    		items.push_back(item_new);
    	}
    }
    
    template <typename T>
    void LoadHuffItem(vector<HuffItem<T>>& items, vector<T>& values)
    {
    	for (ULL i = 0; i < values.size(); i++)
    		InsertHuffItem(items, values[i]);
    }
    

    使用时只需调用 LoadHuffItem(vector<HuffItem<T>>& , vector<T>& ) 即可。

    随后便可使用构造好的节点表构建哈夫曼树。

  3. 每 8 位凑成一个字符:

    vector<char> BoundBits2Char(vector<bool>& bits)
    {
    	ULL pows[8] = { 1, 2, 4, 8, 16, 32, 64, 128 };
    	vector<char> chars;
    	vector<bool> bits_full = bits;
    	while (bits_full.size() % 8 != 0)
    		bits_full.push_back(false);
    
    	for (ULL i = 0; i < bits_full.size(); i += 8)
    	{
    		char ch = '\0';
    		for (int k = 0; k < 8; k++)
    		{
    			ch += pows[k] * bits_full[i + 7 - k];
    		}
    		chars.push_back(ch);
    	}
    	return chars;
    }
    

    该函数中,现将原二进制表补足为 8 的整数长度,随后开始将二进制转化为十进制,再转为字符存放到 vector<char> 中。

  4. 将一个字符拆成 8 位:

    vector<bool> UnboundChar2BitInversed(char value)
    {
    	int ivalue = (int)value;
    	if (ivalue < 0) ivalue += 256;
    
    	vector<bool> result;
    	while (ivalue != 0)
    	{
    		result.push_back(ivalue % 2 == 1);
    		ivalue /= 2;
    	}
    
    	if (result.size() <= 0)
    		for (int i = 0; i < 8; i++)
    			result.push_back(false);
    	else
    		while (result.size() % 8 != 0)
    			result.push_back(false);
    
    	return result;
    }
    

    该函数中,先将 value 转为 int 型,之后等效地去掉符号,随后便将十进制转为二进制,按位存入一个 vector<bool> 中。

    *注意:*十进制转二进制的时候,一方面结果是按从低位到高位存入 vector<bool> 中的,也就是说如果从第一个元素看向最后一个元素,实际上得到的是 ivalue 对应二进制的逆序;另一方面,结果并不是总有 8 位,比如 4 经上述转换获得的是一个长度为 3 的 vector<bool> ,即 { F, F, T } (逆序)。因此,有必要补全八位。此处一定要注意 0 % 8 == 0 这个事实,起初因为没有考虑到这一点,输出的文件常常会丢失几个字节(因为没有把 '\0' 换成 8 位的 0000 0000 而换成了 1 位的 0)。当然,一种更好的方案便是直接使用 bool[8] 存储(注意初始化为八个 false)。

    *剧透:*这里并没有将所有字符对应的二进制“连续地”存入同一个 vector<bool>,而是出于“逆序二进制”以及 vector 不支持“push_forward()”的原因,采用的一个字符返回一个 vector<bool> 的方法;同时在后面解压文件的实现中,单独有一部分代码会将这些分立的逆序表合成一个正序的大表。

一些小细节

  • 因为 ifstream 和 ofstream 库中的 write()read() 函数只支持将字符串(char*)写到文件里,因此处理文件头里面的整数(int 或其他类型)便成为需要特殊对待的事情。

    一种简单的解决办法就是使用 memcpy() 函数。用例如下:

    1. 四字节 int 存入四个元素的 char 数组:

      int a = 123;
      char* a_char = new char[sizeof(int)]; // 也就是 new char[4]
      memcpy(a_char, a, sizeof(int));
      // 最后得到的 a_char 如下:
      // { 11, 7, 0, 0 }
      // 注意:转换之后的 a_char 也是按 a 四个字节的逆序存储的。
      
    2. 四个元素的 char 数组转为四字节的 int

      char b_char[4] = {'\11', '\7', '\0', '\0'}
      int b;
      memcpy(&b, b_char, sizeof(int));
      // 最后得到 b = 123
      // 由此可见,不需要对逆序存储的 char* 做任何操作,原样交给 memcpy() 即可。
      

最终的压缩和解压函数

压缩(ZipFile(const char* src, const char* dst)

template <typename A, typename B>
void ZipFile(const char* in_path, const char* out_path)
{
	// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
	vector<char> file_cont;											// 按字节存储读入的文件内容
	if (!ReadInFile(file_cont, in_path)) return;					// 如果文件未找到,则跳出函数

	vector<HuffItem<char>> huff_items;
	LoadHuffItem(huff_items, file_cont);							// 根据文件内容 file_cont,构造哈夫曼树节点表(字符种类&频数)

	cout << "  * 构建哈夫曼树中...\n";
	HuffTree<char>* huff_tree = BuildHuffTree(huff_items);			// 构造哈夫曼树
	cout << "  * 编码中...\n";
	map<char, HuffCode> huff_code = HuffTree2Code(huff_tree);		// 生成编码表

	cout << "  * 转比特中...\n";
	int bit_num = 0;
	vector<bool> bits;
	for (ULL i = 0; i < file_cont.size(); i++)						// 根据编码表,将文件内容的每一个字节重新编码,记录到 bits 内
	{
		HuffCode cur_code = huff_code[file_cont[i]];
		for (ULL j = 0; j < cur_code.size(); j++)
		{
			bits.push_back(cur_code[j]);
			bit_num++;
		}
	}
    
    cout << "  * 转字符中...\n";
	vector<char> chars;
	chars = BoundBits2Char(bits);									// 每 8 个比特合并为一个字节

	ofstream file_out(out_path, ios::out | ios::binary);			// 开始写文件(将合并好的 chars 内元素逐一写出)

    cout << "  * 写文件中...\n";
	A char_types = huff_code.size();								// 写文件头
	char* char_types_c = new char[sizeof(A)];
	char lastByte_validBits = bits.size() % 8;
	memcpy(char_types_c, &char_types, sizeof(A));

	file_out.write(char_types_c, sizeof(A));
	file_out.write(&lastByte_validBits, sizeof(char));
	for (ULL i = 0; i < huff_items.size(); i++)
	{
		char* char_weight = new char[sieof(B)];
		// 这里的int类型可以根据原文件大小更改为short甚至char类型,
		// 这取决于原文件中每个字符出现的最大频数。
		memcpy(char_weight, &huff_items[i].weight, sizeof(B)); 
		file_out.write(&huff_items[i].elem, sizeof(char));
		file_out.write(char_weight, sizeof(B));
	}
	
	for (ULL i = 0; i < chars.size(); i++)							// 写文件内容
	{
		file_out.write(&chars[i], sizeof(char));
	}
	file_out.close();
	cout << "  # 压缩完成!\n";
}

解压(UnzipFile(const char* src, const char* dst)

template <typename A, typename B>
void UnzipFile(const char* in_path, const char* out_path)
{
	// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
	ifstream file(in_path, ios::in | ios::binary);
	if (!file.is_open())
	{
		cout << "  * 文件打开失败!\n";
		return;
	}

	ULL file_size = 0;
	file_size = GetFileSize(file);

	char* char_types = new char[sizeof(A)];
	char lastByte_validBits;
	vector<HuffItem<char>> huff_items;								// 声明几个用于记录文件头所存储信息的变量

	cout << "  * 读文件中...\n";
	int loc = 0;													// 读文件头
	file.seekg(loc);
	file.read(char_types, sizeof(A));
	loc += sizeof(A);
	file.seekg(loc);
	file.read(&lastByte_validBits, sizeof(char));
	loc++;

	A ichar_types;
	int ilastByte_validBits = (lastByte_validBits < 0 ? (lastByte_validBits + 256) : lastByte_validBits);
	memcpy(&ichar_types, char_types, sizeof(A));

	for (int t = 0; t < ichar_types; t++)
	{
		char target_char;
		B target_weight;

		char* target_weight_c = new char[sizeof(B)];
		file.seekg(loc);
		file.read(&target_char, sizeof(char));
		file.seekg(++loc);
		file.read(target_weight_c, sizeof(B));
		memcpy(&target_weight, target_weight_c, sizeof(B));

		HuffItem<char> new_item;
		new_item.elem = target_char;
		new_item.weight = target_weight;
		huff_items.push_back(new_item);
			
		loc += sizeof(B);
	}
	cout << "  * 构建哈夫曼树中...\n";
	HuffTree<char>* huff_tree = BuildHuffTree(huff_items);			// 文件头读取完毕,开始构造用于解码的哈夫曼树

	cout << "  * 解码中...\n";
	vector<bool> codes;
	for (; loc < file_size; loc++)									// 将压缩文件第四个区域的内容按字节读入
	{
		char value;
		file.seekg(loc);
		file.read(&value, sizeof(char));

		vector<bool> bit_inversed = UnboundChar2BitInversed(value);	// 每个字节转 8 位二进制
		for (ULL i = bit_inversed.size(); i >= 1; i--) // 防止死循环。
			codes.push_back(bit_inversed[i - 1]);					// 将每个字节对应的的二进制表合并入一个大表
	}

	cout << "  * 写文件中...\n";
	ofstream file_out(out_path, ios::out | ios::binary);			// 开始写出解压的文件

	HuffNode<char>* huff_ptr = huff_tree->Root();
	for (ULL i = 0; i < (ULL)codes.size() - ((ULL)8 - (ULL)ilastByte_validBits); i++) // 避免算数溢出。
	{
		if (codes[i]) huff_ptr = huff_ptr->Right();
		else huff_ptr = huff_ptr->Left();

		if (huff_ptr->IsLeaf()) // **注意输出字符与移动指针的顺序!**
		{
			file_out.write(&huff_ptr->Element(), sizeof(char));
			huff_ptr = huff_tree->Root();
			continue;
		}
	}
	file_out.close();
	cout << "  # 解压完成!\n";
}

源代码

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <queue>
#include <map>

#define ULL unsigned long long

using namespace std;

#pragma region Huffmann Tree

template <typename T>
struct HuffItem
{
	T elem;
	int weight;
};

template <typename T>
class HuffNode
{
private:
	HuffNode<T>* left;
	HuffNode<T>* right;
	int weight;
	T elem;

	void DeleteHuffNode(HuffNode<T>* curr) // 递归地删除该节点所连接的所有子节点。
	{
		if (curr->IsLeaf())
		{
			delete curr;
			return;
		}
		DeleteHuffNode(curr->Left());
		curr->SetLeft(nullptr);
		DeleteHuffNode(curr->Right());
		curr->SetRight(nullptr);
		delete curr;
		return;
	}

public:
	HuffNode() { left = right = nullptr; weight = 0; }
	HuffNode(const T& e, int w, HuffNode<T>* l = nullptr, HuffNode<T>* r = nullptr) : elem(e), weight(w), left(l), right(r) { }
	// ~HuffNode() { delete this; }

	int Weight() { return weight; }
	T& Element() { return elem; }
	HuffNode<T>* Left() { return left; }
	HuffNode<T>* Right() { return right; }

	bool IsLeaf() { return left == nullptr && right == nullptr; }
	void SetLeft(HuffNode<T>* v) { left = v; }
	void SetRight(HuffNode<T>* v) { right = v; }

	void DeleteThisNode()
	{
		DeleteHuffNode(this);
	}
};

template <typename T>
class HuffTree
{
private:
	HuffNode<T>* root;

public:
	HuffTree(HuffNode<T>* r = nullptr) : root(r) { }
	HuffTree(int weight, HuffTree<T>* left, HuffTree<T>* right) { root = new HuffNode<T>(false, weight, left->Root(), right->Root()); }
	HuffTree(HuffItem<T>& item) { root = new HuffNode<T>(item.elem, item.weight); }
	~HuffTree() { DeleteTree(); }

	void DeleteTree()
	{
		if (root == nullptr) return;
		root->DeleteThisNode();
		root = nullptr;
	}

	HuffNode<T>* Root() { return root; }
	int Weight() { return root->Weight(); }
};

template <typename T>
class HuffCmp
{
public:
	bool operator()(HuffTree<T>* h1, HuffTree<T>* h2)
	{
		return h1->Weight() > h2->Weight();
	}
};

template <typename T>
HuffTree<T>* BuildHuffTree(vector<HuffItem<T>>& items)
{
	priority_queue<HuffTree<T>*, vector<HuffTree<T>* >, HuffCmp<T>> ascendingTrees;
	for (ULL i = 0; i < items.size(); i++)
		ascendingTrees.push(new HuffTree<T>(items[i]));
	while (ascendingTrees.size() > 1)
	{
		HuffTree<T>* huffTree_newLeft = ascendingTrees.top();
		ascendingTrees.pop();
		HuffTree<T>* huffTree_newRight = ascendingTrees.top();
		ascendingTrees.pop();
		HuffTree<T>* huffTree_newTop = new HuffTree<T>(huffTree_newLeft->Weight() + huffTree_newRight->Weight(), huffTree_newLeft, huffTree_newRight);
		ascendingTrees.push(huffTree_newTop);
	}
	return ascendingTrees.top();
}

enum PrintOrder
{
	pre_order, mid_order, post_order
};

template <typename T>
void PrintHuffNode(HuffNode<T>* curr, PrintOrder order)
{
	if (curr == nullptr)
	{
		cout << "[Empty Tree]\n";
		return;
	}
	if (curr->Left() == nullptr && curr->Right() == nullptr)
	{
		cout << curr->Element();
		return;
	}

	if (order == PrintOrder::pre_order) cout << curr->Element();
	PrintHuffNode(curr->Left(), order);
	if (order == PrintOrder::mid_order) cout << curr->Element();
	PrintHuffNode(curr->Right(), order);
	if (order == PrintOrder::post_order) cout << curr->Element();
	return;
}

template <typename T>
void PrintHuffTree_PreOrder(HuffTree<T>* tree)
{
	PrintHuffNode(tree->Root(), PrintOrder::pre_order);
}

#pragma endregion

#pragma region Huffmann Coding

typedef vector<bool> HuffCode;
template <typename T>
bool GetHuffCode(HuffNode<T>* node, HuffCode code, map<T, HuffCode>& huffMap)
{
	if (node == nullptr) return false;

	if (node->IsLeaf())
	{
		huffMap[node->Element()] = code;
		return true;
	}

	if (node->Left() != nullptr)
	{
		HuffCode left_code = code;
		left_code.push_back(false);
		GetHuffCode(node->Left(), left_code, huffMap);
	}
	if (node->Right() != nullptr)
	{
		HuffCode right_code = code;
		right_code.push_back(true);
		GetHuffCode(node->Right(), right_code, huffMap);
	}
	return true;
}

template <typename T>
map<T, HuffCode> HuffTree2Code(HuffTree<T>* tree)
{
	HuffCode huff_code;
	map<T, HuffCode> huff_map;
	
	if (!GetHuffCode(tree->Root(), huff_code, huff_map)) return huff_map;

	return huff_map;
}

#pragma endregion

#pragma region Filestream Operations

template <typename T>
void InsertHuffItem(vector<HuffItem<T>>& items, const T& value)
{
	bool have_item = false;
	for (ULL i = 0; i < items.size(); i++)
		if (items[i].elem == value)
		{
			have_item = true;
			items[i].weight++;
			break;
		}
	if (!have_item)
	{
		HuffItem<char> item_new;
		item_new.elem = value;
		item_new.weight = 1;
		items.push_back(item_new);
	}
}

template <typename T>
void LoadHuffItem(vector<HuffItem<T>>& items, vector<T>& values)
{
	for (ULL i = 0; i < values.size(); i++)
		InsertHuffItem(items, values[i]);
}

ULL GetFileSize(ifstream& file)
{
	ULL res = 0;
	file.seekg(0, ios::end);
	res = file.tellg();
	return res;
}

bool ReadInFile(vector<char>& container, const char* path)
{
	ifstream file(path, ios::in | ios::binary);
	if (!file.is_open())
	{
		cout << "  * 文件打开失败!\n";
		return false;
	}

	ULL file_size = 0;
	file_size = GetFileSize(file);

	cout << "  * 读取文件中...\n";
	for (ULL i = 0; i < file_size; i++)
	{
		char value;
		file.seekg(i);
		file.read(&value, 1);
		container.push_back(value);
	}
	file.close();
	return true;
}

vector<char> BoundBits2Char(vector<bool>& bits)
{
	ULL pows[8] = { 1, 2, 4, 8, 16, 32, 64, 128 };
	vector<char> chars;
	vector<bool> bits_full = bits;
	while (bits_full.size() % 8 != 0)
		bits_full.push_back(false);

	for (ULL i = 0; i < bits_full.size(); i += 8)
	{
		char ch = '\0';
		for (int k = 0; k < 8; k++)
		{
			ch += pows[k] * bits_full[i + 7 - k];
		}
		chars.push_back(ch);
	}
	return chars;
}

vector<bool> UnboundChar2BitInversed(char value)
{
	int ivalue = (int)value;
	if (ivalue < 0) ivalue += 256;

	vector<bool> result;
	while (ivalue != 0)
	{
		result.push_back(ivalue % 2 == 1);
		ivalue /= 2;
	}

	if (result.size() <= 0)
		for (int i = 0; i < 8; i++)
			result.push_back(false);
	else
		while (result.size() % 8 != 0)
			result.push_back(false);

	return result;
}

template <typename A, typename B>
void ZipFile(const char* in_path, const char* out_path)
{
	// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
	vector<char> file_cont;
	if (!ReadInFile(file_cont, in_path)) return;

	vector<HuffItem<char>> huff_items;
	LoadHuffItem(huff_items, file_cont);

	cout << "  * 构建哈夫曼树中...\n";
	HuffTree<char>* huff_tree = BuildHuffTree(huff_items);
	cout << "  * 编码中...\n";
	map<char, HuffCode> huff_code = HuffTree2Code(huff_tree);

	cout << "  * 转比特中...\n";
	int bit_num = 0;
	vector<bool> bits;
	for (ULL i = 0; i < file_cont.size(); i++)
	{
		HuffCode cur_code = huff_code[file_cont[i]];
		for (ULL j = 0; j < cur_code.size(); j++)
		{
			bits.push_back(cur_code[j]);
			bit_num++;
		}
	}

	cout << "  * 转字符中...\n";
	vector<char> chars;
	chars = BoundBits2Char(bits);

	ofstream file_out(out_path, ios::out | ios::binary);
	
	cout << "  * 写文件中...\n";
	A char_types = huff_code.size();
	char* char_types_c = new char[sizeof(A)];
	char lastByte_validBits = bits.size() % 8;
	memcpy(char_types_c, &char_types, sizeof(A));

	file_out.write(char_types_c, sizeof(A));
	file_out.write(&lastByte_validBits, sizeof(char));
	for (ULL i = 0; i < huff_items.size(); i++)
	{
		char* char_weight = new char[sizeof(B)];
		// 这里的int类型可以根据原文件大小更改为short甚至char类型,
		// 这取决于原文件中每个字符出现的最大频数。
		memcpy(char_weight, &huff_items[i].weight, sizeof(B)); 
		file_out.write(&huff_items[i].elem, sizeof(char));
		file_out.write(char_weight, sizeof(B));
	}
	
	for (ULL i = 0; i < chars.size(); i++)
	{
		file_out.write(&chars[i], sizeof(char));
	}
	file_out.close();
	cout << "  # 压缩完成!\n";
}

template <typename A, typename B>
void UnzipFile(const char* in_path, const char* out_path)
{
	// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
	ifstream file(in_path, ios::in | ios::binary);
	if (!file.is_open())
	{
		cout << "  * 文件打开失败!\n";
		return;
	}

	ULL file_size = 0;
	file_size = GetFileSize(file);

	char* char_types = new char[sizeof(A)];
	char lastByte_validBits;
	vector<HuffItem<char>> huff_items;

	cout << "  * 读文件中...\n";
	int loc = 0;
	file.seekg(loc);
	file.read(char_types, sizeof(A));
	loc += sizeof(A);
	file.seekg(loc);
	file.read(&lastByte_validBits, sizeof(char));
	loc++;

	A ichar_types;
	int ilastByte_validBits = (lastByte_validBits < 0 ? (lastByte_validBits + 256) : lastByte_validBits);
	memcpy(&ichar_types, char_types, sizeof(A));

	for (int t = 0; t < (int)ichar_types; t++)
	{
		char target_char;
		B target_weight;

		char* target_weight_c = new char[sizeof(B)];
		file.seekg(loc);
		file.read(&target_char, sizeof(char));
		file.seekg(++loc);
		file.read(target_weight_c, sizeof(B));
		memcpy(&target_weight, target_weight_c, sizeof(B));

		HuffItem<char> new_item;
		new_item.elem = target_char;
		new_item.weight = (int)target_weight;
		huff_items.push_back(new_item);
			
		loc += sizeof(B);
	}
	cout << "  * 构建哈夫曼树中...\n";
	HuffTree<char>* huff_tree = BuildHuffTree(huff_items);

	cout << "  * 解码中...\n";
	vector<bool> codes;
	for (; loc < file_size; loc++)
	{
		char value;
		file.seekg(loc);
		file.read(&value, sizeof(char));

		vector<bool> bit_inversed = UnboundChar2BitInversed(value);
		for (ULL i = bit_inversed.size(); i >= 1; i--) // 防止死循环。
			codes.push_back(bit_inversed[i - 1]);
	}

	cout << "  * 写文件中...\n";
	ofstream file_out(out_path, ios::out | ios::binary);

	HuffNode<char>* huff_ptr = huff_tree->Root();
	for (ULL i = 0; i < (ULL)codes.size() - ((ULL)8 - (ULL)ilastByte_validBits); i++) // 避免算数溢出。
	{
		if (codes[i]) huff_ptr = huff_ptr->Right();
		else huff_ptr = huff_ptr->Left();

		if (huff_ptr->IsLeaf()) // **注意输出字符与移动指针的顺序!**
		{
			file_out.write(&huff_ptr->Element(), sizeof(char));
			huff_ptr = huff_tree->Root();
			continue;
		}
	}
	file_out.close();
	cout << "  # 解压完成!\n";
}

#pragma endregion

int main()
{
	string in_path;
	string out_path;

	char cmd, flag = 1;
	while (flag)
	{
		char tmp;
		cout << "1. 压缩文件\n2. 解压文件\n0. 退出程序\n请输入操作序号:";
		cin >> cmd;
		
		switch (cmd)
		{
		case '1':
			cout << "请输入文件目录:"; cin.get();
			getline(cin, in_path);
			out_path = in_path + ".dat";

			ZipFile<char, char>(in_path.c_str(), out_path.c_str());
			break;

		case '2':
			cout << "请输入文件目录:"; cin.get();
			getline(cin, in_path);
			out_path = in_path + ".out";

			UnzipFile<char, char>(in_path.c_str(), out_path.c_str());
			break;

		case '0':
			flag = 0;
			break;

		default: break;
		}
	}
	
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值