背景
在看word2vec训练模型时发现它在优化cbow模型时采用了哈夫曼编码,不禁勾起了以前的回忆,趁着模糊的记忆,梳理一下哈夫曼树相关内容,在以前的文章介绍了线性表、图等结构,这次我们正好来介绍下树结构。
树
先来熟悉一下树有关的概念,它其实也只代表了每个数据节点之间的逻辑关系,每个节点中保存的是数据,数据类型可以多种多样,树代表了数据节点之间的逻辑关系。
叶子节点:
-
根节点
顾名思义,根节点正如树的树根,一棵树只有一个树根,向四面八方开散开来,长出来各种子树 -
内部节点
除去根节点、叶子结点以后的节点,内部结点既有孩子节点又有父节点 -
二叉树
在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。 -
满二叉树:即每个非叶子节点都有两个孩子的树结构
-
完全二叉树:每一层自上而下自左向右编号可以连起来(除了最后一层)
-
路径
在一个树中假设有两点A 、B,由A到B可能有很多种边连接顺这边到达B,经过的这些边连起来就是一条路径 -
路径长度
经过的边的个数即路径长度,如经过了3条边那么路径长度为3 -
带权路径长度
一个节点的带权路径长度等于该节点权重乘以根节点到这个节点的路径长度乘机,在哈夫曼编码中我们将每个编码放在了叶子节点,所以,要计算所有叶子节点的带权路径长度。想一下为什么都要放在叶子节点?
什么是哈夫曼树
哈夫曼树也属于二叉树的一种,只不过其具有一些特定性质,俗话说拥有了特殊性其价值也就出来了,二叉树到哈夫曼树可以说是普通到特殊的演化过程,它是不断优化整棵树的带权路径长度,当wpl达到最优的时候我们就称为最优带权路径二叉树,因为其实哈夫曼发明的也就有人称为哈夫曼树,如下图所示树的带权路径构成的树有很多种,其中带权路径长度最小的那个即哈夫曼树。
构建
构建过程也简单,核心思想是贪婪算法,贪婪的是什么要搞清楚,即在每一步解决子问题的时候都是找当前剩余问题中权重之和最小的两个子树结合,如下图:
结合上图归纳如下:
1.确定n个带有权重的节点,每个节点权重为wi,构成了一个只有单个根节点的森林
2.在森林中找到两个权重最小的组成一个二叉树,并且从森林中删除这两节点
3.重复步骤二,知道节点组成一个二叉树为止
应用
通信编码
作为通信编码的两个条件
1.编码发送方和解码接收方内容必须一样,不能有歧义
2.编码长度尽可能短
常用的编码方式有两种
- 等长编码
设字符集为A、B、C、D四个字符,二进制编码分别为00、01、10、11,假设有电文AABCCCCD要为其编码为0000011010101011,解码时两位截取一次即可,此种方法看似比较简单,缺点是编码长度不是最小,发文内容越长占用带宽越大,电报以前都是按文字收费还是挺贵的,为了节省成本还有不等长编码 - 不等长编码
不等长编码的思想是按发送电文尽可能短,如果想在等长编码基础上改进思考一下可以想到让频率出现太高的字符短编码即可,如上面可以让C编码变为一位0或1,不过可能出现问题因为在解码时并不知道哪个字符是几位,很容易造成歧义解码错误的情况发生,这与编码的唯一性条件不符合,既要减少编码的长度又要保持编码唯一性,唯一性的另一种理解就是任何一个字符的编码不能是另一个编码的前缀,可以叫做非前缀编码,其实叫前缀编码。
树结构哈夫曼树正好可以解决类似问题,让每个字符为叶子结点,从根节点到每个叶子节点都是只有一条路径,满足编码唯一性要求,同时叶子节点还有权重可以表示每个叶子节点的重要程度,这里可以是每个字符出现的次数或概率。
例题:
假设一个文本文件TFile中只包含7个字符{A,B,C,D,E,F,G},这7个字符在文本中出现的次数为{5,24,7,17,34,5,13}
利用哈夫曼树可以为文件TFile构造出符合前缀编码要求的不等长编码
具体做法:
- 将上面7个字符都作为叶子结点,每个字符出现次数作为该叶子结点的权值
- 规定哈夫曼树中所有左分支表示字符0,所有右分支表示字符1,将依次从根结点到每个叶子结点所经过的分支的二进制位的序列作为该
结点对应的字符编码 - 由于从根结点到任何一个叶子结点都不可能经过其他叶子,这种编码一定是前缀编码,哈夫曼树的带权路径长度正好是文件TFile编码
的总长度
例子
代码实现
下面是简单实现的哈夫曼编码以及解码过程,利用有序的优先队列实现,优先队列不用自己实现,java.uitl中有,当然也可以自己实现,原理是利用了最大堆最小堆,代码如下:
package sort.huffman;
public class BinaryTreeNode {
/**
* 节点的哈夫曼编码
*/
public String code = "";
/**
* 节点的数据
*/
public String data = "";
/**
* 节点权值
*/
public int count;
public BinaryTreeNode lChild;
public BinaryTreeNode rChild;
public BinaryTreeNode(String data, int count) {
this.data = data;
this.count = count;
}
public BinaryTreeNode(int count, BinaryTreeNode lChild, BinaryTreeNode rChild) {
this.count = count;
this.lChild = lChild;
this.rChild = rChild;
}
}
下面为哈夫曼实现过程
package sort.huffman;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.PriorityQueue;
public class HuffmanTree {
/**
* 压缩文件 可以是文件路径或网络文件
*/
private String sourceFile;
/**
* 二叉树的根节点
*/
private BinaryTreeNode root;
/**
* 存储不同字符以及权重
*/
private LinkedList<CharData> charList;
/**
* 优先队列存储huffman树节点
*/
private PriorityQueue<BinaryTreeNode> huffmanNodeQueue;
private class CharData {
int num = 1;
char c;
public CharData(char ch) {
c = ch;
}
}
/**
* 构建哈夫曼树
* @param sourceFile
*/
public void creatHfmTree(String sourceFile) {
try {
this.sourceFile = sourceFile;
charList = new LinkedList<CharData>();
getCharNum(sourceFile);
huffmanNodeQueue =new PriorityQueue<BinaryTreeNode>(charList.size(),
new Comparator<BinaryTreeNode>() {
@Override
public int compare(BinaryTreeNode o1, BinaryTreeNode o2) {
return o1.count - o2.count;
}
});
creatNodes();
creatTree();
root = huffmanNodeQueue.peek();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 统计出现的字符及其频率
* @param sourceFile
*/
private void getCharNum(String sourceFile) {
boolean flag;
for (int i = 0; i < sourceFile.length(); i++) {
char ch = sourceFile.charAt(i); // 从给定的字符串中取出字符
flag = true;
for (int j = 0; j < charList.size(); j++) {
CharData data = charList.get(j);
if (ch == data.c) {
data.num++;
flag = false;
break;
}
}
if (flag) {
charList.add(new CharData(ch));
}
}
}
/**
* 将出现的字符创建成单个的结点对象
*/
private void creatNodes() {
for (int i = 0; i < charList.size(); i++) {
String data = charList.get(i).c + "";
int count = charList.get(i).num;
BinaryTreeNode node = new BinaryTreeNode(data, count); // 创建节点对象
huffmanNodeQueue.add(node);
}
}
/**
* 构建哈夫曼树
*/
private void creatTree() {
while (huffmanNodeQueue.size() > 1) {
BinaryTreeNode left = huffmanNodeQueue.poll();
BinaryTreeNode right = huffmanNodeQueue.poll();
left.code = "0";
right.code = "1";
setCode(left);
setCode(right);
int parentWeight = left.count + right.count;
BinaryTreeNode parent = new BinaryTreeNode(parentWeight, left, right);
huffmanNodeQueue.add(parent);
}
}
/**
* 设置结点的哈夫曼编码
*
* @param root
*/
private void setCode(BinaryTreeNode root) {
if (root.lChild != null) {
root.lChild.code = root.code + "0";
setCode(root.lChild);
}
if (root.rChild != null) {
root.rChild.code = root.code + "1";
setCode(root.rChild);
}
}
/**
* 遍历
*
* @param node 节点
*/
private void output(BinaryTreeNode node) {
if (node.lChild == null && node.rChild == null) {
System.out.println(node.data + ": " + node.code);
}
if (node.lChild != null) {
output(node.lChild);
}
if (node.rChild != null) {
output(node.rChild);
}
}
/**
* 输出结果字符的哈夫曼编码
*/
public void output() {
output(root);
}
private String hfmCodeStr = "";// 哈夫曼编码连接成的字符串
/**
* 编码
*
* @param str
* @return
*/
public String toHufmCode(String str) {
for (int i = 0; i < str.length(); i++) {
String c = str.charAt(i) + "";
search(root, c);
}
return hfmCodeStr;
}
/**
* @param root 哈夫曼树根节点
* @param c 需要生成编码的字符
*/
private void search(BinaryTreeNode root, String c) {
if (root.lChild == null && root.rChild == null) {
if (c.equals(root.data)) {
hfmCodeStr += root.code; // 找到字符,将其哈夫曼编码拼接到最终返回二进制字符串的后面
}
}
if (root.lChild != null) {
search(root.lChild, c);
}
if (root.rChild != null) {
search(root.rChild, c);
}
}
// 保存解码的字符串
String result = "";
boolean target = false; // 解码标记
/**
* 解码
*
* @param codeStr
* @return
*/
public String CodeToString(String codeStr) {
int start = 0;
int end = 1;
while (end <= codeStr.length()) {
target = false;
String s = codeStr.substring(start, end);
matchCode(root, s); // 解码
// 每解码一个字符,start向后移
if (target) {
start = end;
}
end++;
}
return result;
}
/**
* 匹配字符哈夫曼编码,找到对应的字符
* @param root 哈夫曼树根节点
* @param code 需要解码的二进制字符串
*/
private void matchCode(BinaryTreeNode root, String code) {
if (root.lChild == null && root.rChild == null) {
if (code.equals(root.code)) {
result += root.data; // 找到对应的字符,拼接到解码字符穿后
target = true; // 标志置为true
}
}
if (root.lChild != null) {
matchCode(root.lChild, code);
}
if (root.rChild != null) {
matchCode(root.rChild, code);
}
}
public static void main(String[] args) {
HuffmanTree huffman = new HuffmanTree();// 创建哈弗曼对象
String data = "aaaaaaaabbbbccddd";
huffman.creatHfmTree(data);// 构造树
huffman.output(); // 显示字符的哈夫曼编码
// 将目标字符串利用生成好的哈夫曼编码生成对应的二进制编码
String hufmCode = huffman.toHufmCode(data);
System.out.println("编码:" + hufmCode);
// 将上述二进制编码再翻译成字符串
System.out.println("解码:" + huffman.CodeToString(hufmCode));
}
}
上面是否还记得我们写的一个例子 a(8) b(4) c(2) d(3) ,我们对这个字符编码看看结果和我们画的图是一致的。
贪心
哈夫曼编码是一种数据压缩技术,也可以理解为应用了贪心思想,再想一下利用贪心的图问题,如prim算法加权连通图最小生成树,它主要解决问题是以一种代价最低的方式连接n个点,可应用于通信网路、计算机网络等;另外还有一个Kruskal算法也是用来生成最小树,但是它的思路是不一样的,它是从边的维度出发,prim是从顶点的维度出发,他们的相同点是都应用了贪心思想,可见在算法领域里面贪心算法的思想还是非常有用的。
问题
除了哈夫曼编码还有其它编码问题吗?
huffman在word2vec cbow中的应用?
参考
https://www.cnblogs.com/tomhawk/p/7471133.html
https://www.cnblogs.com/13224ACMer/p/4706174.html