哈夫曼树及哈夫曼编码到底是怎么回事儿?

哈夫曼树:

  • 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(HuffmanTree),或霍夫曼树。
  • 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近

路径

  • 从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径。

路径长

  • 从一个结点到另一个结点所经过的边的数量,被我们称为两个结点之间的路径长度。

结点的带权路径长度

  • 结点的带权路径长度=节点路径长*节点权重

如何构建?

当然构建方法有好多中这里用数组来构建,并且我们把元素的值作为权值
int arr[] = {3,1,7,6,13,8,29};

  1. 数组节点化
	List<Node> nodes = new ArrayList<Node>();
		for (int value : arr) {
			nodes.add(new Node(value));
		}
  1. 数组排序,当然需要在自己创建出来的Node类中实现Comparable接口实现compareTo方法才能让Collections类给你比较排序
Collections.sort(nodes); //[Node(1),Node(3),Node(6),Node(7),Node(8),Node(13),Node(29)]
  1. 取前两个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. 剔除前两个(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)这个东西是有两个子节点的,以下同理
  1. 然后重复2到4这三个步骤:
    在这里插入图片描述
    随后nodes = [Node(7), Node(8), Node(10), Node(13), Node(29)]

  2. 重复2到4这三个步骤:
    在这里插入图片描述
    随后nodes = [Node(10), Node(13), Node(15), Node(29)]

  3. 重复2到4这三个步骤:
    在这里插入图片描述
    随后nodes = [Node(15), Node(23), Node(29)]

  4. 重复2到4这三个步骤:
    在这里插入图片描述
    随后nodes = [Node(29), Node(38)]

  5. 重复2到4这三个步骤:
    在这里插入图片描述
    随后nodes = [Node(64)]

  6. 最后最后,临界条件: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();
		}
	}
}

哈夫曼编码

哈夫曼编码作用

前面讲了哈夫曼树的构建,那它到底有什么用呢?

  1. 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
  2. 赫夫曼码是可变字长编码(VLC)的一种。 Huffman于1952年提出一种编码方法,称之为最佳编码
    通过计算字符出现的次数用来做权值,构建哈夫曼树,这样我们最多出现的字符就可以用较短的二进制来编码
    比如:如果字符P出现次数最多,这时我们哈夫曼编码用01(当然这个01是不一定的,我随便说的)二进制码位表示字符P,而如果使用P的ASCII编码:01010000,将极有可能会使得文件很大。
     举个例子:我有一个字符串PPPPPPPPPPPP要存储到本地文件
     如果我用ASCII编码存,那将要存12*8/8=12个字节
     如果用哈夫曼编码存,只要存12*2/8=3个字节
  • 也许有人会说这样会不会导致二义性,
    比如:如果字符a100表示,字符b1001表示,那就会出现二义性,计算机扫描到100时就得到a了,万一后面有个1呢-_-?
  • 答:永远不会,这就是哈夫曼编码神奇的地方,通过刚刚构建出来的哈夫曼树稍微改一下分析下为什么:
    在这里插入图片描述
  • 发现什么了没有?所有的有实际意义的节点(即红色节点)都是叶子节点,不存在前缀路径相同的情况!也就是说不存再红接红的情况!
    不清楚的话我再举个例子:上面的图可变成哈夫曼编码表如下(从左往右权重减小,这里表示出现频次逐渐减小):
    I:0L:111O:101V:100E:1101U:11001!:11000
    当计算机扫描一个二进制如1001101101
  1. 扫描下一位为1,发现哈夫曼编码表里有几个是1开头的但是没有为1编码的字符,继续
  2. 扫描下一位为0,发现哈夫曼编码表里有几个是10开头的但是也没有为10编码的字符,继续
  3. 扫描下一位为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(真值)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值