[C/C++] 构造最优二叉树-赫夫曼(哈夫曼、Huffman)树算法实现

2024-02-26 07:38

本文主要是介绍[C/C++] 构造最优二叉树-赫夫曼(哈夫曼、Huffman)树算法实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、基本概念

1、赫夫曼(Huffman)树又称最优二叉树或最优搜索树,是一种带权路径长度最短的二叉树。在许多应用中,常常赋给树中结点一个有某种意义的实数,称此实数为该结点的权。从树根结点到该结点之间的路径长度与该结点上权的乘积称为结点的带权路径长度(WPL),树中所有叶子结点的带权路径长度之和称为该树的带权路径长度.

 

2、两结点间的路径:从一结点到另一结点所经过的结点序列;路径长度:从根结点到相应结点路径上的分支数目;树的路径长度:从根到每一结点的路径长度之和。

3、深度为k,结点数为n的二叉树,当且仅当每个结点的编号都与相同深度的满二叉树中从1n的结点一一对应时,称为完全二叉树。在结点数目相同的二叉树中完全二叉树是路径长度最短的二叉树。

4WPL最小的二叉树是最优二叉树(Huffman )

5、赫夫曼(Huffman)树的特征

① 当叶子上的权值均相同时,完全二叉树一定是最优二叉树。否则完全二叉树不一定是最优二叉树。

② 在最优二叉树中,权值越大的叶子离根越近。

③ 最优二叉树的形态不唯一,但WPL最小。 

   

如上图中,只有(d)才是赫夫曼树。其中,圆围中的数值代表权值。

二、算法思想

  (1)  以权值分别为W1,W2...Wn的n各结点,构成n棵二叉树T1,T2,...Tn并组成森林F={T1,T2,...Tn},其中每棵二叉树 Ti仅有一个权值为 Wi的根结点;

  (2) F中选取两棵根结点权值最小的树作为左右子树构造一棵新二叉树,并且置新二叉树根结点权值为左右子树上根结点的权值之和(根结点的权值=左右孩子权值之和,叶结点的权值= Wi

  (3) F中删除这两棵二叉树,同时将新二叉树加入到F;

  (4) 重复(2)(3)直到F中只含一棵二叉树为止,这棵二叉树就是Huffman树。

 

三、C语言描述

(1)我们用如下结构来存储赫夫曼树:

       用大小为2n-1的一维数组来存储哈夫曼树中的结点,其存储结构为:

[cpp] view plain copy
  1. #define n 100               //叶结点数目  
  2. #define m 2*n-1             //树中结点总数  
  3. typedef struct  
  4. {  
  5.     float weight;             //权值,设权值均大于零  
  6.     int lchild,rchild,parent;  //左右孩子及双亲指针  
  7. } HTNode;  
  8.  typedef HTNode HuffmanTree[m]; //哈夫曼树是一维数组  
 

      因为C语言数组的下界为0,用-1表示空指针。树中结点的lchildrchildparent不等于-1时,分别表示该结点的左、右孩子和双亲结点在数组中的下标。

设置parent域有两个作用:一是使查找某结点的双亲变得简单;二是可通过判定parent的值是否为-1来区分根与非根结点。

(2)哈夫曼算法的简要描述

在上述存储结构上实现的哈夫曼算法可大致描述为(T的类型为HuffmanTree)

①初始化 将T[0m-1]2n-1个结点里的三个指针均置为空(即置为-1),权值置为0

②输入 读入n个叶子的权值存于数组的前n个分量(T[0n-1])中。它们是初始森林中n个孤立的根结点上的权值。

③合并 对森林中的树共进行n-1次合并,所产生的新结点依次放入数组T的第i个分量中(nim-1)。每次合并分两步:

1) 在当前森林T[0i-1]的所有结点中,选取权值最小和次小的两个根结点T [p1]T[p2]作为合并对象,这里0p1p2i-1

        2) 将根为T[p1]T[p2]的两棵树作为左右子树合并为一棵新的树,新树的根是新结点T[i]

具体操作:

T[p1]T[p2]parent置为i

T[i]lchildrchild分别置为p1p2

新结点T[i]的权值置为T[p1]T[p2]的权值之和。

合并后T[pl]T[p2]在当前森林中已不再是根,因为它们的双亲指针均已指向了T[i],所以下一次合并时不会被选中为合并对象。

(3)赫夫曼算法的数组法构造

[cpp] view plain copy
  1. void CreateHuffmanTree(HuffmanTree T)  
  2. {  
  3.     int i,p1,p2;                        //构造哈夫曼树,T[m-1]为其根结点  
  4.     InitHuffmanTree(T);       //T初始化  
  5.     InputWeight(T);               //输入叶子权值至T[0..n-1]的weight域  
  6.     for(i=n;i<m;i++)  
  7.     {    
  8.          SelectMin(T,i-1,&p1,&p2);//共进行n-1次合并,新结点依次存于T[i]中  
  9.                  //在T[0…i-1]中选择两个权最小的根结点,其序号分别为p1和p2  
  10.         T[p1].parent=T[p2].parent=i;  
  11.         T[i].1child=p1;              //最小权的根结点是新结点的左孩子  
  12.         T[i].rchild=p2;               //次小权的根结点是新结点的右孩子  
  13.         T[i].weight=T[p1].weight+T[p2].weight;  
  14.     }//for  
  15. }//CreateHuffman  
 

四、C语言实现

[cpp] view plain copy
  1. #include "stdio.h"  
  2. #include "stdlib.h"  
  3. #define m 100  
  4.    
  5. struct ptree              //定义二叉树结点类型  
  6. {  
  7. int w;                   //定义结点权值  
  8. struct ptree *lchild;     //定义左子结点指针  
  9. struct ptree *rchild;     //定义右子结点指针  
  10. };  
  11.    
  12. struct pforest              //定义链表结点类型  
  13. {  
  14. struct pforest *link;  
  15. struct ptree *root;  
  16. };  
  17.    
  18. int WPL=0;                  //初始化WTL为0  
  19. struct ptree *hafm();  
  20. void travel();  
  21. struct pforest *inforest(struct pforest *f,struct ptree *t);  
  22.    
  23. void travel(struct ptree *head,int n)                       
  24. {  
  25. //为验证harfm算法的正确性进行的遍历  
  26. struct ptree *p;  
  27. p=head;  
  28. if(p!=NULL)  
  29.  {  
  30.        if((p->lchild)==NULL && (p->rchild)==NULL) //如果是叶子结点  
  31.       {  
  32.               printf("%d ",p->w);  
  33.               printf("the hops of the node is: %d/n",n);  
  34.        WPL=WPL+n*(p->w);    //计算权值  
  35.          }//if  
  36. travel(p->lchild,n+1);  
  37. travel(p->rchild,n+1);  
  38. }//if  
  39. }//travel  
  40.    
  41. struct ptree *hafm(int n, int w[m])  
  42. {  
  43. struct pforest *p1,*p2,*f;  
  44. struct ptree *ti,*t,*t1,*t2;  
  45. int i;  
  46. f=(pforest *)malloc(sizeof(pforest));  
  47. f->link=NULL;  
  48. for(i=1;i<=n;i++)          //产生n棵只有根结点的二叉树  
  49.   {  
  50.        ti=(ptree*)malloc(sizeof(ptree));//开辟新的结点空间  
  51.     ti->w=w[i];               //给结点赋权值  
  52.     ti->lchild=NULL;  
  53.     ti->rchild=NULL;  
  54.     f=inforest(f, ti);  
  55.        //按权值从小到大的顺序将结点从上到下地挂在一颗树上        
  56.   }//for  
  57. while(((f->link)->link)!=NULL)//至少有二棵二叉树  
  58.   {  
  59.   p1=f->link;  
  60.   p2=p1->link;  
  61.   f->link=p2->link;           //取出前两棵树  
  62.   t1=p1->root;  
  63.   t2=p2->root;  
  64.   free(p1);                 //释放p1  
  65.   free(p2);                 //释放p2  
  66.   t=(ptree *)malloc(sizeof(ptree));//开辟新的结点空间  
  67.   t->w = (t1->w)+(t2->w);         //权相加  
  68.   t->lchild=t1;  
  69.   t->rchild=t2;             //产生新二叉树  
  70.   f=inforest(f,t);  
  71.   }//while  
  72.   p1=f->link;  
  73.   t=p1->root;  
  74.   free(f);  
  75.  return(t);                  //返回t  
  76.  }  
  77.    
  78. pforest *inforest(struct pforest *f,struct ptree *t)  
  79. {  
  80. //按权值从小到大的顺序将结点从上到下地挂在一颗树上        
  81. struct pforest *p, *q, *r;  
  82. struct ptree *ti;  
  83. r=(pforest *)malloc(sizeof(pforest)); //开辟新的结点空间  
  84. r->root=t;  
  85. q=f;  
  86. p=f->link;                
  87. while (p!=NULL)            //寻找插入位置  
  88.  {  
  89.    ti=p->root;  
  90.    if(t->w > ti->w)          //如果t的权值大于ti的权值  
  91.      {  
  92.          q=p;  
  93.       p=p->link;             //p向后寻找  
  94.      }//if  
  95.    else  
  96.       p=NULL;                  //强迫退出循环  
  97.   }//while  
  98. r->link=q->link;  
  99. q->link=r;                 //r接在q的后面  
  100. return(f);                 //返回f  
  101. }  
  102.    
  103. void InPut(int &n,int w[m])  
  104. {  
  105. printf("please input the sum of node/n"); //提示输入结点数  
  106. scanf("%d",&n);      //输入结点数  
  107. printf ("please input weight of every node/n"); //提示输入每个结点的权值  
  108. for(int i=1;i<=n;i++)  
  109. scanf("%d",&w[i]);  //输入每个结点权值  
  110. }  
  111.    
  112. int main( )  
  113. {  
  114. struct ptree *head;  
  115. int n,w[m];  
  116. InPut(n,w);  
  117. head=hafm(n,w);   
  118. travel(head,0);  
  119. printf("The length of the best path is WPL=%d", WPL);//输出最佳路径权值之和  
  120. return 1;  
  121. }  
 

五、最优二叉树的应用

哈夫曼树的应用很广,哈夫曼编码就是哈夫曼树在电讯通信中的应用之一。它采用不等长编码,让出现次数多的字符用短码,且任一编码不能是另一编码的前缀(我们称之为前缀编码,或非前缀码)。设有n种字符,每种字符出现的次数为Wi,其编码长度为Li (i=1,2,...n),则整个电文总长度为∑ Wi Li ,要得到最短的电文,即使得∑ Wi Li最小。也就是以字符出现的次数为权值,构造一棵Huffman树,并规定左分支编码位0,右分支编码为1,则字符的编码就是从根到该字符所在的叶结点的路径上的分支编号序列。用构造Huffman树编出来的码,称为Huffman编码。

为了获得传送电文的最短长度,可将字符出现的次数(频率)作为权值赋予该结点,构造一棵WPL最小的哈夫曼树,由此得到的二进制前缀编码就是最优前缀编码,也称哈夫曼编码。可以验证,用这样的编码传送电文可使总长最短。

 

       我们在修改程序时,只要将原来的travel改成如下即可实现赫夫曼编码,当然,译码原理也相同:

[cpp] view plain copy
  1. #define count 15  
  2. char ch[count][count];  
  3. int counter=0;  
  4. int tag,counter2=0;  
  5. void Initch()  
  6. {  
  7.     for(int i=0;i<count;i++)  
  8.             ch[i][count-1]='/0';  
  9. }  
  10. void travel(struct ptree *head,int n,int tag)                        
  11. {  
  12. //为验证harfm算法的正确性进行的遍历  
  13. struct ptree *p;   
  14. p=head;  
  15. if(p!=NULL)  
  16.  {  
  17.     if(tag==-1)   
  18.         {  
  19.         ch[counter][counter2++]='0';  
  20.         //for(int j=counter;j<count-1;j++) strcpy(ch[j+1],ch[counter]);  
  21.         }  
  22.     if(tag==+1)   
  23.         {  
  24.         ch[counter][counter2++]='1';  
  25.         for(int j=counter;j<count-1;j++) strcpy(ch[j+1],ch[counter]);  
  26.         }  
  27.     if((p->lchild)==NULL && (p->rchild)==NULL) //如果是叶子结点  
  28.       {   
  29.         printf("%d ",p->w);  
  30.         printf("the hops of the node is: %d ",n);  
  31.         printf("Code is %s/n",ch[counter]);  
  32.         counter++;  
  33.         counter2--;  
  34.         WPL=WPL+n*(p->w);    //计算权值  
  35.       }//if  
  36. travel(p->lchild,n+1,-1);  
  37. travel(p->rchild,n+1,+1);  
  38. }//if  
  39. }//travel  

这篇关于[C/C++] 构造最优二叉树-赫夫曼(哈夫曼、Huffman)树算法实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/748111

相关文章

使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)

《使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)》在现代软件开发中,处理JSON数据是一项非常常见的任务,无论是从API接口获取数据,还是将数据存储为JSON格式,解析... 目录1. 背景介绍1.1 jsON简介1.2 实际案例2. 准备工作2.1 环境搭建2.1.1 添加

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur