说明:禁止转载,对源码的要求是禁止把这个东西原封不动或非常小量改动后用于课程设计(我很建议你自己动手实现,你会做的比我更好),源码仅供学习参考,思路仅供参考,仍有不足,欢迎评论指出。
1 课题概述
采用哈夫曼树求得的用于通信的二进制编码称为哈夫曼编码。利用哈夫曼编码对文本或图像进行数据压缩,设计数据压缩软件。
课程任务:
设计基于哈夫曼编码的文本和图像压缩软件。
(1)采用静态链表的二叉树等数据结构。
(2)可以随机、文件及人工输入数据。
(3)创建哈夫曼树,生成哈夫曼编码和译码。
(4)源码、编码和压缩后的信息均以文件形式保存。
(5)可以查询和更新数据。
(6)其它完善性或扩展性功能。
2 需求分析
在通信中,我们每天需要交换的信息非常大,如果总是以原文件传输,则会占用非常大的带宽,如果我们能有一个公用的方法,实现文件的解压缩,将大大减少信息传输的压力。霍夫曼编码正是基于字节频率,实现这一功能。
3 方案设计
3.1 总体功能设计
3.2. 数据结构设计
抽象数据类型的定义:总共使用了三种抽象数据类型,小根堆template<class T>
class mini_heap
{
T *heap;
int capacity;//容量
int heap_length;//堆元素个数
public:
mini_heap(T *p, int p_length);
//void insert(,int pos)
void push(T p);
//T top() const { return heap[heap_length]; }
T pop();
~mini_heap() {}//delete []heap; }//释放掉申请的内存
};搜索树: template<class K, class E>
struct search_node
{
h_pair< K, E> element;//h_pair.first代表key,pair.second代表value
search_node *p;
search_node *left;
search_node *right;
};(h_pair为我们模仿STLpair写的数据类型)
霍夫曼树:class hf_tree
{
hf_node *hf_array;//静态霍夫曼树组
int root;//记录根节点的位置
int size;//字符数
int code_flag;//判断是否有密码
int code_length;//密码的长度
int remain;//最后一个字符的位数
int char_count[256];//箱子
string de_eof;//储存结束符所对应的编码
search_tree<unsigned char, string> tab;//编码表
//search_tree<string, unsigned char> de_tab;//解码表
string s_in, s_buff, s_out;//输入输出及中途字符串
int i;
string f_code;//保存文件密码
FILE *f_out, *f_in;
void make_hftree();
void make_tab();
void make_code(string s);
//void decode(string s, string f_suffix);
public:
hf_tree() {};
void decode(string s, string f_suffix);
void compresss(string s);
};其中最主要的hf_node *hf_array;是基于 struct hf_node
{
int parent, right, left;
h_pair<int, unsigned char> node;//储存的字符及频率
string code;
public:
hf_node() :parent(0), right(0), left(0) {}
//hf_node(hf_node& x):code(x.code),node(x.node), parent(x.parent), right(x.right),left(x.left) {}
bool operator <(hf_node q) { return node < q.node; }
bool operator >(hf_node q) { return node >q.node; }
};设计的结构体
主程序的流程:选择压缩功能还是解压功能,输入文件名,输出对应的解压文件。
各程序模块之间的调用关系:main函数里调用hf_tree.cpp模块,hf_tree类调用mini_heap.h和h_pair.h和search_tree.h模块。
3. 主算法设计
3.3 构想:首先我们要确定是构想。
3.31编码原理:霍夫曼编码的原理是啥,许多教科书上的写法是按照字符出现的频率编码。我也险些被拉入坑里。虽然这么说也有一定的正确性,我们考察一个字符串aaabbc,如果我们不压缩存储按照ASCII码,我们需要6x8+8(\0)个存储空间。如果我们重新对aaabbc编码,使每个字符在计算机的存储不占8位,而使出现频率高的字符编码位01或者011等小于8位,那么存储所需要的空间将减少。(这里通过小根堆,因为小根堆是一个很好的优先级队列,你只用抛出两个最小的,然后把抛出后的两个最小的和,成为一个新节点,再加进去,重复,直到优先级队列为空就行了)
3.32实际碰到问题及解决方案:
3.321 编码问题: 教科书上说的编码,ASCII码,在实际用C++编程时,碰到的问题,比如ASCII码值就128个,但一个字节实际2的8次方,256个。超了怎么办?还有就是中文字符问题,中文字符表示可不止占一个字节,这可怎么压缩?曾经尝试过用宽字符wchar_t后来,证明这是一个错误的方案。仔细研究下,相通了。教科书上很大的误区,是按字符根据其出现的频率压缩。我觉得是不准确的,应该是按一个字节出现的频率压缩,一个字节有0到255种取值,根据字节值出现得频率,对每个字节进行编码。这样我们也就不用考虑中文,英文,反正最后在计算机中都是以字符存储得。
3.322 文件操作与字符串问题:
在知道原理后,最大的问题在于比如011在文件中占一个字节不到,我们操作数据的最小单位是字节,不是bit。怎么办呢?解决方案是位运算操作实现文件01序列与字符串01序列。在操作时,我们操作字符串的01序列,写入文件时按位写如文件。
那就有两个转换。首先时压缩时我们要把字符串的0和1,压缩到文件里。这里还有一个考量是我们每个字节是占8位的,字符串的长度不一定是8的倍数。怎么办呢,结尾全部补以1。不然不能就无法存储到计算机里,那还有一个问题就是怎么直到字符串什么时候截止呢。网上我曾经看到的一个想法是设立一个eof,这很像字符串的设计思想。实际上我之前也这么做了,只是后来我想到一个更好的解决方案。因为你如果设置eof, 极端情况会不会一个字节256个字符全部出现,那么你就需要一个257来编码,你的编码表就不能是char型了,要换成short型得,这显然是不划算得。所以后来我选择了多记录一个remain,记录剩余的位。再文件与字符串之间转换时,通过remain区分哪些是多余的。
3.323 编码解码效率问题考量
刚才我们已经讨论了,如何实现huffman树,在实现huffman树的时候struct hf_node
{
int parent, right, left;
h_pair<int, unsigned char> node;//储存的字符及频率
string code;
}
编码:在节点里,我其实已经通过一个字符串储存了code的编码了,我们把原文件的01串unsigned char串转换成01,一个很好的方法,我们挨个再扫字符串,找霍夫曼树的叶子节点把对应的code取出,但,,,这速度真的铁定比如不如搜索树快,所以我写了制作编码表的函数,你只要根据unsigned char的值,找搜索树就行了(以前写模板类的时候,红黑树没写出来),红黑树会更快。
然后解码,我也尝试试过写解码表,这里效率确实就不如huffman树了,测试慢了非常非常多,我分析出了为什么,解码你用的是code也就是字符对应的编码字符串,去找对应的字符,如果用搜索树,是比较字符串是否相等,比较字符串的实质是挨个比字符,速度当然慢了,所以这里采用的策略是根据编码串的01顺着霍夫曼树找,找到一个叶子节点出来一个字符。
3.324 加密解密功能
这个是一个额外的功能,主要是将密码,它的长度,标志为写到文件首用于判断。
4方案实现
最基础的是h_pair的实现,这是模仿C++的pair
template<class K, class E>
struct h_pair
{
public:
K first;
E second;
h_pair() {}
h_pair(K a, E b) :first(a), second(b) {}
h_pair(K a) :first(a) {}
bool operator <(h_pair<K, E> node) { return first < node.first; }//因为要构造最小优先级队列,重载小于
bool operator >(h_pair<K, E> node) { return first > node.first; }//因为要构造最小优先级队列,重载大于
~h_pair() {}
};
首先是小根堆的实现,我采用了模板类,主要参考了《算法导论》,小根堆即堆顶一定是最小元素的一种数据结构,它是一个顺序结构,它的初始化通过数组,是从底层开始要满足小根堆的性质,一直往前走,然后插入和删除,均是通过比较和位置变动实现。不详谈,这是代码。
template<class T>
class mini_heap
{
T *heap;
int capacity;//容量
int heap_length;//堆元素个数
public:
mini_heap(T *p, int p_length);
//void insert(,int pos)
void push(T p);
//T top() const { return heap[heap_length]; }
T pop();
~mini_heap() {}//delete []heap; }//释放掉申请的内存
};
template<class T>
mini_heap<T>::mini_heap(T *p, int p_length)
{
capacity = heap_length = p_length;
heap = p;//用堆封装数组
for (int root = heap_length / 2; root >= 1; --root)//因为完全二叉树从n/2+1为叶子节点,所以从最下层的一个有孩子节点的算起
{
int s = root;//s代表当前向下走的节点
int left, right;//左右孩子
int mini;//最小值
while (s * 2 <= heap_length)//当s没到外部节点的时候
{
left = s * 2;
right = s * 2 + 1;
mini = s;
if (heap[left]<heap[mini])
mini = left;//如果小于左孩子
if (right <= heap_length && heap[right] < heap[mini])
mini = right;//分析可知,对于内部节点左孩子一定存在,有孩子可能不存在
if (mini == s)
break;//找出三者最小,如果已经满足则跳出
else
{
T temp = heap[mini];
heap[mini] = heap[s];
heap[s] = temp;//交换
s = mini;//向下走
}
}
}
}
template<class T>
T mini_heap<T>::pop()
{
T max = heap[1];
heap[1] = heap[heap_length--];//先将第一个覆盖并减一
int s = 1;//s代表当前向下走的节点
int left, right;//左右孩子
int mini;//最小值
while (s * 2 <= heap_length)//当s没到外部节点的时候
{
left = s * 2;
right = s * 2 + 1;
mini = s;
if (heap[left]<heap[mini])
mini = left;//如果小于左孩子
if (right <= heap_length && heap[right] < heap[mini])
mini = right;//分析可知,对于内部节点左孩子一定存在,有孩子可能不存在
if (mini == s)
break;//找出三者最小,如果已经满足则跳出
else
{
T temp = heap[mini];
heap[mini] = heap[s];
heap[s] = temp;//交换
s = mini;//向下走
}
}
return max;
}
template<class T>
void mini_heap<T>::push(T p)
{
if (heap_length == capacity)
{
T*q = new T[2 * capacity + 1];
for (int i = 0; i <= capacity; ++i)
q[i] = heap[i];
//delete heap;
capacity *= 2;
heap = q;
}//如果容量已满先加倍
heap[++heap_length] = p;//先放入
int pos = heap_length;
while (pos / 2)//当没到根节点的时候
{
int parent = pos / 2;
if (heap[parent] >heap[pos])
{
T temp = heap[parent];
heap[parent] = heap[pos];
heap[pos] = temp;
pos = parent;
}//不满足二叉树性质,往上走
else
break;
}
}
搜索树,搜索树是通过链表来实现的,因为没考虑平衡因子,也没考虑旋转的红黑,这也没什么难度。唯一的亮点在于通过内嵌类实现迭代器,这是我很早之前的一个尝试。方便了我遍历搜索树。
template<class K, class E>
struct search_node
{
h_pair< K, E> element;//h_pair.first代表key,pair.second代表value
search_node *p;
search_node *left;
search_node *right;
};
template<class K, class E>
class search_tree
{
search_node<K, E> * root, *bg;//bg保存第一个最小指针的位置
int num;
public:
search_tree() { bg = root = NULL; num = 0; }
int size()const { return num; }
bool empty() const { return num == 0; }
void insert(h_pair <K, E> &thepair);
h_pair<K, E> top()const { return root->element; }
h_pair<K, E>* find(K key) const;
search_node<K, E> * begin() { return bg; }
search_node<K, E> * end() { return NULL; }
//void for_inorder(node<K, E> *p)const;
class iterator;//内嵌类做map的迭代器
~search_tree() {}
};
template<class K, class E>
class search_tree<K, E>::iterator
{
search_node< K, E> *it;
public:
iterator() { it = NULL; }
iterator(search_node< K, E> *p)
{
it = p;
}
h_pair<K, E> * operator ->()
{
return &(it->element);
}
h_pair<K, E> operator *()
{
return (it->element);
}//重载->和*
void operator =(search_node<K, E>* p)
{
it = p;
}//重载=
bool operator ==(search_node<K, E>* p)
{
return it == p;
}//重载==
bool operator !=(search_node<K, E>* p)
{
return it != p;
}//重载!=
void operator ++()
{
if (it == NULL)
{
throw;
}
if (it->right != NULL)//当当前节点有右孩子的情况
{
it = it->right;
while (it->left != NULL)
it = it->left;
}
else
{
while ((it->p != NULL) && (it == it->p->right))
it = it->p;
it = it->p;
}
}//重载前置++
void operator --()
{
if (it == NULL)
{
throw;
}
if (it->left != NULL)//当当前节点有左孩子的情况
{
it = it->left;
while (it->right != NULL)
it = it->right;
}
else
{
while ((it->p != NULL) && (it == it->p->left))
it = it->p;
it = it->p;
}
}//重载前置--
~iterator() {};
};//内嵌类做map的迭代器*/
template<class K, class E>
h_pair< K, E>* search_tree<K, E>::find(K key) const
{
search_node < K, E> *p = root;
while (p != NULL)
{
if (p->element.first < key)
p = p->right;
else if (p->element.first > key)
p = p->left;
else
return &(p->element);//找到了返回其中的element
}
return NULL;//未找到
}
template<class K, class E>
void search_tree<K, E>::insert(h_pair < K, E> &thepair)
{
search_node<K, E> *newsearch_node = new search_node<K, E>;
newsearch_node->element.first = thepair.first;
newsearch_node->element.second = thepair.second;
newsearch_node->left = newsearch_node->right = NULL;//上色//先开辟一个新的节点空间,并赋值
if (root == NULL)//还没有节点的情况下
{
root = newsearch_node;
root->p = NULL;//没有父亲
bg = root;
++num;
return;//提前退出
}
search_node<K, E> *p = root, *pp = NULL;//pp代表当前节点的父亲
while (p != NULL)
{
pp = p;
if (thepair.first < p->element.first)
{
p = p->left;
}
else if (thepair.first > p->element.first)
{
p = p->right;
}
else //如果key相等,直接覆盖提前退出
{
p->element.second = thepair.second;
return;
}
}
++num;
newsearch_node->p = pp;//p为当前节点插入的位置,所以他的父亲是pp
if (thepair.first > pp->element.first)
{
pp->right = newsearch_node;
}
else
pp->left = newsearch_node;
p = root;//p用来遍历bg是否需要矫正
while (p->left != NULL)
p = p->left;
bg = p;//矫正
}
霍夫曼树的实现:
首先是节点,它要包含什么? 他得有父亲,有左右孩子,但它还应该有的是它所储存的字符及其频率,以及它所对应的编码。
struct hf_node
{
int parent, right, left;
h_pair<int, unsigned char> node;//储存的字符及频率
string code;
public:
hf_node() :parent(0), right(0), left(0) {}
//hf_node(hf_node& x):code(x.code),node(x.node), parent(x.parent), right(x.right),left(x.left) {}
bool operator <(hf_node q) { return node < q.node; }
bool operator >(hf_node q) { return node >q.node; }
};
其次是霍夫曼类,我给它留了两个接口,一个是压缩,一个是解压。这里要说明一下文件操作我用的是C语言的,因为C++用的时候出了一些bug,最后就用的C语言。这个类重点是它的私有部分。
class hf_tree
{
hf_node *hf_array;//静态霍夫曼树组
int root;//记录根节点的位置
int size;//字符数
int code_flag;//判断是否有密码
int code_length;//密码的长度
int remain;//最后一个字符的位数
int char_count[256];//箱子
string de_eof;//储存结束符所对应的编码
search_tree<unsigned char, string> tab;//编码表
//search_tree<string, unsigned char> de_tab;//解码表
string s_in, s_buff, s_out;//输入输出及中途字符串
int i;
string f_code;//保存文件密码
FILE *f_out, *f_in;
void make_hftree();
void make_tab();
void make_code(string s);
//void decode(string s, string f_suffix);
public:
hf_tree() {};
void decode(string s, string f_suffix);
void compresss(string s);
};
首先我们来看压缩
const char* filename = s.c_str();
f_in = fopen(filename, "rb");
while (!feof(f_in))
s_in += getc(f_in);
fclose(f_in);
//cout << s_in.length()<<endl;
s_in.pop_back();
for (int i = 0; i < 255; ++i)
char_count[i] = 0;
for (int i = 0; i<s_in.length(); ++i)
{
int q = unsigned char(s_in[i]);
++char_count[q];
}//因为ASCII为一个字节,所以来个0xFF的箱子
首先要读,并储存频率,这里面最难的是下面
size = 0;
for (int i = 0; i<256; ++i)
if (char_count[i] != 0)
++size;
hf_node*pq = new hf_node[size + 1];//初始化优先级队列pq
cout << size << endl;
int pos = 0;
for (int i = 0; i<256; ++i)
if (char_count[i] != 0)
{
++pos;//因为小根堆,我们没用0元素,所以从1开始
pq[pos].node.second = i;
pq[pos].node.first = char_count[i];
}
pq[pos].node.first = 0;//加入结束符,将频率设置为1,确保他一定在表的第0位
mini_heap<hf_node> min(pq, size);//初始化小根堆
delete[]hf_array;
hf_array = new hf_node[size * 2];//因为完全二叉树2*n-1,但一号元素不用,所以申请2*size
pos = 0;
for (int i = 1; i <= size - 1; ++i)
{
hf_array[++pos] = min.pop();//删除并返回最小值
hf_array[++pos] = min.pop();
hf_node parent;
parent.left = pos - 1;
parent.right = pos;
parent.node.first = hf_array[pos].node.first + hf_array[pos - 1].node.first;
//cout<< parent.node.first << " " << parent.left << " " << parent.right << endl;
min.push(parent);
}
hf_array[++pos] = min.pop();
for (int i = 1; i <= 2 * size - 1; ++i)
{
int child;
if (hf_array[i].left != 0)
{
child = hf_array[i].left;
hf_array[child].parent = i;
}
if (hf_array[i].right != 0)
{
child = hf_array[i].right;
hf_array[child].parent = i;
}
}//因为父亲的位置再次排序会变所以根据判断有没有孩子,认父亲
root = pos;
注意:hf_array = new hf_node[size * 2];//因为完全二叉树2*n-1,但一号元素不用,所以申请2*size,这是我们建立一个静态链表的基础,知道要编码的节点数,然后申请了整棵树的节点,刚开始觉得铁定用动态链表方便啊,但无奈题目要求静态那没办法了,这着实花了一番功夫,因为父亲的位置是会变得,你从小跟堆出来的两个节点,你制造一个新的节点进去,这个新的节点什么时候出来?你之后很难找父亲,经过一番推导,再次遍历建立正确得节点关系。
for (int i = 1; i <= 2 * size - 1; ++i)
{
int child;
if (hf_array[i].left != 0)
{
child = hf_array[i].left;
hf_array[child].parent = i;
}
if (hf_array[i].right != 0)
{
child = hf_array[i].right;
hf_array[child].parent = i;
}
}//因为父亲的位置再次排序会变所以根据判断有没有孩子,认父亲
然后就是转码问题了,
首先是原串比如abc,转成对应得01串,再写入文件,这里主要运用位运算。8个为一位压缩
void hf_tree::make_code(string s)
{
for (int i = 0; i <s_in.length(); ++i)
{
unsigned char b = unsigned char(s_in[i]);
h_pair<unsigned char, string> *p = tab.find(b);
if (p != NULL)
{
s_buff += p->second;
}
}
remain = s_buff.length() % 8;//求余下的位
for (int i = 1; i <= (8 - remain); ++i)
{
s_buff += '1';
}//后面的位全部填满1
//cout << s_buff << endl;
int q = s_buff.length() / 8;//先分成整的字节段
for (int i = 0; i <q; ++i)
{
unsigned char c = 0;
for (int j = 8 * i; j < 8 * (i + 1); ++j)//
{
c = c << 1;//左移一位
if (s_buff[j] == '1')
{
c += 1;
}
}
s_out += c;
}
//cout << s_out.length() << endl;
int i;
for (i = s.length() - 1; s[i] != '.'; --i);
s.erase(i);
s += ".hf";
//cout << "请选择是否加密:" << endl;
cout << "请输入密码" << endl;
cin >> f_code;
const char* filename = s.c_str();
f_out = fopen(filename, "wb");
code_length = f_code.length();
putw(code_length, f_out);
code_flag = 0;
putw(code_flag, f_out);
const void *buff = f_code.c_str();
fwrite(buff, f_code.length(), 1, f_out);
putw(remain, f_out);
putw(size, f_out);
for (int i = 0; i < 256; ++i)
{
if (char_count[i] != 0)
{
unsigned char q = i;
fputc(q, f_out);
putw(char_count[i], f_out);
}
}
buff = s_out.c_str();
fwrite(buff, s_out.length(), 1, f_out);
fclose(f_out);
}
01串变为abc,是上面的逆过程,先变成一个字符串01,然后通过huffman树变为abc。
void hf_tree::decode(string s, string f_suffix)
{
s_buff.clear();
s_out.clear();
const char* filename = s.c_str();
f_in = fopen(filename, "rb");
code_length = getw(f_in);
code_flag = getw(f_in);
if (code_flag == 0)
{
cout << "请输入密码:" << endl;
cin >> f_code;
string match;
for (int i = 0; i < code_length; ++i)
{
match += fgetc(f_in);
}
if (match != f_code)
{
cout << "有误" << endl;
return;
}
remain = getw(f_in);
size = getw(f_in);
}
else
{
remain = code_length;
size = code_flag;
}
for (int i = 0; i < size; ++i)
{
int pos;
pos = fgetc(f_in);
char_count[pos] = getw(f_in);
}
while (!feof(f_in))
s_out += fgetc(f_in);
s_out.erase(s_out.length() - 1);
//cout << s_out.length();
//cout << b;
make_hftree();
for (int i = 0; i < s_out.length(); ++i)
{
unsigned char c(s_out[i]);
for (int j = 1; j <= 8; ++j)
{
if (c >= 128)
s_buff += '1';
else
s_buff += '0';
c = c << 1;//通过不断左移判断最高位找出值
}
}
s_out.clear();
bool flag = 0;
cout << s_buff.length() << endl;
int pos = root;
int q = (s_buff.length() / 8 - 1) * 8 + remain;
for (int i = 0; i < q; ++i)
{
if (s_buff[i] == '0')
pos = hf_array[pos].right;
else
pos = hf_array[pos].left;
if (hf_array[pos].left == 0 && hf_array[pos].right == 0)
{
s_out += (unsigned char)hf_array[pos].node.second;
pos = root;//重置
}
}
s.erase(s.length() - 2);
s += f_suffix;
filename = s.c_str();
f_out = fopen(filename, "wb");
const void *buff = s_out.c_str();
fwrite(buff, s_out.length(), 1, f_out);
fclose(f_out);
}
最后是加密解密问题:
这个功能不难就将加密解密需要的信息写在文件首判断就行。
加密:
cout << "请输入密码" << endl;
cin >> f_code;
const char* filename = s.c_str();
f_out = fopen(filename, "wb");
code_length = f_code.length();
putw(code_length, f_out);
code_flag = 0;
putw(code_flag, f_out);
const void *buff = f_code.c_str();
解密:
cout << "请输入密码:" << endl;
cin >> f_code;
string match;
for (int i = 0; i < code_length; ++i)
{
match += fgetc(f_in);
}
if (match != f_code)
{
cout << "有误" << endl;
return;
}
不足:尾缀问题:
这是一个没有解决的问题,也是刚开始设计完全没考虑到的问题,尾缀其实就文件名嘛,写完才发现,你比如一个txt加密,你不仅仅应该还原完文件的所有内容,你还应该储存文件的格式,因为你解压完,你一定是有格式的,如果你不储存txt,怎么输出,我这里是投机取巧的把txt再解压的时候人为的输入了进去,但其实不应该的。希望寒假有空能帮这个东西继续完善。
5.测试与调试:
原文件,压缩文件,及还原文件
调试:通过VS还有一款软件
,它可以看一个文件里面的二进制编码,帮助了我找出了文件的错误。
这是用一个最简单的abc文件
第一个输入是压缩,第二个为解码
最后一个字节为0010111,正好是abc及我们补的1,至于为什么前面那么多东西,你得储存频率和字符,还有密码,所以少字节没啥好压缩的。
也还原了。