参考:数据结构与算法基础(青岛大学-王卓)
传送门:
数据结构与算法_【1】概念引入(C++实现)
数据结构与算法_【2】线性表(顺序表链表)(C++实现)
数据结构与算法_【3】栈和队列(C++实现)
数据结构与算法_【4】串数组广义表(C++实现)
数据结构与算法_【5】树和二叉树(C++实现)
数据结构与算法_【6】树和森林(C++实现)
数据结构与算法_【7】哈夫曼树(C++实现)
数据结构与算法_【8】图(C++实现)
数据结构与算法_【9】查找(C++实现)
数据结构与算法_【10】排序(C++实现)
哈夫曼树
判断树:用于描述分类过程的二叉树
不同判断树的判断效率不同—>哈夫曼树(最优二叉树)
1 哈夫曼树基本概念
路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
结点路径的长度:两结点间路径上的分支数
树的路径长度:从树根到每一个结点的路径长度之和,记作:TL
权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和,记作WPL (注意只需要乘叶子结点一个权)
哈夫曼树:最优树!带权路径长度(WPL)最短的树
注意:“带权路径长度最短”是在“度”相同的树中比较而得的结果,因此有最优二叉树,最优三叉树之称等。
满二叉树不一定是哈夫曼树,具有相同带权结点的哈夫曼树不唯一。
2 哈夫曼树的构造算法
哈夫曼树中权越大的叶子离根越近
贪心算法:构造哈夫曼树时首先选择权值小的叶子结点
构造哈夫曼树算法步骤:
(1)构造森林全是根,(2)选用两小造新树,(3)删除两小添新人,(4)重复2、3剩单根
哈夫曼树的结点的度数为0或2,没有度为1的结点!
包含n各叶子结点的哈夫曼树中一定有2n-1个结点!
包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
3 哈夫曼树算法的实现
采用顺序存储结构->一维结构数组
结点类型定义:包含weigth,parent,lch,rch
代码:
void HuffmanTree::CreatHuffmanTree(float *weigth, int n)
{
if (n <= 1)
{
return;
}
//计算最终存储哈夫曼树所需的存储空间
int m = 2 * n - 1;
//创建存储空间
this->HT = new H_Node[m + 1];//从序号1开始储存,方便计算
//将原始结点输入到哈夫曼树存储空间内
for (int i = 1; i <= n; i++)
{
this->HT[i].weight = weigth[i-1];
}
//this->ShowHuffmanTree(n);
//构造哈夫曼树,
H_Node* p = HT;
for (int j = n + 1; j <= m; j++)
{
//提取未使用的,最小的两个结点
int S1 = 0, S2 = 0;
int count1 = 0, count2 = 0;
//提取 S1
for (int k = 1; k <= j - 1; k++)
{
if(p[k].partent == 0)
{
if (count1 == 0)//第一次循环时给S1赋初始值
{
S1 = k;
count1++;
}
if (p[S1].weight >= p[k].weight)
{
S1 = k;
}
}
}
//提取 S2
for (int h = 1; h <= j - 1; h++)
{
if(p[h].partent == 0 && h != S1)//如果双亲为0,才进行比较
{
if (count2 == 0)//第一次循环时给S2赋初始值
{
S2 = h;
count2++;
}
if (p[S2].weight >= p[h].weight)
{
S2 = h;
}
}
}
//cout << S1 << " " << S2 << endl;
HT[S1].partent = j;
HT[S2].partent = j;
HT[j].lch = S1;
HT[j].rch = S2;
HT[j].weight = HT[S1].weight + HT[S2].weight;
}
this->ShowHuffmanTree(m);
}
输出:
4 哈夫曼编码
设计一种编码方式:
什么样的前缀码能够使得电文总长最短? —>哈夫曼编码
编码步骤:
案例:
两个问题:
4 哈夫曼编码算法的实现
实现步骤:
先将数据存储到哈夫曼树中,然后遍历n个子结点,左子树路径赋值0,右子树路径赋值1,使用一个字符数组(大小为n-1,并且最后一位赋值‘\0’,因为n个数据,最多产生n-2个路径)存储遍历过程中的路径值,最后将路径值反转存储到编码数组中即可。
代码:
//哈夫曼树已经在构造函数中直接创建了,这里直接使用即可!!!
void HuffmanTree::CreatHuffmanCode(float* weight, int n)
{
H_Node* HT = CreatHuffmanTree(weight, n);
for (int i = 1; i <= n; i++)
{
char* cd = new char[n];//建立存放临时编码的字符串,反向存储,正向读取
cd[n - 1] = '\0';
int start = n - 1;
int c = i;//临时存放结点的索引
int f = HT[i].partent;//保存结点的双亲索引
while (f != 0)//双亲不为0,说明没有编码结束,一直执行
{
--start;//临时字符串数组的指针先指向倒数第二个位置
if (HT[f].lch == c)//左节点给0
{
cd[start] = '0';
}
else//右结点给1
{
cd[start] = '1';
}
c = f;
f = HT[f].partent;
}
//将临时存储的数组,正向复制到code属性中
HT[i].code = new char[n - start];
for (int j = start; j <= n - 1; j++)
{
HT[i].code[j-start] = cd[j];
}
//delete cd;
}
this->ShowHuffmanCode(n);
}
输出:
5 解码
6 完整代码(可直接运行)
#pragma once
#include<iostream>
using namespace std;
#include"seqList.hpp"
//#include"string.h"
class HuffmanTree;
class H_Node {
private:
float weight;
int partent, lch, rch;
//string ch;
char *code;//定义存储哈夫曼编码的字符串指针备用
public:
friend class HuffmanTree;//友元类,能够访问私有属性
H_Node()//构造函数
{
this->weight = 0;
this->partent = 0;
this->lch = 0;
this->rch = 0;
this->code = NULL;
}
};
class HuffmanTree {
private:
H_Node *HT;
public:
HuffmanTree();//默认构造
H_Node* CreatHuffmanTree(float *weight,int n);//有参构造
void ShowHuffmanTree(int lenth);//打印哈夫曼树
void CreatHuffmanCode(float* weight,int n);//通过HC返回
void ShowHuffmanCode(int n);
~HuffmanTree();//析构函数
};
HuffmanTree::HuffmanTree()
{
}
//输入权值和结点数
H_Node* HuffmanTree::CreatHuffmanTree(float*weigth, int n)
{
if (n <= 1)
{
return NULL;
}
//计算最终存储哈夫曼树所需的存储空间
int m = 2 * n - 1;
//创建存储空间
this->HT = new H_Node[m + 1];//从序号1开始储存,方便计算
//将原始结点输入到哈夫曼树存储空间内
for (int i = 1; i <= n; i++)
{
this->HT[i].weight = weigth[i-1];
}
//this->ShowHuffmanTree(n);
//构造哈夫曼树,
H_Node* p = HT;
for (int j = n + 1; j <= m; j++)
{
//提取未使用的,最小的两个结点
int S1 = 0, S2 = 0;
int count1 = 0, count2 = 0;
//提取 S1
for (int k = 1; k <= j - 1; k++)
{
if(p[k].partent == 0)
{
if (count1 == 0)//第一次循环时给S1赋初始值
{
S1 = k;
count1++;
}
if (p[S1].weight >= p[k].weight)
{
S1 = k;
}
}
}
//提取 S2
for (int h = 1; h <= j - 1; h++)
{
if(p[h].partent == 0 && h != S1)//如果双亲为0,才进行比较
{
if (count2 == 0)//第一次循环时给S2赋初始值
{
S2 = h;
count2++;
}
if (p[S2].weight >= p[h].weight)
{
S2 = h;
}
}
}
//cout << S1 << " " << S2 << endl;
//存储双亲结点、孩子结点的index,构造新的结点
HT[S1].partent = j;
HT[S2].partent = j;
HT[j].lch = S1;
HT[j].rch = S2;
HT[j].weight = HT[S1].weight + HT[S2].weight;
}
this->ShowHuffmanTree(m);
return HT;
}
void HuffmanTree::ShowHuffmanTree(int lenth)
{
cout << "index" << "\t" << "weigth" << "\t" << "parent" << "\t" << "lch" << "\t" << "rch" << endl;
for (int i = 0; i <= lenth; i++)
{
cout << i << "\t" << HT[i].weight << "\t" << HT[i].partent << "\t" << HT[i].lch << "\t" << HT[i].rch << endl;
}
}
//哈夫曼树已经在构造函数中直接创建了,这里直接使用即可!!!
void HuffmanTree::CreatHuffmanCode(float* weight, int n)
{
H_Node* HT = CreatHuffmanTree(weight, n);
for (int i = 1; i <= n; i++)
{
char* cd = new char[n];//建立存放临时编码的字符串,反向存储,正向读取
cd[n - 1] = '\0';
int start = n - 1;
int c = i;//临时存放结点的索引
int f = HT[i].partent;//保存结点的双亲索引
while (f != 0)//双亲不为0,说明没有编码结束,一直执行
{
--start;//临时字符串数组的指针先指向倒数第二个位置
if (HT[f].lch == c)//左节点给0
{
cd[start] = '0';
}
else//右结点给1
{
cd[start] = '1';
}
c = f;
f = HT[f].partent;
}
//将临时存储的数组,正向复制到code属性中
HT[i].code = new char[n - start];
for (int j = start; j <= n - 1; j++)
{
HT[i].code[j-start] = cd[j];
}
//delete cd;
}
this->ShowHuffmanCode(n);
}
void HuffmanTree::ShowHuffmanCode(int n)
{
cout << "index" << "\t" << "weigth" << "\t" << "code" << endl;
for (int i = 1; i <= n; i++)
{
cout << i << "\t" << HT[i].weight << "\t";
//遍历打印字符串数组
char* p = HT[i].code;
while(*p)//判断是否到截止符号"\0"
{
cout << *(p++);
}
cout<<endl;
}
}
HuffmanTree::~HuffmanTree()
{
}
测试:
void test08()
{
HuffmanTree HT;
float weigth[7] = {0.4,0.3,0.15,0.05,0.04,0.03,0.03 };
int n = 7;
//HT.CreatHuffmanTree(weigth,n);
HT.CreatHuffmanCode(weigth, n);
}