热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

c/c++开发分享漫谈C++哈夫曼树的原理及实现

1.前言什么是哈夫曼树?把权值不同的n个结点构造成一棵二叉树,如果此树满足以下几个条件:此n个结点为二叉树的叶结点。权值较大的结点离根结点较近,权值较小的结点离根结点较远。该树的

1. 前言

什么是哈夫曼树?

把权值不同的n个结点构造成一棵二叉树,如果此树满足以下几个条件:

  • 此 n 个结点为二叉树的叶结点 。
  • 权值较大的结点离根结点较近,权值较小的结点离根结点较远。
  • 该树的带权路径长度是所有可能构建的二叉树中最小的。

则称符合上述条件的二叉树为最优二叉树,也称为哈夫曼树(huffman tree)。

构建哈夫曼树的目的是什么?

用来解决在通信系统中如何使用最少的二进制位编码字符信息。

c/c++开发分享漫谈C++哈夫曼树的原理及实现将和大家聊聊哈夫曼树的设计思想以及构建过程。

2. 设计思路

哈夫曼树产生的背景:

在通信系统中传递一串字符串文本时,需要对这一串字符串文本信息进行二进制编码。编码时如何保证所用到的bit位是最少的,或保证整个编码后的传输长度最短。

现假设字符串由abcd 4个字符组成,最直接的想法是使用 2 个bit位进行等长编码,如下表格所示:

字符 编码
a 00
b 01
c 10
d 11

传输abcd字符串一次时,所需bit为 2位,当通信次数达到 n次时,则需要的总传输长度为 n*2。当字符串的传输次数为 1000次时,所需要传输的总长度为 2000bit

使用等长编码时,如果传输的报文中有 26 个不同字符时,因需要对每一个字符进行编码,至少需要 5bit

但在实际应用中,各个字符的出现频率或使用次数是不相同的,如a、b、c的使用频率远远高于x、y、z。使用等长编码特点是无论字符出现的频率差异有多大,每一个字符都得使用相同的bit位。

哈夫曼的设计思想

  • 对字符串信息进行编码设计时,让使用频率高的字符使用短码,使用频率低的用长码,以优化整个信息编码的长度。
  • 基于这种简单、朴素的想法设计出来的编码也称为不等长编码

哈夫曼不等长编码的具体思路如下:

如现在要发送仅由a、b、c、d 4 个字符组成的报文信息 ,a字符在信息中占比为 50%b的占比是 20%c的占比是 15%, d的 占比是10%

不等长编码的朴实思想是字符的占比越大,所用的bit位就少,占比越小,所用bit位越多。如下为每一个字符使用的bit位数:

  • a使用 1bit编码。
  • b使用 2 位 bit编码。
  • c 使用 3 位 bit编码。
  • d 使用 3 位 bit 编码。

具体编码如下表格所示:

字符 占比 编码
a 0.5 0
b 0.2 10
c 0.15 110
d 0.1 111

如此编码后,是否真的比前面的等长编码所使用的总bit位要少?

计算结果=0.5*1+0.2*2+0.15*3+0.1*3=1.65

先计算每一个字符在报文信息中的占比乘以字符所使用的bit位。

然后对上述每一个字符计算后的结果进行相加。

显然,编码abcd只需要 1.65 个bit ,比等长编码用到的2 个 bit位要少 。当传输信息量为 1000时,总共所需要的bit位=1.65*1000=1650 bit

哈夫曼编码和哈夫曼树有什么关系?

因为字符的编码是通过构建一棵自下向上的二叉树推导出来的,如下图所示:

漫谈C++哈夫曼树的原理及实现

哈夫曼树的特点:

  • 信息结点都是叶子结点。
  • 叶子结点具有权值。如上二叉树,a结点权值为0.5b结点权值为0.2c结点权值为0.15d结点权值为 0.1
  • 哈夫曼编码为不等长前缀编码(即要求一个字符的编码不能是另一个字符编码的前缀)。
  • 从根结点开始,为左右分支分别编号01,然后顺序连接从根结点到叶结点所有分支上的编号得到字符的编码。

相信大家对哈夫曼树有了一个大概了解,至于如何通过构建哈夫曼树,咱们继续再聊。

3. 构建思路

在构建哈夫曼树之前,先了解几个相关概念:

  • 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第l层结点的路径长度为l-1
  • 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
  • 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为wpl

如有权值为{3,4,9,15}的 4 个结点,则可构造出不同的二叉树,其带权路径长度也会不同。如下 3 种二叉树中,b的树带权路径长度是最小的。

漫谈C++哈夫曼树的原理及实现

哈夫曼树的构建过程就是要保证树的带权路径长度最小。

那么,如何构建二叉树,才能保证构建出来的二叉树的带权路径长度最小?

如有一字符串信息由 abcdefgh 8个字符组成,每一个字符的权值分别为{3,6,12,9,4,8,21,22},构建最优哈夫曼树的流程:

1.以每一个结点为根结点构建一个单根二叉树,二叉树的左右子结点为空,根结点的权值为每个结点的权值。并存储到一个树集合中。

漫谈C++哈夫曼树的原理及实现

2.从树集合中选择根结点的权值最小的 2 个树。重新构建一棵新二叉树,让刚选择出来的2 棵树的根结点成为这棵新树的左右子结点,新树的根结点的权值为 2 个左右子结点权值的和。构建完成后从树集合中删除原来 2个结点,并把新二叉树放入树集合中。

如下图所示。权值为 34的结点为新二叉树的左右子结点,新树根结点的权值为7

漫谈C++哈夫曼树的原理及实现

3.重复第二步,直到树集合中只有一个根结点为止。

漫谈C++哈夫曼树的原理及实现

漫谈C++哈夫曼树的原理及实现

漫谈C++哈夫曼树的原理及实现

漫谈C++哈夫曼树的原理及实现

漫谈C++哈夫曼树的原理及实现

当集合中只存在一个根结点时,停止构建,并且为最后生成树的每一个非叶子结点的左结点分支标注0,右结点分支标注1。如下图所示:

漫谈C++哈夫曼树的原理及实现

通过上述从下向上的思想构建出来的二叉树,可以保证权值较小的结点离根结点较远,权值较大的结点离根结点较近。最终二叉树的带权路径长度: wpl=(3+4)*5+6*4+(8+9+12)*3+(21+22)*2=232 。并且此树的带权路径长度是所有可能构建出来的二叉树中最小的。

上述的构建思想即为哈夫曼树设计思想,不同权值的字符编码就是结点路径上01的顺序组合。如下表所述,权值越大,其编码越小,权值越小,其编码越大。其编码长度即从根结点到此叶结点的路径长度。

字符 权值 编码
a 3 11110
b 6 1110
c 12 110
d 9 001
e 4 11111
f 8 000
g 21 01
h 22 10

4. 编码实现

4.1 使用优先队列

可以把权值不同的结点分别存储在优先队列(priority queue)中,并且给与权重较低的结点较高的优先级(priority)。

具体实现哈夫曼树算法如下:

1.把n个结点存储到优先队列中,则n个节点都有一个优先权pi。这里是权值越小,优先权越高。

2.如果队列内的节点数>1,则:

  • 从队列中移除两个最小的结点。
  • 产生一个新节点,此节点为队列中移除节点的父节点,且此节点的权重值为两节点之权值之和,把新结点加入队列中。
  • 重复上述过程,最后留在优先队列里的结点为哈夫曼树的根节点(root)。

完整代码:

#include   #include   #include   using namespace std;  //树结点  struct treenode {  	//结点权值  	float weight;  	//左结点  	treenode *lelfchild;  	//右结点  	treenode *rightchild;      //初始化  	treenode(float w) {  		weight=w;  		lelfchild=null;  		rightchild=null;      }  };  //为优先队列提供比较函数  struct comp {  	bool operator() (treenode * a, treenode * b) {          //由大到小排列  		return a->weight > b->weight;   	}  };    //哈夫曼树类  class hfmtree {  	private:           //优先队列容器  		priority_queue,comp> hfmqueue;  	public:  		//构造函数,构建单根结点树  		hfmtree(int weights[8]) {  			for(int i=0; i<8; i++) {  				//创建不同权值的单根树  				treenode *tn=new treenode(weights[i]);  				hfmqueue.push(tn);  			}  		}  		//显示队列中的最一个结点  		treenode* showhfmroot() {  			treenode *tn;  			while(!hfmqueue.empty()) {  				tn= hfmqueue.top();  				hfmqueue.pop();  			}  			return tn;  		}  		//构建哈夫曼树  		void create() {               //重复直到队列中只有一个结点  			while(hfmqueue.size()!=1) {  				//从优先队列中找到权值最小的 2 个单根树  				treenode *minfirst=hfmqueue.top();  				hfmqueue.pop();  				treenode *minsecOnd=hfmqueue.top();  				hfmqueue.pop();  				//创建新的二叉树  				treenode *newroot=new treenode(minfirst->weight+minsecond->weight);  				newroot->lelfchild=minfirst;  				newroot->rightchild=minsecond;  				//新二叉树放入队列中  				hfmqueue.push(newroot);  			}  		}  		//按前序遍历哈夫曼树的所有结点  		void showhfmtree(treenode *root) {  			if(root!=null) {  				cout<weight<lelfchild);  				showhfmtree(root->rightchild);  			}  		}  		//析构函数  		~hfmtree() {              //省略  		}  };    //测试  int main(int argc, char** argv) {  	//不同权值的结点  	int weights[8]= {3,6,12,9,4,8,21,22};      //调用构造函数  	hfmtree hfmtree(weights);      //创建哈夫曼树  	hfmtree.create();      //前序方式显示哈夫曼树  	treenode *root= hfmtree.showhfmroot();  	hfmtree.showhfmtree(root);  	return 0;  }  

显示结果:

漫谈C++哈夫曼树的原理及实现

上述输出结果,和前文的演示结果是一样的。

此算法的时间复杂度为o(nlogn)。因为有n个结点,所以树总共有2n-1个节点,使用优先队列每个循环须o(log n)

4.2 使用一维数组

除了上文的使用优先队列之外,还可以使用一维数组的存储方式实现。

在哈夫曼树中,叶子结点有 n个,非叶子结点有 n-1个,使用数组保存哈夫曼树上所的结点需要 2n-1个存储空间 。其算法思路和前文使用队列的思路差不多。直接上代码:

#include   using namespace std;  //叶结点数量  const unsigned int n=8;  //一维数组长度  const unsigned int m= 2*n -1;  //树结点  struct treenode {  	//权值  	float weight;  	//父结点  	int parent;  	//左结点  	int leftchild;  	//右结点  	int rightchild;  };  class huffmantree {  	public:  		//创建一维数组  		treenode hfmnodes[m+1];  	public:  		//构造函数  		huffmantree(int weights[8]);  		~huffmantree( ) {    		}  		void findminnode(int k, int &s1, int &s2);  		void showinfo() {  			for(int i=0; ifindminnode(i-1, firstmin, secondmin);  		hfmnodes[firstmin].parent = i;  		hfmnodes[secondmin].parent = i;  		hfmnodes[i].leftchild = firstmin;  		hfmnodes[i].rightchild = secondmin;  		hfmnodes[i].weight = hfmnodes[firstmin].weight + hfmnodes[secondmin].weight;  	}  }  void huffmantree::findminnode(int k, int & firstmin, int & secondmin) {  	hfmnodes[0].weight = 32767;  	firstmin=secOndmin=0;  	for(int i=1; i<=k; i++) {  		if(hfmnodes[i].weight!=0 && hfmnodes[i].parent==-1) {  			if(hfmnodes[i].weight 

测试结果:

漫谈C++哈夫曼树的原理及实现

5. 总结

哈夫曼树是二叉树的应用之一,掌握哈夫曼树的建立和编码方法对解决实际问题有很大帮助。

以上就是漫谈c++哈夫曼树的原理及实现的详细内容,更多关于c++哈夫曼树的资料请关注<编程笔记>其它相关文章!

需要了解更多c/c++开发分享漫谈C++哈夫曼树的原理及实现,都可以关注C/C++技术分享栏目—编程笔记


推荐阅读
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 广度优先遍历(BFS)算法的概述、代码实现和应用
    本文介绍了广度优先遍历(BFS)算法的概述、邻接矩阵和邻接表的代码实现,并讨论了BFS在求解最短路径或最短步数问题上的应用。以LeetCode中的934.最短的桥为例,详细阐述了BFS的具体思路和代码实现。最后,推荐了一些相关的BFS算法题目供大家练习。 ... [详细]
  • 判断编码是否可立即解码的程序及电话号码一致性判断程序
    本文介绍了两个编程题目,一个是判断编码是否可立即解码的程序,另一个是判断电话号码一致性的程序。对于第一个题目,给出一组二进制编码,判断是否存在一个编码是另一个编码的前缀,如果不存在则称为可立即解码的编码。对于第二个题目,给出一些电话号码,判断是否存在一个号码是另一个号码的前缀,如果不存在则说明这些号码是一致的。两个题目的解法类似,都使用了树的数据结构来实现。 ... [详细]
author-avatar
手机用户2502877507
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有