哈夫曼编码
哈夫曼(Huffman)编码算法是基于二叉树构建编码压缩结构的,它是数据结构当中典型的一种算法。算法根据文本字符出现的频率,重新对字符进行编码。
不知道你们曾经有没有想过压缩的原理是什么?又是如何解压回去的?我认为是有很多大量重复的东西可以用更简短的方式来表示,而需要复原时,只需要根据对应关系对照着还原回去就好了。
比如:大家都知晓著名西班牙画家毕加索,可是不知道他的全名是:巴勃罗·迭戈·荷瑟·山迪亚哥·弗朗西斯科·德·保拉·居安·尼波莫切诺·克瑞斯皮尼亚诺·德·罗斯·瑞米迪欧斯·西波瑞亚诺·德·拉·山迪西玛·特立尼达·玛利亚·帕里西奥·克里托·瑞兹·布拉斯科·毕加索,对于这么长的名字,仅用毕加索三个字代替,可以大大缩短名字的字数。
哈夫曼编码的原理就是这个,但是只是缩短单个字符的编码,按照文本字符出现的频率,出现次数越多的字符用越简短的编码代替,因为为了缩短编码的长度,我们希望出现频率越高的字符,编码越短,这样才能最大化压缩文本数据的空间。
哈夫曼编码举例
假设我们要对“like baby baby baby no”进行压缩。
压缩前,我们是使用的ASCII保存。
108 | 105 | 107 | 101 | 32 | 98 | 97 | 98 | 121 | 32 | 98 | 97 | 98 | 121 | 32 | 98 | 97 | 98 | 121 | 32 | 110 | 111 |
总共需要 22 个字节来保存-196 个位。
我们来统计一下这句话中各字符出现的频率,制成下表。
字符 | l | i | k | e | n | o | a | y | 空格(z) | b |
---|---|---|---|---|---|---|---|---|---|---|
频率 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | 4 | 6 |
再根据这个频率表制成编码表。
字符 | b | 空格 | y | a | o | n | e | k | i | l |
---|---|---|---|---|---|---|---|---|---|---|
编码 | 10 | 00 | 110 | 011 | 11101 | 11100 | 11111 | 11110 | 0101 | 0100 |
这样以后刚刚那句话就可以用以下位来表示:
0100 0101 11110 11111 00 10 011 10 110 00 10 011 10 110 00 10 011 10 110 00 11100 11101
这是肯定比之前少了,我就不数了:),当需要解压缩时,按照这个表还原就好了,你可以试试对不对得上。(这个表会保存在压缩文件的头部)
有人可能会问会不会有出现这种情况:其中一个编码是另一个编码的前缀。例如一个是 00 ,一个是 001 。发生这种情况,那001就永远不可能匹配上,因为会被00截胡,但是哈夫曼树就是避免了这种情况。
如何避免那种情况发生呢?那就是所有字符的节点在树中都是叶子节点,如果不是叶子节点,那么必然会发生,如下图。
哈夫曼树---其中一个左分支路径为 0 ,一个右分支路径为 1 ,例如 k 的编码 11110,便是 4 个右分支 和一个左分支,当然颠倒也行。
哈夫曼二叉树的构建
例如刚刚那句话“like baby baby baby no”。
1.首先用数组保存各字符的频率和字符本身,按从小到大的顺序。
2.从左至右依次进行合并,构建二叉树。选取字符 l 和字符 i 分别作为二叉树的子节点,根节点的频率为两个子节点频率之和为 2,新构建出的节点用红色表示。
3.继续合并。这次为字符 k 和字符 e 。
4.重复以上步骤,直至所有的字符节点都变成叶子节点,也就是数组中只剩下了一个节点。
……
最后得到的就是最开始展示的哈夫曼二叉树了,但是其中可能会有疑问,比如新加一个节点后,怎么让数组自动排序?其实在实现时使用了优先队列,巧妙地让频率小的出队列。又如何保证新加节点在和新加节点频率相等的节点的前面?例如在第三步中,k、e新生成的父节点是在l、e的父节点的前面,一般我们的队列是后插法入队的,这里采用前插法入队就可以了,可以保证新加节点的相对位置在和他相同频率的节点的前面(左边)。
然后呢肯定不止有这一种方法,比如就可以直接创建一个数组,用几个函数,也能到达这种效果。
比如查找两个优先级最高的函数,从数组中删除操作的函数(可能不需要,可以直接覆盖),构建一个二叉树的函数,插入数组的函数……
代码实现
Huff.h
#pragma once
#include <iostream>
#define QUEUE_MAX_SIZE 120
//因为构建哈夫曼树的过程不需要节点的删除、插入、查找等操作
// 我就只写它的结点结构体定义和前序遍历操作
typedef struct _BNode
{
char ch; //保存字符
int value; //权重,字符的出现频率
struct _BNode* lchild;
struct _BNode* rchild;
struct _BNode* parent;
}BNode, BTree;
//前序遍历
void PreVisit(BTree* root)
{
if (!root)
{
return;
}
printf("-%c-", root->ch);
PreVisit(root->lchild);
PreVisit(root->rchild);
}
typedef BNode* DateElem;
typedef struct _QueueNode //优先队列节点
{
int value; //优先级,越小优先级越高
DateElem date;
struct _QueueNode* next;
}QueueNode;
typedef QueueNode* QueuePtr;
typedef struct _Queue //优先队列
{
int length;
QueuePtr head;
QueuePtr tail;
}Queue;
//队列初始化
bool initQueue(Queue* q)
{
if (q == NULL)
{
return false;
}
q->length = 0;
q->head = NULL;
q->tail = NULL;
return true;
}
//判空
bool isEmpty(Queue* q)
{
if (!q)
{
return false;
}
if (q->head == NULL)
{
return true;
}
else
{
return false;
}
}
//判满
bool isFull(Queue* q)
{
if (!q)
{
return false;
}
if (q->length >= QUEUE_MAX_SIZE)
{
return true;
}
else
{
return false;
}
}
//入队---前插法,不是平常的后插法
bool pushQueue(Queue* q, DateElem e,int value)
{
if (!q || isFull(q))
{
return false;
}
QueueNode* p = new QueueNode;
p->date = e;
p->value= value;
p->next = NULL;
if (isEmpty(q)) //空队列
{
q->head = p;
q->tail = p;
}
else //正常情况
{
p->next = q->head;
q->head = p;
}
q->length++;
return true;
}
//出队---out指针用来返回被删除的元素的值
bool popQueue(Queue* q, DateElem &out)
{
if (!q || isEmpty(q) || !out)
{
return false;
}
QueuePtr max_tmp1 = q->head, max_tmp2 = NULL; // 1 在 2 右边,2保存着上一个节点,这里的max指的是优先级最大的
QueuePtr tmp1 = q->head, tmp2 = NULL;
int max = q->head->value;
while (tmp1 != NULL)
{
if (tmp1->date->value < max)
{
//std::cout << "找到了一个更大的优先级,为:" << tmp1->value << std::endl;
max = tmp1 ->date->value;
max_tmp1 = tmp1;
max_tmp2 = tmp2;
}
else
{
tmp2 = tmp1;
tmp1 = tmp1->next;
}
}
if ( max_tmp1 != NULL) //确保max_tmp1不为空,因为不这样会报错
{
out = max_tmp1->date;
}
if (max_tmp2 == NULL && max_tmp1 != NULL) //说明出队的是第一个结点。并且确保max_tmp1不为空,因为不这样会报错
{
q->head = max_tmp1->next;
}
else if(max_tmp2 != NULL && max_tmp1 != NULL) //确保max_tmp1、max_tmp2不为空,,因为不这样会报错
{
max_tmp2->next = max_tmp1->next;
}
delete max_tmp1;
q->length--;
//删除有两种情况,需要移动头尾指针
if (isEmpty(q))//情况一,删除节点后,为空队列
{
q->head = q->tail = NULL;
}
else if(max_tmp2 != NULL && max_tmp2->next == NULL) //情况二,删除的是最后一个节点
{
q->tail = max_tmp2;
}
return true;
}
//打印队列
void printQueue(Queue* q)
{
if (!q )
{
return ;
}
if (isEmpty(q))
{
std::cout << "队列为空" << std::endl;
return;
}
QueueNode* p = q->head;
while (p != NULL)
{
printf("-%c[优先级%d]-", p->date->ch, p->value);
p = p->next;
}
puts("");
return;
}
//销毁队列
void destoryQueue(Queue* q)
{
if (!q || isEmpty(q))
{
return;
}
QueueNode* p = q->head, *tmp = NULL;
while (p != NULL)
{
tmp = p->next;
delete p;
p = tmp;
}
q->length = 0;
q->head = q->tail = NULL;
return;
}
//哈夫曼函数
void Huffman(BTree*& root, int n)
{
Queue* q = new Queue;
initQueue(q);
for (int i = 0; i < n; i++)
{
BNode* p = new BNode;
std::cout << "请输入第" << i + 1 << "个字符和它出现的频率:";
//b 6
//z 4
//y 3
//a 3
//o 1
//n 1
//e 1
//k 1
//i 1
//l 1
//应该按照这种顺序输入,因为是前插法入队
std::cin >> p->ch >> p->value;
p->lchild = NULL;
p->rchild = NULL;
p->parent = NULL;
pushQueue(q, p, p->value);
}
printQueue(q);
DateElem out1, out2; //分别是出队的第一个和第二个元素
BNode* tmp;
while (1)
{
if (!isEmpty(q))
{
popQueue(q, out1);
printf("第一个出队的字符为:%c,出现的频率为:%d\n", out1->ch, out1->value);
}
else
{
break;
}
if (!isEmpty(q))
{
popQueue(q, out2);
printf("第二个出队的字符为:%c,出现的频率为:%d\n", out2->ch, out2->value);
tmp = new BNode; //新加节点
tmp->ch = ' ';
tmp->value = out1->value + out2->value;
tmp->lchild = out1;
tmp->rchild = out2;
tmp->parent = NULL;
out1->parent = tmp;
out2->parent = tmp;
printf("合并节点的字符为:%c,出现的频率为:%d\n", tmp->ch, tmp->value);
pushQueue(q, tmp, tmp->value);
}
else
{
root = out1; //哈夫曼二叉树已经构建好了
}
}
destoryQueue(q);
}
main.cpp
#include <iostream>
#include "Huff.h"
using namespace std;
int main(void)
{
BTree* root = NULL;
Huffman(root, 10); //构建哈夫曼树---/*like baby baby baby no*/ 10个字符
PreVisit(root); //前序遍历
return 0;
}
实现完了哈夫曼二叉树后,还有点问题,空格怎么输入,这个问题可以自己探索。
哈夫曼编码的打印
看到这里有人是否想到了之前的父节点,没错用的就是父节点。思路:先递归找到各个叶子节点,找到后,去看父节点和这个孩子节点的关系,看看编码是 0 还是 1 ,然后用vector数组存储下来,重复此操作,最后打印就好了
查找叶子节点
void SeekLeaf(BTree* root) //根节点
{
if (root == NULL)
{
return;
}
if (root->lchild != NULL)
{
SeekLeaf(root->lchild);
}
if (root->rchild != NULL)
{
SeekLeaf(root->rchild);
}
//是叶子节点
if (root->lchild ==NULL && root->rchild == NULL && root->ch != ' ') //其实还是有瑕疵,因为存储的如果就是空格怎么办,所以新加节点的ch的值还要改,不过我这里就不改了
{
Print(root);
}
}
打印哈夫曼编码
void Print(BNode* l)
{
std::vector<int> print;
BNode* p = l,* tmp = NULL;
while (p->parent != NULL) //直到是根节点
{
tmp = p; //tmp保存子节点
p = p->parent; //p保存着父节点
if (p->lchild == tmp)
{
print.push_back(0);
}
else
{
print.push_back(1);
}
}
std::cout << "字符" << l->ch << "的编码是:";
for (int i =(int) print.size() - 1; i >= 0; i--)
{
printf("%d", print[i]);
}
std::cout << std::endl;
}
结束