哈夫曼树的构造以及编码实现

哈夫曼树的介绍

Huffman Tree,中文名是哈夫曼树或霍夫曼树或者赫夫曼树,它是最优二叉树。

定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若树的带权路径长度达到最小,则这棵树被称为哈夫曼树。 

(01) 路径和路径长度

定义:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。 

(02) 结点的权及带权路径长度

定义:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。 

(03) 树的带权路径长度

定义:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。 

哈夫曼树的解析:

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,哈夫曼树的构造规则为:

1. 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点); 
2. 在森林中选出根结点的权值最小的两棵树进行合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和; 
3. 从森林中删除选取的两棵树,并将新树加入森林; 
4. 重复(02)、(03)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。


以{5,6,7,8,15}为例,来构造一棵哈夫曼树。

第1步:创建森林,森林包括5棵树,这5棵树的权值分别是5,6,7,8,15。 
第2步:在森林中,选择根节点权值最小的两棵树(5和6)来进行合并,将它们作为一颗新树的左右孩子(谁左谁右无关紧要,这里,我们选择较小的作为左孩子),并且新树的权值是左右孩子的权值之和。即,新树的权值是11。 然后,将"树5"和"树6"从森林中删除,并将新的树(树11)添加到森林中。 
第3步:在森林中,选择根节点权值最小的两棵树(7和8)来进行合并。得到的新树的权值是15。 然后,将"树7"和"树8"从森林中删除,并将新的树(树15)添加到森林中。 
第4步:在森林中,选择根节点权值最小的两棵树(11和15)来进行合并。得到的新树的权值是26。 然后,将"树11"和"树15"从森林中删除,并将新的树(树26)添加到森林中。 
第5步:在森林中,选择根节点权值最小的两棵树(15和26)来进行合并。得到的新树的权值是41。 然后,将"树15"和"树26"从森林中删除,并将新的树(树41)添加到森林中。 
此时,森林中只有一棵树(树41)。这棵树就是我们需要的哈夫曼树!

哈夫曼树的基本操作:

 在上述存储结构上实现的哈夫曼算法可大致描述为(设T的类型为HuffmanTree):
 (1)初始化
     将T[0..m-1]中2n-1个结点里的三个指针均置为空(即置为-1),权值置为0。
 (2)输入
     读入n个叶子的权值存于向量的前n个分量(即T[0..n-1])中。它们是初始森林中n个孤立的根结点上的权值。
 (3)合并
     对森林中的树共进行n-1次合并,所产生的新结点依次放人向量T的第i个分量中(n≤i≤m-1)。每次合并分两步:
     ①在当前森林T[0..i-1]的所有结点中,选取权最小和次小的两个根结点[p1]和T[p2]作为合并对象,这里0≤p1,p2≤i-1。
     ② 将根为T[p1]和T[p2]的两棵树作为左右子树合并为一棵新的树,新树的根是新结点T[i]。具体操作:
  将T[p1]和T[p2]的parent置为i,
  将T[i]的lchild和rchild分别置为p1和p2
  新结点T[i]的权值置为T[p1]和T[p2]的权值之和。
  注意:
     合并后T[pl]和T[p2]在当前森林中已不再是根,因为它们的双亲指针均已指向了T[i],所以下一次合并时不会被选中为合并对象。


哈夫曼树的存储结构:

<span style="font-size:18px;">/*
哈夫曼树的存储结构,它也是一种二叉树结构,
这种存储结构既适合表示树,也适合表示森林。
*/
typedef struct Node
{
int weight;                //权值
int parent;                //父节点的序号,为-1的是根节点
int lchild,rchild;         //左右孩子节点的序号,为-1的是叶子节点
}HTNode,*HuffmanTree;          //用来存储哈夫曼树中的所有节点
typedef char **HuffmanCode;    //用来存储每个叶子节点的哈夫曼编码</span>

根据哈夫曼树的构建步骤,写出构建哈夫曼树的代码如下

<span style="font-size:18px;">/*
根据给定的n个权值构造一棵赫夫曼树,wet中存放n个权值
*/
HuffmanTree create_HuffmanTree(int *wet,int n)
{
//一棵有n个叶子节点的赫夫曼树共有2n-1个节点
int total = 2*n-1;
HuffmanTree HT = (HuffmanTree)malloc(total*sizeof(HTNode));
if(!HT)
{
printf("HuffmanTree malloc faild!");
exit(-1);
}
int i;
 
//以下初始化序号全部用-1表示,
//这样在编码函数中进行循环判断parent或lchild或rchild的序号时,
//不会与HT数组中的任何一个下标混淆
 
//HT[0],HT[1]...HT[n-1]中存放需要编码的n个叶子节点
for(i=0;i<n;i++)
{
HT[i].parent = -1;
HT[i].lchild = -1;
HT[i].rchild = -1;
HT[i].weight = *wet;
wet++;
}
 
//HT[n],HT[n+1]...HT[2n-2]中存放的是中间构造出的每棵二叉树的根节点
for(;i<total;i++)
{
HT[i].parent = -1;
HT[i].lchild = -1;
HT[i].rchild = -1;
HT[i].weight = 0;
}
 
int min1,min2; //用来保存每一轮选出的两个weight最小且parent为0的节点
//每一轮比较后选择出min1和min2构成一课二叉树,最后构成一棵赫夫曼树
for(i=n;i<total;i++)
{
select_minium(HT,i,min1,min2);
HT[min1].parent = i;
HT[min2].parent = i;
//这里左孩子和右孩子可以反过来,构成的也是一棵赫夫曼树,只是所得的编码不同
HT[i].lchild = min1;
HT[i].rchild = min2;
HT[i].weight =HT[min1].weight + HT[min2].weight;
}
return HT;
}</span>

上述代码中调用到了select_minium()函数,它表示从集合中选出两个最小的二叉树,下面列出两种求两个最小值的方法。

方法一代码如下:

<span style="font-size:18px;">/*
从HT数组的前k个元素中选出weight最小且parent为-1的两个,分别将其序号保存在min1和min2中
*/
void select_minium(HuffmanTree HT,int k,int &min1,int &min2)
{
   min1 = min(HT,k);
   min2 = min(HT,k);
}</span>

这里调用到的min()函数代码如下:

<span style="font-size:18px;">/*
从HT数组的前k个元素中选出weight最小且parent为-1的元素,并将该元素的序号返回
*/
int min(HuffmanTree HT,int k)
{
   int i = 0;
   int min;        //用来存放weight最小且parent为-1的元素的序号
   int min_weight; //用来存放weight最小且parent为-1的元素的weight值
 
//先将第一个parent为-1的元素的weight值赋给min_weight,留作以后比较用。
//注意,这里不能按照一般的做法,先直接将HT[0].weight赋给min_weight,
//因为如果HT[0].weight的值比较小,那么在第一次构造二叉树时就会被选走,
//而后续的每一轮选择最小权值构造二叉树的比较还是先用HT[0].weight的值来进行判断,
//这样又会再次将其选走,从而产生逻辑上的错误。
   while(HT[i].parent != -1)
   i++;
   min_weight = HT[i].weight;
   min = i;
 
//选出weight最小且parent为-1的元素,并将其序号赋给min
   for(;i<k;i++)
  {
     if(HT[i].weight<min_weight && HT[i].parent==-1)
      {
        min_weight = HT[i].weight;
        min = i;
      }
  }
 
   //选出weight最小的元素后,将其parent置1,使得下一次比较时将其排除在外。
   HT[min].parent = 1;
   return min;
}</span>


方法二代码如下:

<span style="font-size:18px;">/*
从HT数组的前k个元素中选出weight最小且parent为-1的两个,分别将其序号保存在min1和min2中
*/
void select_minium(HuffmanTree HT,int k,int &min1,int &min2)
{
int i = 0;
   Int tempMin;
while(HT[i].parent != -1)
i++;
min1 = HT[i].weight;
while(HT[i].parent != -1)
i++;
min2 = HT[i].weight;
if(min1>min2)
{
tempMin = min1;
min1=min2;
Min2=tempMin;
}
//选出weight最小且parent为-1的元素,并将其序号赋给min
for(;i<k;i++)
{
if(HT[i].weight<min1 && HT[i].parent==-1)//min1始终保存最小的,min2保存第二小的数据
{
min2 = min1;
min1 = HT[i].weight;
}else if(HT[i].weight<min2 && HT[i].parent==-1)
{
Min2 = HT[i].weight;
}
}
}</span>


构建了赫夫曼树,便可以进行赫夫曼编码了,要求赫夫曼编码,就需要遍历出从根节点到叶子节点的路径,下面给出两种遍历赫夫曼树求编码的方法。

1、 采用从叶子节点到根节点逆向遍历求每个字符的赫夫曼编码,代码如下

<span style="font-size:18px;">/*
从叶子节点到根节点逆向求赫夫曼树HT中n个叶子节点的赫夫曼编码,并保存在HC中
*/
void HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int n)
{
//用来保存指向每个赫夫曼编码串的指针
HC = (HuffmanCode)malloc(n*sizeof(char *));
if(!HC)
{
printf("HuffmanCode malloc faild!");
exit(-1);
}
 
//临时空间,用来保存每次求得的赫夫曼编码串
//对于有n个叶子节点的赫夫曼树,各叶子节点的编码长度最长不超过n-1
//外加一个'\0'结束符,因此分配的数组长度最长为n即可
char *code = (char *)malloc(n*sizeof(char));
if(!code)
{
printf("code malloc faild!");
exit(-1);
}
 
code[n-1] = '\0';  //编码结束符,亦是字符数组的结束标志
//求每个字符的赫夫曼编码
int i;
for(i=0;i<n;i++)
{
int current = i;           //定义当前访问的节点
int father = HT[i].parent; //当前节点的父节点
int start = n-1;           //每次编码的位置,初始为编码结束符的位置
//从叶子节点遍历赫夫曼树直到根节点
while(father != -1)
{
if(HT[father].lchild == current)   //如果是左孩子,则编码为0
code[--start] = '0';   
else                              //如果是右孩子,则编码为1      
code[--start] = '1';
current = father;
father = HT[father].parent;
}
 
//为第i个字符的编码串分配存储空间
HC[i] = (char *)malloc((n-start)*sizeof(char));
if(!HC[i])
{
printf("HC[i] malloc faild!");
exit(-1);
}
//将编码串从code复制到HC
strcpy(HC[i],code+start);
}
 
free(code); //释放保存编码串的临时空间
}</span>

2采用从根节点到叶子节点无栈非递归遍历赫夫曼树,求每个字符的赫夫曼编码,代码如下

<span style="font-size:18px;">/*
从根节点到叶子节点无栈非递归遍历赫夫曼树HT,求其中n个叶子节点的赫夫曼编码,并保存在HC中
*/
void HuffmanCoding2(HuffmanTree HT,HuffmanCode &HC,int n)
{
//用来保存指向每个赫夫曼编码串的指针
HC = (HuffmanCode)malloc(n*sizeof(char *));
if(!HC)
{
printf("HuffmanCode malloc faild!");
exit(-1);
}
 
//临时空间,用来保存每次求得的赫夫曼编码串
//对于有n个叶子节点的赫夫曼树,各叶子节点的编码长度最长不超过n-1
//外加一个'\0'结束符,因此分配的数组长度最长为n即可
char *code = (char *)malloc(n*sizeof(char));
if(!code)
{
printf("code malloc faild!");
exit(-1);
}
 
int cur = 2*n-2;    //当前遍历到的节点的序号,初始时为根节点序号
int code_len = 0;   //定义编码的长度
 
//构建好赫夫曼树后,把weight用来当做遍历树时每个节点的状态标志
//weight=0表明当前节点的左右孩子都还没有被遍历
//weight=1表示当前节点的左孩子已经被遍历过,右孩子尚未被遍历
//weight=2表示当前节点的左右孩子均被遍历过
int i;
for(i=0;i<cur+1;i++)
{
HT[i].weight = 0;  
}
 
//从根节点开始遍历,最后回到根节点结束
//当cur为根节点的parent时,退出循环
while(cur != -1)
{
//左右孩子均未被遍历,先向左遍历
if(HT[cur].weight == 0)  
{
HT[cur].weight = 1;    //表明其左孩子已经被遍历过了
if(HT[cur].lchild != -1) 
{   //如果当前节点不是叶子节点,则记下编码,并继续向左遍历
code[code_len++] = '0';
cur = HT[cur].lchild;
}
else
{   //如果当前节点是叶子节点,则终止编码,并将其保存起来
code[code_len] = '\0';
HC[cur] = (char *)malloc((code_len+1)*sizeof(char));
if(!HC[cur])
{
printf("HC[cur] malloc faild!");
exit(-1);
}
strcpy(HC[cur],code);  //复制编码串
}
}
 
//左孩子已被遍历,开始向右遍历右孩子
else if(HT[cur].weight == 1)  
{
HT[cur].weight = 2;   //表明其左右孩子均被遍历过了
if(HT[cur].rchild != -1)
{   //如果当前节点不是叶子节点,则记下编码,并继续向右遍历
code[code_len++] = '1';
cur = HT[cur].rchild;
}
}
 
//左右孩子均已被遍历,退回到父节点,同时编码长度减1
else
{
HT[cur].weight = 0;
cur = HT[cur].parent;
--code_len;
}
 
}
free(code);
}</span>

  • 31
    点赞
  • 110
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是构造哈夫曼树并根据哈夫曼树构造哈夫曼编码的步骤和示例代码: 1. 统计每个字符出现的频率,并将它们存储在一个字典中。 2. 将每个字符及其频率作为一个节点,构造一个森林。 3. 从森林中选出两个频率最小的节点,将它们合并成一个新节点,并将新节点的频率设置为两个节点的频率之和。将新节点插入森林中。 4. 重复步骤3,直到森林中只剩下一个节点,这个节点就是哈夫曼树的根节点。 5. 根据哈夫曼树构造哈夫曼编码。从根节点开始,遍历哈夫曼树,当遇到左子树时,在编码的末尾添加0,当遇到右子树时,在编码的末尾添加1。当遍历到叶子节点时,就得到了该字符的哈夫曼编码。 以下是示例代码: ```python # 定义节点类 class Node: def __init__(self, freq, char=None): self.freq = freq self.char = char self.left = None self.right = None # 统计字符频率 def count_freq(text): freq = {} for char in text: if char in freq: freq[char] += 1 else: freq[char] = 1 return freq # 构造哈夫曼树 def build_huffman_tree(freq): forest = [Node(freq[char], char) for char in freq] while len(forest) > 1: forest.sort(key=lambda x: x.freq) node1 = forest.pop(0) node2 = forest.pop(0) new_node = Node(node1.freq + node2.freq) new_node.left = node1 new_node.right = node2 forest.append(new_node) return forest[0] # 构造哈夫曼编码 def build_huffman_code(node, code=''): if node.char: print(node.char, code) else: build_huffman_code(node.left, code + '0') build_huffman_code(node.right, code + '1') # 示例 text = 'hello world' freq = count_freq(text) tree = build_huffman_tree(freq) build_huffman_code(tree) ``` 输出结果为: ``` d 00 h 01 r 100 e 1010 w 1011 o 110 l 1110 1111 ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值