目录
哈夫曼树:
- 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(HuffmanTree),或霍夫曼树。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
路径
- 从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径。
路径长
- 从一个结点到另一个结点所经过的边的数量,被我们称为两个结点之间的路径长度。
结点的带权路径长度
结点的带权路径长度=节点路径长*节点权重
如何构建?
当然构建方法有好多中这里用数组来构建,并且我们把元素的值作为权值
int arr[] = {3,1,7,6,13,8,29};
- 数组节点化
List<Node> nodes = new ArrayList<Node>();
for (int value : arr) {
nodes.add(new Node(value));
}
- 数组排序,当然需要在自己创建出来的Node类中实现Comparable接口实现compareTo方法才能让Collections类给你比较排序
Collections.sort(nodes); //[Node(1),Node(3),Node(6),Node(7),Node(8),Node(13),Node(29)]
- 取前两个Node构建二叉树,其权值等于元素值,根节点为这两个权值的和:
Node left = nodes.get(0);
Node right = nodes.get(1);
//构建新二叉树
Node parent = new Node(left.value+right.value);
parent.left = left;
parent.right = right;
- 剔除前两个(1,3)节点,加进新构建出来的树Node(4)
nodes.remove(left);
nodes.remove(right);
nodes.add(parent);
然后重新进行排序
- 此时
nodes = [Node(4), Node(6), Node(7), Node(8), Node(13), Node(29)]
当然Node(4)这个东西是有两个子节点的,以下同理
-
然后重复2到4这三个步骤:
随后nodes = [Node(7), Node(8), Node(10), Node(13), Node(29)]
-
重复2到4这三个步骤:
随后nodes = [Node(10), Node(13), Node(15), Node(29)]
-
重复2到4这三个步骤:
随后nodes = [Node(15), Node(23), Node(29)]
-
重复2到4这三个步骤:
随后nodes = [Node(29), Node(38)]
-
重复2到4这三个步骤:
随后nodes = [Node(64)]
-
最后最后,临界条件:
nodes.size()<=1;
,结束循环,返回nodes.get(0);
代码实现
public class 赫夫曼树 {
public static void main(String[] args) {
int arr[] = {1,3,6,7,8,13,29};
HuffmanNode huffmanTree = createHuffmanTree(arr);
huffmanTree.preOrder();
}
public static HuffmanNode createHuffmanTree(int [] arr) {
//遍历数组将每个元素构建成HuffmanNode放入ArrayList中
List<HuffmanNode> HuffmanNodes = new ArrayList<HuffmanNode>();
for (int value : arr) {
HuffmanNodes.add(new HuffmanNode(value));
}
//开始构建
while (HuffmanNodes.size()>1) {
Collections.sort(HuffmanNodes);
HuffmanNode left = HuffmanNodes.get(0);
HuffmanNode right = HuffmanNodes.get(1);
//构建新二叉树
HuffmanNode parent = new HuffmanNode(left.value+right.value);
parent.left = left;
parent.right = right;
HuffmanNodes.remove(left);
HuffmanNodes.remove(right);
HuffmanNodes.add(parent);
}
return HuffmanNodes.get(0);
}
}
class HuffmanNode implements Comparable<HuffmanNode>{
int value;//权重
HuffmanNode left;
HuffmanNode right;
public HuffmanNode(int value) {
this.value = value;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "HuffmanNode [value=" + value + "]";
}
@Override
public int compareTo(HuffmanNode HuffmanNode) {
// 从小到大排序
return this.value - HuffmanNode.value;
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left!=null) {
this.left.preOrder();
}
if (this.right!=null) {
this.right.preOrder();
}
}
}
哈夫曼编码
哈夫曼编码作用
前面讲了哈夫曼树的构建,那它到底有什么用呢?
- 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
- 赫夫曼码是可变字长编码(VLC)的一种。 Huffman于1952年提出一种编码方法,称之为最佳编码
通过计算字符出现的次数用来做权值,构建哈夫曼树,这样我们最多出现的字符就可以用较短的二进制来编码
比如:如果字符P
出现次数最多,这时我们哈夫曼编码用01(当然这个01是不一定的,我随便说的)二进制码位表示字符P
,而如果使用P
的ASCII编码:01010000
,将极有可能会使得文件很大。
举个例子:我有一个字符串PPPPPPPPPPPP
要存储到本地文件
如果我用ASCII编码存,那将要存12*8/8=12
个字节
如果用哈夫曼编码存,只要存12*2/8=3
个字节
- 也许有人会说这样会不会导致二义性,
比如:如果字符a
用100
表示,字符b
用1001
表示,那就会出现二义性,计算机扫描到100
时就得到a
了,万一后面有个1
呢-_-? - 答:永远不会,这就是哈夫曼编码神奇的地方,通过刚刚构建出来的哈夫曼树稍微改一下分析下为什么:
- 发现什么了没有?所有的有实际意义的节点(即红色节点)都是叶子节点,不存在前缀路径相同的情况!也就是说不存再红接红的情况!
不清楚的话我再举个例子:上面的图可变成哈夫曼编码表如下(从左往右权重减小,这里表示出现频次逐渐减小):
I:0
,L:111
,O:101
,V:100
,E:1101
,U:11001
,!:11000
当计算机扫描一个二进制如1001101101
- 扫描下一位为
1
,发现哈夫曼编码表里有几个是1
开头的但是没有为1
编码的字符,继续 - 扫描下一位为
0
,发现哈夫曼编码表里有几个是10
开头的但是也没有为10
编码的字符,继续 - 扫描下一位为
0
,发现哈夫曼编码表里有个是100
的编码字符,结束得到V
- 看到了把尽管你前面那几个位是相同的,但计算机是怎么也找不到那几个位的编码出的字符滴。(真实太神奇了┗|`O′|┛ 嗷~~)
代码实现
主要修改了HuffmanNode的结构和增加了getCode()方法
public class 赫夫曼树 {
public static void main(String[] args) {
HashMap<Integer, Character> hashMap = new HashMap<Integer, Character>();
hashMap.put(29, 'I');
hashMap.put(13, 'L');
hashMap.put(8, 'O');
hashMap.put(7, 'V');
hashMap.put(6, 'E');
hashMap.put(3, 'U');
hashMap.put(1, '!');
HuffmanNode huffmanTree = createHuffmanTree(hashMap);
huffmanTree.preOrder();
Map<Character, String> codes = getCodes(huffmanTree);
System.out.println("哈夫曼编码表:" + codes);
}
public static HuffmanNode createHuffmanTree(Map<Integer, Character> map) {
// 遍历数组将每个元素构建成HuffmanNode放入ArrayList中
List<HuffmanNode> HuffmanNodes = new ArrayList<HuffmanNode>();
for (Map.Entry<Integer, Character> entry : map.entrySet()) {
HuffmanNodes.add(new HuffmanNode(entry.getKey(), entry.getValue()));
}
// 开始构建
while (HuffmanNodes.size() > 1) {
Collections.sort(HuffmanNodes);
HuffmanNode left = HuffmanNodes.get(0);
HuffmanNode right = HuffmanNodes.get(1);
// 构建新二叉树
HuffmanNode parent = new HuffmanNode(left.weight + right.weight, '\0');
parent.left = left;
parent.right = right;
HuffmanNodes.remove(left);
HuffmanNodes.remove(right);
HuffmanNodes.add(parent);
}
return HuffmanNodes.get(0);
}
// 保存路径即编码
static StringBuilder stringBuilder = new StringBuilder();
// 为了调用方便,我们重载getCodes
private static Map<Character, String> getCodes(HuffmanNode root) {
if (root == null) {
return null;
}
// 处理root的左子树
getCodes(root.left, "0", stringBuilder);
// 处理root的右子树
getCodes(root.right, "1", stringBuilder);
return huffmancodesMap;
}
// 保存编码对应关系
static Map<Character, String> huffmancodesMap = new HashMap<Character, String>();
/**
* 生成哈夫曼编码表
*
* @param node 传入节点
* @param code 哈夫曼编码,等同于路径
* @param stringBuilder 用于拼接路径
*/
public static void getCodes(HuffmanNode node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);
if (node != null) {
if (node.data == '\0') {// 必然同时存在左右子节点,因此左右都得递归
getCodes(node.left, "0", stringBuilder2);
getCodes(node.right, "1", stringBuilder2);
} else {// 说明是叶子节点
huffmancodesMap.put(node.data, stringBuilder2.toString());
}
}
}
}
class HuffmanNode implements Comparable<HuffmanNode> {
int weight;// 权重
char data;
HuffmanNode left;
HuffmanNode right;
public HuffmanNode(int value, char data) {
this.weight = value;
this.data = data;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "HuffmanNode [weight=" + weight + ",data=" + data + "]";
}
@Override
public int compareTo(HuffmanNode HuffmanNode) {
// 从小到大排序
return this.weight - HuffmanNode.weight;
}
// 前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
哈夫曼压缩
压缩过程
字符串 PPPPPPPPPPPP
->哈夫曼编码后的数组:010101010101010101010101
->转成byte数组byte b[]={125,125,125}
->存储
代码实现
public class 赫夫曼树 {
public static void main(String[] args) {
HashMap<Integer, Character> hashMap = new HashMap<Integer, Character>();
hashMap.put(29, 'I');
hashMap.put(13, 'L');
hashMap.put(8, 'O');
hashMap.put(7, 'V');
hashMap.put(6, 'E');
hashMap.put(3, 'U');
hashMap.put(1, '!');
HuffmanNode huffmanTree = createHuffmanTree(hashMap);
huffmanTree.preOrder();
Map<Character, String> codes = getCodes(huffmanTree);
System.out.println("哈夫曼编码表:" + codes);
String s = "ILOVEU!";
char[] charArray = s.toCharArray();
byte[] zipped = zip(charArray, codes);
for (byte b : zipped) {
System.out.println(b);
}
}
public static HuffmanNode createHuffmanTree(Map<Integer, Character> map) {
// 遍历数组将每个元素构建成HuffmanNode放入ArrayList中
List<HuffmanNode> HuffmanNodes = new ArrayList<HuffmanNode>();
for (Map.Entry<Integer, Character> entry : map.entrySet()) {
HuffmanNodes.add(new HuffmanNode(entry.getKey(), entry.getValue()));
}
// 开始构建
while (HuffmanNodes.size() > 1) {
Collections.sort(HuffmanNodes);
HuffmanNode left = HuffmanNodes.get(0);
HuffmanNode right = HuffmanNodes.get(1);
// 构建新二叉树
HuffmanNode parent = new HuffmanNode(left.weight + right.weight, '\0');
parent.left = left;
parent.right = right;
HuffmanNodes.remove(left);
HuffmanNodes.remove(right);
HuffmanNodes.add(parent);
}
return HuffmanNodes.get(0);
}
// 保存路径即编码
static StringBuilder stringBuilder = new StringBuilder();
// 为了调用方便,我们重载getCodes
private static Map<Character, String> getCodes(HuffmanNode root) {
if (root == null) {
return null;
}
// 处理root的左子树
getCodes(root.left, "0", stringBuilder);
// 处理root的右子树
getCodes(root.right, "1", stringBuilder);
return huffmancodesMap;
}
// 保存编码对应关系
static Map<Character, String> huffmancodesMap = new HashMap<Character, String>();
/**
* 生成哈夫曼编码表
*
* @param node 传入节点
* @param code 哈夫曼编码,等同于路径
* @param stringBuilder 用于拼接路径
*/
public static void getCodes(HuffmanNode node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);
if (node != null) {
if (node.data == '\0') {// 必然同时存在左右子节点,因此左右都得递归
getCodes(node.left, "0", stringBuilder2);
getCodes(node.right, "1", stringBuilder2);
} else {// 说明是叶子节点
huffmancodesMap.put(node.data, stringBuilder2.toString());
}
}
}
/**
* @param char 要编码的字节数组,需要自己将字符串转成char[]
* @param huffmanCodes 哈夫曼编码表
* @return 哈夫曼编码后的字节数组
*/
private static byte[] zip(char[] characters, Map<Character, String> huffmanCodes) {
// 1.利用huffmanCodes将bytes 转成赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
for (char c : characters) {
stringBuilder.append(huffmanCodes.get(c));
}
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
// 创建存储压缩后的byte数组
byte[] huffmanCodeBytes = new byte[len];
int index = 0;// 记录是第几个byte
for (int i = 0; i < stringBuilder.length(); i += 8) { // 因为是每8位对应-个byte,所以步长 +8
String strByte;
if (i + 8 > stringBuilder.length()) {// 不够8位
strByte = stringBuilder.substring(i);
} else {
strByte = stringBuilder.substring(i, i + 8);
}
// 将strByte转成一byte,放入到huffmanCodeBytes
//第二个参数表示我想把传入的strByte当成2进制数转成十进制
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanCodeBytes;
}
}
class HuffmanNode implements Comparable<HuffmanNode> {
int weight;// 权重
char data;
HuffmanNode left;
HuffmanNode right;
public HuffmanNode(int value, char data) {
this.weight = value;
this.data = data;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "HuffmanNode [weight=" + weight + ",data=" + data + "]";
}
@Override
public int compareTo(HuffmanNode HuffmanNode) {
// 从小到大排序
return this.weight - HuffmanNode.weight;
}
// 前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
补充
二进制知识
什么是符号位?
符号位就是二进制数中的最高位,如果为0
则是正数,为1
则是负数。
什么是无符号数和有符号数?
简单:无符号数最高位不做符号位所以只有整数。而有符号数最高位做符号位,在java中int类型(有4个字节)为有符号数,上限为(2*位数-1)-1
,下限为-(2*位数-1)
,
什么是溢出
溢出就是数位溢出到符号位,最高位溢出就抛弃。
比如:
有符号数01111111
,一旦加1
将会变成10000000
,直接变成下限。
如果是11111111
,加1
变成00000000
,直接变回0
为什么要有符号位?
因为计算机只有加法器(按位做加法),没有减法器。比如计算机做减法只能这么做3+(-2)
,但这有个问题您看:
明显不符合实际结果的真值,那计算机怎么运算的呢? 答:利用原码转成补码运算。
反码
- 正数的反码还是等于原码
- 负数的反码就是他的原码除符号位外,按位取反。
当然这只是反码求法
补码
- 正数的补码等于他的原码
- 负数的补码等于反码+1
到底数学家怎么退出这几种码的呢?那我只能赞叹人家智商了。
如何进行运算
- 只需把原码转成补码进行加法运算即可的到结果真值:
公式:原码+原码=补码+补码=补码
最后将补码转为十进制即可得到结果真值
如:
5+(-2)
(真值)
=00000101
(原码)+10000010
(原码)
=00000101
(补码)+11111110
(补码)
=00000011
(补码)
=3
(真值)