哈夫曼树与哈夫曼编码

哈夫曼树与哈夫曼编码

哈夫曼博士

image-20230525100616874

判断树:用于分类过程的二叉树.

image-20230525100913829

如果采用右面的方法建立二叉树则需要比较31500次

我们还可以采用左边的方法建立树需要比较22000次

显然两种判别树的效率是不一样的

image-20230525101529603

如何找到效率最高的判别树?

这就是哈夫曼树(最优二叉树)

image-20230525101927169

哈夫曼树的基本概念

  1. 路径
  2. 结点的路径长度

image-20230528111119212

树的路径长度

从树根到每一个结点的路径长度之和.记作:TL

完全二叉树的路径长度最短,但路径最短的二叉树不一定是完全二叉树.

因为完全二叉树的结点层次是最小的.

image-20230528111503840

几个重要概念

  1. 结点的带权路径长度
  2. 树的带权路径长度

image-20230528112141976

例子

image-20230528112813910

哈夫曼树

最优树:带权路径长度(WPL)最短的树.

注意:只有度相同才能比较.

image-20230528113011408

image-20230528113304546

  1. 满二叉树不一定是哈夫曼树?
  2. 哈夫曼树重权越大的叶子结点离根越近
  3. 具有相同带权结点的哈夫曼树不唯一

image-20230528113452375

构造哈夫曼树

贪心算法

构造哈夫曼树时首先选择权值小的叶子结点.

image-20230529102110514

哈夫曼算法

  1. 构造森林全是根
  2. 选用两小造新树
  3. 删除两小添新人
  4. 重复2,3剩单根

这样就可以得到一个哈夫曼树.

image-20230529102457846

哈夫曼树中只有度为2或者度为0的结点,没有读为1的结点

n个叶子结点的哈夫曼树共有2n-1个结点.

image-20230529103231519

image-20230529103345121

例2

image-20230529104150900

总结

  1. n个结点通过n-1次合并最终形成哈夫曼树.
  2. 哈夫曼树中共有2n-1个结点

原本有n个结点,两两结合出现了n-1个新结点,同时因为这样两两结合的方式,产生的结点就没有度为1的结点.所以哈夫曼树共有2n-1个节点.

image-20230529104322802

哈夫曼树的构造算法

一维结构数组

采用一维结构数组

需要2n-1个结点的数组,不适用0下标.

结点结构有三部分组成,分别是权值,双亲结点,左孩子结点和右孩子结点.

image-20230531172108192

#include <bits/stdc++.h>

using namespace std;

typedef struct {
	int weight;//树的权重
	int parent;//树的双亲结点
	int lch;//左孩子
	int rch;//右孩子

} HTNode, *HuffmanTree;
int main () {
	HuffmanTree H = NULL;
	return 0;
}
  1. 构造森林全是根
  2. 选用两小造新树
  3. 删除两小添新人
  4. 重复2,3剩单根

image-20230531172446481

image-20230531173106405

算法代码实现

  1. 初始化 lch=rch=parent=0,这样的话每个结点都是孤立的一个结点.
  2. 输入初始n个叶子结点的weight值,需要放入权值进行贪心.

image-20230531173156598

image-20230531173430675

  1. 进行n-1次合并

image-20230531173559273

例子

image-20230531173942705

数组构造好二叉树之后的样子

image-20230531173959063

#include <bits/stdc++.h>

using namespace std;

typedef struct {//哈夫曼树的结点结构
	int weight;
	int parent;
	int lch;
	int rch;
} HTNode, *HuffamnTree;


/*
  该函数用来挑选出两个最小值的下标
 */
void Select(HuffamnTree HT, int length, int &s1, int &s2) {
	/*
	  先假定数组的前两位中的较小的是最小,较大的是次小
	  循环检查其余的元素,
	  若新元素小于最小,则,最小变为次小,新元素成为最小
	  否则,再判断新元素是否小于次小,是,则新元素变为次小
	  循环结束,输出两个数。
	 */
	int s1_min = 99999999;
	int s2_min = 99999999;
	for (int i = 1; i <= length; i++) {
		if (HT[i].parent == 0) {
			if (HT[i].weight < s1_min) {
				s2_min = s1_min;
				s1_min = HT[i].weight;
			} else if (HT[i].weight < s2_min) {
				s2_min = HT[i].weight;
			}
		}

	}
	//该循环找到了这个数组中的最小的权重,并且保证下来

	for (int i = 1; i <= length; i++) {
		if (HT[i].parent == 0) {
			if (HT[i].weight == s1_min) {
				s1 = i;
			}
		}
	}//该函数用来找到s1的下标
	for (int i = 1; i <= length; i++) {
		if (HT[i].parent == 0) {
			if (HT[i].weight == s2_min && i != s1) {//万一s1和s2相同,特殊判断一下
				s2 = i;
			}
		}
	}
//	cout << "两个最小值" << '\n';
//	cout << s1_min << ' ' << s2_min << '\n';
//	cout << "两个最大的id" << '\n';
//	cout << s1 << ' ' << s2 << '\n';
}

/*
  该函数用来构造哈夫曼树,需要哈夫曼数组的地址
 */
void CreateHuffmanTree(HuffamnTree &HT, int n) {
	if (n <= 1) {
		return;
	}

	int m = 2 * n - 1;//原本有n个数据但是要使用2*n-1个数组
	HT = new HTNode[m + 1]; //构造一个0---2n-1的数组,0号位置不用,多构造一位从下标1开始
	for (int i = 1; i <= m; i++) {//全部进行初始化
		HT[i].lch = 0;
		HT[i].rch = 0;
		HT[i].parent = 0; //初始化代码
	}
	for (int i = 1; i <= n; i++) {
		cin >> HT[i].weight; //读入每个结点的权值
	}
	//从第n+1个结点开始构造,一直到第m个结点
	for (int i = n + 1; i <= m; i++) {
		int s1 = 0, s2 = 0;//s1,s2保存前i-1个结点的最小的两个结点的下标
		Select(HT, i - 1, s1, s2);//挑选前i-1个结点的最小的两个结点的下标
		HT[s1].parent = i;
		HT[s2].parent = i;//让这两个结点的双亲指向i;
		HT[i].lch = s1;
		HT[i].rch = s2;//让第i个结点的左孩子右孩子指向s1和s2
		HT[i].weight = HT[s1].weight + HT[s2].weight;//当前结点的权重为两个结点的权重之和
//		printf("%d %d\n", s1, s2);
//		printf("%d %d\n", HT[s1].weight, HT[s2].weight);
//		printf("序号=%d  weight=%d  parent=%d   lch=%d  rch=%d\n", i, HT[i].weight, HT[i].parent, HT[i].lch, HT[i].rch);
	}
}


/*
  遍历哈夫曼树的函数

 */
void print(HuffamnTree HT, int n) {
	for (int i = 1; i <= 2 * n - 1; i++) {
		printf("序号=%d  weight=%d  parent=%d   lch=%d  rch=%d\n", i, HT[i].weight, HT[i].parent, HT[i].lch, HT[i].rch);
	}
}

int main () {
	HuffamnTree HT = NULL;
	CreateHuffmanTree(HT, 8);
	cout << HT << '\n';
	print(HT, 8);

	/*
测试样例
	   7 19 2 6 32 3 21 10
	  5 29 7 8 14 23 3 11
	 */
	return 0;
}

哈夫曼编码

为什么使用哈夫曼编码?

让出现次数较多的字符采用进可能短的编码.

同时使用次数越多的字母,编码越短,使用次数少的字母编码越长

image-20230608105447138

不能出现重码

要设计任一字符的编码都不是另一个字符的编码的前缀.否则就会重复.

这样的编码叫做前缀编码.

image-20230608110101400

采用哈夫曼编码

可以有效的解决上面的问题

image-20230608110323366

例题

image-20230608110907497

为什么哈夫曼编码是前缀编码?

为什么哈夫曼编码能保证字符编码总长最短?

image-20230608111429203

  1. 哈夫曼编码是前缀码
  2. 哈夫曼编码是最优前缀码

例题

image-20230608111927177

例题中的哈夫曼树

image-20230608112414092

哈夫曼编码的实现过程

image-20230907195832580

为了更加的方便我们使用了C++中的string对算法进行了具体实现

#include <bits/stdc++.h>
#include<windows.h>
#define yes cout<<"YES"<<'\n'
#define no 	cout<<"NO"<<'\n'

using namespace std;
typedef pair<int, int> PII;
const int N = 100008;

typedef struct {//哈夫曼树的结构体
	double weight;//权重
	int parent;
	int lch;
	int rch;
} HTNode, *HuffmanTree;
/*
  创建哈夫曼树的函数
  n为元素个数
 */
void Select(HuffmanTree HT, int length, int &s1, int &s2) {
//	先假定数组的前两位中的较小的是最小,较大的是次小
//	循环检查其余的元素,
//	若新元素小于最小,则,最小变为次小,新元素成为最小
//	否则,再判断新元素是否小于次小,是,则新元素变为次小
//	循环结束,输出两个数。
	double s1_min = 0x3f3f3f3f;
	double s2_min = 0x3f3f3f3f; //赋值为极大值
	for (int i = 1; i <= length; i++) {
		if (HT[i].parent == 0) {
			if (HT[i].weight < s1_min) {
				s2_min = s1_min;
				s1_min = HT[i].weight;
			} else if (HT[i].weight < s2_min) {
				s2_min = HT[i].weight;
			}
		}

	}
//先找到最小值下面开始找下标
	for (int i = 1; i <= length; i++) {
		if (HT[i].parent == 0) {
			if (HT[i].weight == s1_min) {
				s1 = i; //找到第一个
			}
		}
	}
	for (int i = 1; i <= length; i++) {
		if (HT[i].parent == 0) {
			if (HT[i].weight == s2_min && i != s1) { //避免s1_min和s2_min出现bug

				s2 = i;
			}

		}
    }
}
void CreatHuffman(HuffmanTree &HT, int n) {
	if (n <= 1) {
		return;
	}
	int m = 2 * n - 1;
	HT = new HTNode[m + 1]; //下标0不用
	for (int i = 1; i <= m; i++) { //初始化哈夫曼树
		HT[i].lch = 0;
		HT[i].rch = 0;
		HT[i].parent = 0;
	}
	//初始化结束
	for (int i = 1; i <= n; i++) {
		cin >> HT[i].weight; //读入权值
	}
	//下面构建哈夫曼树
	for (int i = n + 1; i <= m; i++) {
		int s1 = 0, s2 = 0;

		Select(HT, i - 1, s1, s2);
		HT[s1].parent = i;
		HT[s2].parent = i; //让s1,s2指向现在的双亲
		HT[i].lch = s1;
		HT[i].rch = s2; //第i个节点保存左孩子右孩子
		HT[i].weight = HT[s1].weight + HT[s2].weight;
	}
}
//void traverse(HuffmanTree HT, int n) {
//	for (int i = 1; i <= 2 * n - 1; i++) {
//		printf("序号=%d  weight=%d  parent=%d   lch=%d  rch=%d\n", i, HT[i].weight, HT[i].parent, HT[i].lch, HT[i].rch);
//	}
//
//}
void HuffmanCode(HuffmanTree HT, string HC[], int n) {
	for (int i = 1; i <= n; i++) {
		int f = HT[i].parent;
		int c = i;
		while (f != 0) {
			if (HT[f].lch == c) {
				HC[i] = HC[i] + '0';
			} else {
				HC[i] = HC[i] + '1';

			}
			c = f;//将c赋值为当前的f
			f = HT[f].parent;//f为当前f的双亲
		}
		reverse(HC[i].begin(), HC[i].end());
	}
}
int main () {
	int n;
	printf("请输入你要进行编码的字符的数量\n");
	cin >> n;
	HuffmanTree HT = NULL;
	printf("下面请您输入每个字符对应的权重\n");
	CreatHuffman(HT, n);
//	traverse(HT, 8);
	string HC[10000];

	HuffmanCode(HT, HC, n);
	printf("下面是生成的哈夫曼编码\n");
	for (int i = 1; i <= n; i++) {
		cout << HC[i] << '\n';
	}
	Sleep(5000);
	return 0;
}

文件的编码和译码

使用ascii码来编码

image-20230618121333711

使用哈夫曼编码

image-20230618121435152

编码

image-20230618121627755

  1. 输入各字符及其权值

  2. 构造哈夫曼树--HT[i]

  3. 进行哈夫曼编码--HC[I]

  4. 查询HC[i],得到各字符串的哈夫曼编码

    image-20230618121955010

解码

  1. 构造哈夫曼树
  2. 依次读入二进制码
  3. 读入0,则走左孩子;读入1,则走右孩子
  4. 一旦到达叶子结点时,即可翻译出字符
  5. 然后再从根出发继续译码,指导结束

按照字符频度表w构建哈夫曼树,求出原码报文OC

image-20230618122551924

posted @ 2023-09-08 21:56  harper886  阅读(198)  评论(0编辑  收藏  举报