[C++ 学习笔记] 哈夫曼树与哈夫曼编码-Huffman Tree

1. 基本知识


哈夫曼树 (Huffman Tree),又称最优二叉树,是 带权路径长度最短 的树

路径和路径长度:

  1. 在一颗树中,从一个节点往下可以达到的子孙节点之间的通路,称为路径
  2. 通路中分支的数目称为路径长度
  3. 若规定根节点的深度为 1,则从根节点到第 L 层节点的路径长度为 L-1

图例:

如图中二叉树,根节点 与 红色节点的路径长度为 3

节点的带权路径长度和树的带权路径长度:

  1. 节点的带权路径长度,为从根结点到该结点之间的路径长度与该结点的权值的乘积
  2. 树的带权路径长度 (WPL),为所有叶子节点的带权路径长度之和

图例:

如图中二叉树,红色节点的带权路径长度为 12,树的带权路径长度 (WPL) 为4*3+2*2+3*1=19

哈夫曼编码是一种压缩编码的编码算法,是基于哈夫曼树的一种编码方式

那么哈夫曼编码跟 ASCII 编码有什么区别?ASCII 编码是对照 ASCII 表进行的编码,每一个字符符号都有对应的编码,其编码长度是固定的。而哈夫曼编码对于不同出现频率的字符,其使用的编码是不一样的,会对频率较高的字符使用较短的编码,频率低的字符使用较长的编码,这样保证总体使用的编码长度会更少,从而实现到了数据压缩的目的

那么如何将哈夫曼树与哈夫曼编码对应起来?

从根节点开始,向左走记为加上 '0' (可以更改为别的字符),向右走记为加上 '1' (可以更改为别的字符),走到叶子节点处记录下当前编码即为该叶子节点的哈夫曼编码

2. 初始化哈夫曼树


一般哈夫曼树的一个节点有如下属性:

  1. 当前节点的下标
  2. 左孩子的下标
  3. 右孩子的下标
  4. 当前节点的权值
  5. 当前节点代表的字符

于是可以得到节点的代码:

struct Node {
	int self;    // 当前节点的下标
	int lhs;    // 左孩子的下标
	int rhs;    // 右孩子的下标
	int weig;    // 当前节点的权值
	char c;    // 当前节点代表的字符
} HT[60];

3. 构建哈夫曼树


对于给定的字符串种类,及每种字符串的出现次数,大致建树思路如下:

  1. 将每个字符串的出现次数看做许多一一对应的根节点,即一个森林
  2. 每次从森林中选出 两个根节点权值最小 的树,作为一个新建节点的左右子树,且这个新建节点的权值等于其左、右子树根节点之和
  3. 从森林中删除选取的两棵树,再将新树加入森林
  4. 重复执行 1.2.3. 操作,直到森林中只省下一棵树,该树即为所求的哈夫曼树

图例:

对于以下给出的字符串种类,及每种字符串的出现次数,构建一棵哈夫曼树

abcd
5341

1. 将其看作有四棵树的森林(每棵树中都只有根节点)

2. 选出两棵根节点权值最小的树,作为一个新建节点的左右子树,并删除这两棵树,加入新树

3. 重复 1.2. 操作,直到森林中只剩下一棵树

4. 此时,这棵树即为所求的哈夫曼树,例如记向左走为加上 '0',向右走为加上 '1',此时各叶子节点的编码为

a(5)b(3)c(4)d(1)
011010111

于是可以得到构建哈夫曼树的代码:

// 对于给定字符串 (只包括小写字母),构建哈夫曼树

#include <bits/stdc++.h>
using namespace std;
int n, m;    // 字符串的长度,共有多少个小写字母

int frequent[30] = {0};    // frequent[i] 指第 i 个小写字母的出现次数
string s, kkk = "abcdefghijklmnopqrstuvwxyz";

struct Node {    // 初始化哈夫曼树
	int self;
	int lhs;
	int rhs;
	int weig;
	char c;
} HT[60];

bool operator > (Node node1, Node node2) {    // 重载运算符,定义优先队列中结构体的比较逻辑
	return node1.weig > node2.weig;
}

priority_queue<Node, vector<Node>, greater<Node> > q;    // 使用优先队列模拟,因为其可以自动排序

int main() {
    scanf("%d%d", &n, &m);
    cin >> s;

    for (int i = 0; i < s.size(); i++) {
		frequent[s[i] - 'a']++;    // 统计单个小写字母的出现频率
	}

	for (int i = 0; i < m; i++) {
		HT[i] = Node{i, -1, -1, frequent[i], kkk[i]};    // 初始化,构建只有根节点的树
		q.push(HT[i]);    // 入队
	}

	int pos = m + 1;

	while (q.size() > 1) {    // 注意不是 !q.empty(),因为队列中只有一棵树时就应停止循环
		Node tmp1 = q.top();    // 找到两棵根节点权值最小的树
		q.pop();   // 出队
		Node tmp2 = q.top();
		q.pop();   // 出队

		HT[pos] = Node{pos, tmp1.self, tmp2.self, tmp1.weig + tmp2.weig, '*'};    // 构建新节点
		q.push(HT[pos]);    // 入队
		pos++;
	}

	return 0;
}

4. 获取哈夫曼编码


具体获取方法其实已经在 基本知识 中介绍过了,这里直接放代码,具体可以看注释

map <char, string> s_code;    // 定义一个 map 来存小写字母对应的编码

void dfs(int pos, string code) {    // 当前节点的位置,当前的编码
	if (HT[pos].lhs == -1 && HT[pos].rhs == -1) {    // 如果当前节点是叶子节点
		s_code[HT[pos].c] = code;    // 直接记录编码
		return;
	}

	dfs(HT[pos].lhs, code + "0");    // 向左子树搜索
	dfs(HT[pos].rhs, code + "1");    // 向右子树搜索
}

创作不易,各位看官若觉得有用就点个赞吧~

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值