基于HuffmanTree的文件压缩及解压

个人博客传送门

HuffmanTree

定义

哈弗曼树是一种优化的二叉树,称为最优二叉树,是加权路径长度最小的二叉树。所谓权值在这里指的是节点中的数据。本文的哈弗曼树用数组提供数据,例如:arr[]={1,2,3,4,5,6}创建的哈弗曼树见下图:
hft
图中蓝色的节点值是数组arr提供,红色节点值是两个孩子节点值相加得到。

名词解释

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

性质

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

创建HuffmanTree

1、取出数组中最小的两个值,组成最小的一个分支,用两者的和作为两者的父亲节点并在原数组中替换两者。
2、依次循环直到数组所有符合要求的值被使用。
使用示例:arr[] = {1,2,3,4,5,6};
init

节点定义

哈弗曼树节点采用二叉链结构,包含关键值_val;指向左节点的指针_left;指向右节点的指针_right。

template <class T>
struct HuffmanTreeNode{
    T _val;
    HuffmanTreeNode<T>* _left;
    HuffmanTreeNode<T>* _right;

    HuffmanTreeNode(const T& val)
        :_val(val)
        ,_left(NULL)
        ,_right(NULL)
    {}
};
构造函数

构造函数提供了两个,一个是空的构造函数,防止因为没有默认构造函数产生错误。另一个是带参的构造函数。

参数解释

arr是一个数组名,用来提供创建哈弗曼树的数值来源;size是数组的大小,这个大小在构建最小堆的时候会用到;invalid指的是非法值,如果数组中有一些特定的值,不希望插入到哈弗曼树中,用这个值来判断。一般来说,所有的非法值是相同的。

构建的思想

构建哈夫曼树的方式在上面已经提到,但是如何用代码来实现呢?这里可以借用最小堆来实现。如果不太熟悉最小堆可以看这个传送门。每次取最小堆的堆顶元素第一次是作为左节点,第二次作为右节点。同时将两者出堆,接着用两者的关键值创建一个父亲节点入堆。利用循环就可以创建出来哈弗曼树了。至于为什么要用最小堆,根据前述的方法,我们需要每次取数组中的最小的两个值来构建新的节点,用最小堆的话可以分两次取堆顶数据,然后让创建的新的节点再入堆。最小堆会自动调整,不需要人为参与修改。而且堆的存储机制就是动态增长的数组,天然符合这里利用数组传参的要求。其中这里传入给最小堆的数据类型是节点指针,为什么是节点指针很好理解。如果是节点本身,那么我们就没有办法将之前创建的内容链接在一起,见下图:
Node*
为什么不用节点的引用呢?引用的话不太好,如果是引用,可能出现节点结构太大的情况,这样开销有点大,不划算。这里还需要解释一下,需要自己创建一个比较仿函数,因为Node是一个自定义类型,在创建最小堆的时候是没有办法知道比较方式的,仿函数如下:(其中为了书写方便,在HuffmanTree类中定义了节点Node)

typedef HuffmanTreeNode<T> Node;

struct NodeCompare{
    //const Node* left, const Node* right
    //wrong
    bool operator()(Node* left, Node* right){
        return left->_val < right->_val;
    }
};

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

HuffmanTree()
    :_root(NULL)
{}
HuffmanTree(T* arr, size_t size, const T& invalid){
    //注意这里使用的是Node*
    Heap<Node*, NodeCompare> minheap;
    for(size_t i = 0; i < size; ++i){
        if(arr[i] != invalid){
            //Node*
            minheap.Push(new Node(arr[i]));
        }
    }
    //minheap.Size()>1
    while(minheap.Size() > 1){
        Node* left = minheap.Top();
        minheap.Pop();
        Node* right = minheap.Top();
        minheap.Pop();
        Node* parent = new Node(left->_val + right->_val);
        parent->_left = left;
        parent->_right = right;
        minheap.Push(parent);
    }
    _root = minheap.Top();
}
析构函数

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

//destructor
~HuffmanTree(){
    Destroy(_root);
    _root = NULL;
}
void Destroy(Node* root){
    if(root){
        Destroy(root->_left);
        Destroy(root->_right);
        delete root;
    }
}
获取根节点函数
//GetRoot
Node* GetRoot(){
    return _root;
}
补充

因为没有实现拷贝构造和赋值运算符的重载,所以将他们都声明为私有成员变量。防止错误使用导致未知错误

文件压缩及解压缩

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

哈夫曼编码

所谓哈夫曼编码是在哈弗曼树的基础上,定义向左的路径为零,向右的路径为一。这样定义以后,每一个叶子节点都会有一个唯一的编码值。而重要的是,在哈弗曼树中,所有叶子节点就是我们用来构建哈弗曼树的原值。使用示例:arr[] = {1,2,3,4,5,6};如下图:
哈夫曼编码
需要注意的是,哈夫曼编码不是唯一的,就算是同样的原值,因为插入的顺序不一样会导致不同的编码,但是毋庸置疑的是,每一个叶子节点一定会有唯一的编码值。

压缩思路

1、扫描文件内容,统计文件中字符出现的次数。
2、利用文件出现次数构建哈弗曼树。
3、创建哈夫曼编码。
4、将创建哈弗曼树的字符及其对应出现次数写入压缩文件中,用于解压使用。
5、将哈夫曼编码替换原子符写入压缩文件。

压缩原理

举例:文件test.txt中内容:aaaaaabbbbbccccdddeef
其中字符出现的次数统计:a->6;b->5;c->4;d->3;e->2;f->1。利用次数构建的哈弗曼树和编码如下:
example
图中黑色字体是出现的字符,蓝色数字是对应出现的次数,红色字体是构建时两字符出现次数相加之和。为什么通过哈夫曼编码可以压缩文件?我们通过编码将原字符替换,只有当编码的长度超过一个字节的时候(也就是八个比特位)才会比替换之前的字符大小要大。但是出现次数多的字符的编码都十分的短,出现次数少的字符编码才会超过一个字节。这样抵消之下,肯定是相比于压缩之前文件大小要小。上例子中,写入压缩文件的压缩编码如下:
比较

压缩与解压代码分析
结构体分析

结构体CharInfo是用来存储字符、字符出现的次数、字符的编码三者的结构类型。其中将_count定义为long long类型,防止字符出现的次数超过了整型表示范围。_code用string类型;通过这个结构体可以将三者紧紧地联系在一起。其中对该结构体重载了!=、+、< 这三个运算符。在代码中都会用到。

typedef long long LongType;
struct CharInfo{
    char _ch;
    LongType _count;
    string _code;

    //for invalid
    bool 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值