基于HuffmanTree的文件压缩及解压缩

目录

 

HuffmanTree

定义

名词解释

性质

创建HuffmanTree

定义HuffmanTree节点

构造函数

参数解释

构建思想

析构函数

获取根节点

文件压缩及解压缩

哈夫曼编码

压缩思路

压缩文件格式

解压缩

完整的项目


HuffmanTree

定义

哈弗曼树是一种优化的二叉树,称为最优二叉树,是加权路径长度最小的二叉树。所谓权值在这里指的是节点中的数据。

名词解释

节点的权值:权就相当于重要程度,通过一个具体的数字来表示 
路径:在树中从一个节点到另一个节点的分支。 
路径长度:一条路径上的分支数量。 
树的路径长度:从树的根节点到每个节点的路径长度之和。 
树的带权路径长度:树中各个叶子节点的路径长度*该叶子节点的权的和。

性质

1、根节点值是所有的叶子节点值相加得到 
2、创建哈弗曼树的节点值全部在叶子节点上,而且只在叶子节点出现 
3、哈夫曼树的加权路径长度是最小的,这个是因为权值大的节点离根节点近,权值小的节点离根节点远。

创建HuffmanTree

1. 由给定的n个权值{ w1, w2, w3, … , wn}构造n棵只有根节点的二叉树森林F={T1, T2 , T3, … ,Tn},每棵二叉树Ti只有一个带权值wi的根节点,左右孩子均为空。

2. 重复以下步骤,直到F中只剩下一棵树为止

  • 在F中选取两棵根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,新二叉树根节点的权值为其左右子树根节点的权值之和
  • 在F中删除这两棵二叉树
  • 把新的二叉树加入到F中

定义HuffmanTree节点

二叉树的节点采用类模板的结构,而且用双亲孩子表示法构建链表。包括键值_val;父亲节点指针pParent,和左右孩子指针pLeft以及pRight。

template<class T>
struct HaffmanTreeNode
{
	HaffmanTreeNode(const T& val)
	:_val(val)
	, pLeft(nullptr)
	, pRight(nullptr)
	, pParent(nullptr)
	{}
	T _val;
	HaffmanTreeNode<T>* pLeft;
	HaffmanTreeNode<T>* pRight;
	HaffmanTreeNode<T>* pParent;
};

构造函数

构造函数提供了以个,是空的构造函数,防止因为没有默认构造函数产生错误。另外又写了一个构造数的方法。

CreateHaffmanTree()
		:pRoot(nullptr)
	{}

	

参数解释

vector<CharInfo> v这个数组中存储的是权值的信息。

构建思想

构建哈夫曼树的方式在上面已经提到,但是如何用代码来实现呢?这里可以借用最小堆来实现。每次取最小堆的堆顶元素第一次是作为左节点,第二次作为右节点。同时将两者出堆,接着用两者的关键值创建一个父亲节点入堆。利用循环就可以创建出来哈弗曼树了。至于为什么要用最小堆,根据前述的方法,我们需要每次取数组中的最小的两个值来构建新的节点,用最小堆的话可以分两次取堆顶数据,然后让创建的新的节点再入堆。最小堆会自动调整,不需要人为参与修改。而且堆的存储机制就是动态增长的数组,天然符合这里利用数组传参的要求。其中这里传入给最小堆的数据类型是节点指针,为什么是节点指针很好理解。如果是节点本身,那么我们就没有办法将之前创建的内容链接在一起,见下图: 

如果自己实现最小堆过于麻烦,所以这里我使用优先级队列。不过因为优先级队列默认是大队,所以必须自己写一个比较仿函数。如下 :

struct cmp
	{
		bool operator()(pNode& left, pNode& right)
		{
			return left->_val._count > right->_val._count;
		}
	};

这里需要注意的是,仿函数的参数不能够定义为const对象。需要修改。 
构建的代码如下:注意循环结束的条件是最小堆中只有一个元素,因为此时已经构建完成。

void GetHaffmanTree(std::vector<T>& v)
	{
		if (v.empty())
			return;
		std::priority_queue < pNode, std::vector<pNode>, cmp> q;
		for (size_t i = 0; i < v.size(); ++i)
		{
			if (v[i]._count != 0)
			{
				pNode node = new Node(v[i]);
				q.push(node);
			}
		}

		while (q.size() > 1)
		{
			
			pNode left = q.top();
			q.pop();
			pNode right = q.top();
			q.pop();

			pNode parent = new Node(left->_val + right->_val);
			
			parent->pLeft = left;
			parent->pRight = right;
			left->pParent = parent;
			right->pParent = parent;
			
			q.push(parent);
		}
		pRoot = q.top();
	}

析构函数

析构函数通过调用Destroy函数实现,而Destroy函数通过递归实现。

    ~CreateHaffmanTree()
    {
		Destroy(pRoot);
    }

private:
	void Destroy(pNode root)
	{
		if (nullptr == root)
			return;

		Destroy(root->pLeft);
		Destroy(root->pRight);
		delete root;
		root = nullptr;
	}

获取根节点

    pNode GetRoot()
	{
		return pRoot;
	}

文件压缩及解压缩

有了哈弗曼树,我们就可以利用哈弗曼树创建哈夫曼编码,通过哈夫曼编码可以实现文件压缩的功能。

哈夫曼编码

所谓哈夫曼编码是在哈弗曼树的基础上,定义向左的路径为零,向右的路径为一。这样定义以后,每一个叶子节点都会有一个唯一的编码值。而重要的是,在哈弗曼树中,所有叶子节点就是我们用来构建哈弗曼树的原值。

需要注意的是,哈夫曼编码不是唯一的,就算是同样的原值,因为插入的顺序不一样会导致不同的编码,但是毋庸置疑的是,每一个叶子节点一定会有唯一的编码值。

压缩思路

1. 打开被压缩文件,获取文件中每个字符串出现的总次数。
2. 以每个字符出现的总次数为权值构建huffman树。
3. 通过huffman树获取每个字符的huffman编码。
4. 读取源文件,对源文件中的每个字符使用获取的huffman编码进行改写,将改写结果写到压缩文件中,直到文件结束。

压缩文件格式

举例:ABBBCCCCCDDDDDDD

假设按照上述操作已经对源文件压缩完毕,怎么解压缩?解压缩之后文件的后缀怎么与源文件保持一致?因此:压缩文件中除了保存压缩数据外,还必须保存解压缩所需的信息。压缩文件格式:

解压缩

1. 从压缩文件中获取源文件的后缀
2. 从压缩文件中获取字符次数的总行数
3. 获取每个字符出现的次数
4. 重建huffman树

5. 解压压缩数据

a. 从压缩文件中读取一个字节的获取压缩数据ch
b. 从根节点开始,按照ch的8个比特位信息从高到低遍历huffman树:

  • 该比特位是0,取当前节点的左孩子,否则取右孩子,直到遍历到叶子节点位置,该字符就被解析成功
  • 将解压出的字符写入文件
  • 如果在遍历huffman过程中,8个比特位已经比较完毕还没有到达叶子节点,从a开始执行

c. 重复以上过程,直到所有的数据解析完毕

完整的项目

猜你喜欢

转载自blog.csdn.net/yulong__li/article/details/87861837