哈夫曼树与哈夫曼编码

哈夫曼编码

哈夫曼(Huffman)编码算法是基于二叉树构建编码压缩结构的,它是数据结构当中典型的一种算法。算法根据文本字符出现的频率,重新对字符进行编码。

不知道你们曾经有没有想过压缩的原理是什么?又是如何解压回去的?我认为是有很多大量重复的东西可以用更简短的方式来表示,而需要复原时,只需要根据对应关系对照着还原回去就好了。

比如:大家都知晓著名西班牙画家毕加索,可是不知道他的全名是:巴勃罗·迭戈·荷瑟·山迪亚哥·弗朗西斯科·德·保拉·居安·尼波莫切诺·克瑞斯皮尼亚诺·德·罗斯·瑞米迪欧斯·西波瑞亚诺·德·拉·山迪西玛·特立尼达·玛利亚·帕里西奥·克里托·瑞兹·布拉斯科·毕加索,对于这么长的名字,仅用毕加索三个字代替,可以大大缩短名字的字数。

哈夫曼编码的原理就是这个,但是只是缩短单个字符的编码,按照文本字符出现的频率,出现次数越多的字符用越简短的编码代替,因为为了缩短编码的长度,我们希望出现频率越高的字符,编码越短,这样才能最大化压缩文本数据的空间。

哈夫曼编码举例

假设我们要对“like baby baby baby no”进行压缩。

压缩前,我们是使用的ASCII保存。

10810510710132989798121329897981213298979812132110111

总共需要 22 个字节来保存-196 个位。

我们来统计一下这句话中各字符出现的频率,制成下表。

字符likenoay空格(z)b
频率1111113346

再根据这个频率表制成编码表。

字符b空格yaonekil
编码10001100111110111100111111111001010100

这样以后刚刚那句话就可以用以下位来表示:

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 个右分支 和一个左分支,当然颠倒也行。

3a3808fa23084e33b35731d912deed83.png

哈夫曼二叉树的构建

例如刚刚那句话“like baby baby baby no”。

1.首先用数组保存各字符的频率和字符本身,按从小到大的顺序。

46a17ce768ba4e7bac648bfca5cde6d9.png

5fbcd91ad22e4b8ab24ef52363cfcb39.png

2.从左至右依次进行合并,构建二叉树。选取字符 l 和字符 i 分别作为二叉树的子节点,根节点的频率为两个子节点频率之和为 2,新构建出的节点用红色表示。

61b9bf1033504c40a21b2474872eee83.png

3.继续合并。这次为字符 k 和字符 e 。

c76cee90b09945df85bd3ab83c155035.png

4.重复以上步骤,直至所有的字符节点都变成叶子节点,也就是数组中只剩下了一个节点。

95d04b64ad0b4b8295cecb4eadae5517.png

6df53a589e294c29a207f38565c605df.png

2759cf8cef5245bba864b605f7e84507.png

……

c12a105499a64e1996a170d742e6d8e4.png

最后得到的就是最开始展示的哈夫曼二叉树了,但是其中可能会有疑问,比如新加一个节点后,怎么让数组自动排序?其实在实现时使用了优先队列,巧妙地让频率小的出队列。又如何保证新加节点在和新加节点频率相等的节点的前面?例如在第三步中,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;
}

结束

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值