12.树结构-二叉树-赫夫曼树
二叉树
1.概念
1.数有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。
2.二叉树的子节点分为左节点和右节点。
3.如果该二叉树的所有叶子节点都在最后一层,并且总节点数为=2ⁿ-1,n为层数,则我们称为满二叉树
4.如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称之为完全二叉树。
2.遍历二叉树
1.前序遍历:先输出父节点,再遍历左子树和右子树
2.中序遍历:先遍历左子树,再输出父节点,再遍历右子树
3.后序遍历:先遍历左子树,在遍历右子树,最后输出父节点
小结:
看父节点的输出顺序,确定是前序、中序、后序
示例:
public class BinaryTreeDemo {
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "宋江");
HeroNode heroNode2 = new HeroNode(2, "无用");
HeroNode heroNode3 = new HeroNode(3, "卢俊义");
HeroNode heroNode4 = new HeroNode(4, "林冲");
root.setLeft(heroNode2);
root.setRight(heroNode3);
heroNode3.setRight(heroNode4);
BinaryTree tree=new BinaryTree(root);
//前序输出
System.out.println("前序输出=================");
tree.preOrder();
System.out.println("中序输出==================");
tree.infixOrder();
System.out.println("后序输出==================");
tree.postOrder();
}
}
/**
* 树结构
*/
class BinaryTree {
//根节点
HeroNode root;
public BinaryTree(HeroNode root) {
this.root = root;
}
/**
* 前序遍历
*/
public void preOrder() {
root.preOrder();
}
/**
* 中序遍历
*/
public void infixOrder() {
root.infixOrder();
}
/**
* 后续遍历
*/
public void postOrder() {
root.postOrder();
}
}
/**
* 实体节点
*/
class HeroNode {
//编号
private int no;
//名称
private String name;
//左节点
private HeroNode left;
//右节点
private HeroNode right;
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
public HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
public void setRight(HeroNode right) {
this.right = right;
}
/**
* 前序遍历
*/
public void preOrder() {
//1.先输出当前节点
System.out.println(this);
//2.输出左节点
if (this.getLeft() != null) {
this.getLeft().preOrder();
}
//2.输出右节点
if (this.getRight() != null) {
this.getRight().preOrder();
}
}
/**
* 中序遍历
*/
public void infixOrder() {
//1.输出左节点
if (this.getLeft() != null) {
this.getLeft().infixOrder();
}
//2.输出本节点
System.out.println(this);
//3.输出右节点
if (this.getRight() != null) {
this.getRight().infixOrder();
}
}
/**
* 后续遍历
*/
public void postOrder() {
//1.输出左节点
if (this.getLeft() != null) {
this.getLeft().postOrder();
}
//2.输出右节点
if (this.getRight() != null) {
this.getRight().postOrder();
}
//3.输出该节点
System.out.println(this);
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
输出;
前序输出=================
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='无用'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=4, name='林冲'}
中序输出==================
HeroNode{no=2, name='无用'}
HeroNode{no=1, name='宋江'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=4, name='林冲'}
后序输出==================
HeroNode{no=2, name='无用'}
HeroNode{no=4, name='林冲'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=1, name='宋江'}
1.前序遍历分析
2.中序遍历分析
3.后序遍历分析
查找指定节点
1.前序查找代码
1.实体代码
/**
* 实体节点
*/
class HeroNode {
...
/**
* 前序遍历
*
* @param no 查找的英雄编号
*/
public HeroNode preOrderSearch(int no) {
System.out.println("----->前序查找");
//1.判断当前对象是否符合
if (this.getNo() == no) {
return this;
}
HeroNode node = null;
//2.左遍历
if (this.getLeft() != null) {
node = this.getLeft().preOrderSearch(no);
}
//找到了结束递归
if (node != null) {
return node;
}
//3.没找到,右遍历
if (this.getRight() != null) {
node = this.getRight().preOrderSearch(no);
}
return node;
}
...
}
2.树的代码
/**
* 前序查找
*
* @param no 查找编号
* @return
*/
public HeroNode preOrderSearch(int no) {
return root.preOrderSearch(no);
}
3.查找代码
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "宋江");
HeroNode heroNode2 = new HeroNode(2, "无用");
HeroNode heroNode3 = new HeroNode(3, "卢俊义");
HeroNode heroNode4 = new HeroNode(4, "林冲");
HeroNode heroNode5 = new HeroNode(5, "关胜");
root.setLeft(heroNode2);
root.setRight(heroNode3);
heroNode3.setRight(heroNode4);
heroNode3.setLeft(heroNode5);
BinaryTree tree = new BinaryTree(root);
HeroNode heroNode = tree.preOrderSearch(5);
if (heroNode != null)
System.out.println(heroNode.toString());
}
输出:发现进行了四次比较
----->前序查找
----->前序查找
----->前序查找
----->前序查找
HeroNode{no=5, name='关胜'}
2.中序查找代码
1.实体类中的查找方法
/**
* 中序查找
*
* @param no 查找编号
*/
public HeroNode infixOrderSearch(int no) {
HeroNode node = null;
//1.左查找
if (this.getLeft() != null) {
node = this.getLeft().infixOrderSearch(no);
}
//查找到了
if (node != null) {
return node;
}
//2.查看当前节点
System.out.println("中序查找------->"+this.toString());
if (this.getNo() == no) {
return this;
}
//3.没找到,右遍历
if (this.getRight() != null) {
node = this.getRight().infixOrderSearch(no);
}
return node;
}
2.树类代码
/**
* 中序查找
*
* @param no 查找编号
*/
public HeroNode infixOrderSearch(int no) {
return root.infixOrderSearch(no);
}
3.测试;
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "宋江");
HeroNode heroNode2 = new HeroNode(2, "无用");
HeroNode heroNode3 = new HeroNode(3, "卢俊义");
HeroNode heroNode4 = new HeroNode(4, "林冲");
HeroNode heroNode5 = new HeroNode(5, "关胜");
root.setLeft(heroNode2);
root.setRight(heroNode3);
heroNode3.setRight(heroNode4);
heroNode3.setLeft(heroNode5);
BinaryTree tree = new BinaryTree(root);
HeroNode heroNode = tree.infixOrderSearch(5);
if (heroNode != null)
System.out.println(heroNode.toString());
}
输出:三次比较
中序查找------->HeroNode{no=2, name='无用'}
中序查找------->HeroNode{no=1, name='宋江'}
中序查找------->HeroNode{no=5, name='关胜'}
HeroNode{no=5, name='关胜'}
3.后序查找代码
实体类代码:
/**
* 后序查找
*
* @param no 查找编号
*/
public HeroNode postOrderSearch(int no) {
HeroNode node = null;
//找左边
if (this.getLeft() != null) {
node = this.getLeft().postOrderSearch(no);
}
if (node != null) {
return node;
}
//找右边
if (this.getRight() != null) {
node = this.getRight().postOrderSearch(no);
}
if (node != null) {
return node;
}
System.out.println("后序查找---->" + this.toString());
if (this.getNo() == no) {
node = this;
}
return node;
}
树代码:
/**
* 后序查找
*
* @param no 查找编号
*/
public HeroNode postOrderSearch(int no) {
return root.postOrderSearch(no);
}
测试代码:
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "宋江");
HeroNode heroNode2 = new HeroNode(2, "无用");
HeroNode heroNode3 = new HeroNode(3, "卢俊义");
HeroNode heroNode4 = new HeroNode(4, "林冲");
HeroNode heroNode5 = new HeroNode(5, "关胜");
root.setLeft(heroNode2);
root.setRight(heroNode3);
heroNode3.setRight(heroNode4);
heroNode3.setLeft(heroNode5);
BinaryTree tree = new BinaryTree(root);
HeroNode heroNode = tree.postOrderSearch(5);
if (heroNode != null)
System.out.println(heroNode.toString());
}
输出:两次比较
后序查找---->HeroNode{no=2, name='无用'}
后序查找---->HeroNode{no=5, name='关胜'}
HeroNode{no=5, name='关胜'}
二叉树-删除节点
代码示例
实体类代码:
/**
* 删除
*
* @param no 编号
*/
public void deleteByNo(int no) {
/*
思路:
1.因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否是删除的no,
2.如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将this.left=null;并返回(结束递归)
3.如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,就将this.right=null;并返回
4.如果第二,三步没有删除节点,就向左子树进行递归删除。
5.以上四步都没有删除节点,应该向右子树行递归删除。
*/
if (this.getLeft() != null && this.getLeft().getNo() == no) {
this.setLeft(null);
return;
}
if (this.getRight() != null && this.getRight().getNo() == no) {
this.setRight(null);
return;
}
if (this.getLeft() != null) {
this.getLeft().deleteByNo(no);
}
if (this.getRight() != null) {
this.getRight().deleteByNo(no);
}
}
树类代码:
/**
* 根据no删除
*
* @param no
*/
public void deleteByNo(int no) {
if (root.getNo() == no) {
root = null;
return;
}
root.deleteByNo(no);
}
测试类代码:
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "宋江");
HeroNode heroNode2 = new HeroNode(2, "无用");
HeroNode heroNode3 = new HeroNode(3, "卢俊义");
HeroNode heroNode4 = new HeroNode(4, "林冲");
HeroNode heroNode5 = new HeroNode(5, "关胜");
HeroNode heroNode6 = new HeroNode(6, "李青");
HeroNode heroNode7 = new HeroNode(7, "李逵");
root.setLeft(heroNode2);
root.setRight(heroNode3);
heroNode3.setRight(heroNode4);
heroNode3.setLeft(heroNode5);
heroNode5.setLeft(heroNode6);
heroNode4.setRight(heroNode7);
BinaryTree tree = new BinaryTree(root);
tree.preOrder();
System.out.println("删除=====>");
tree.deleteByNo(6);
tree.preOrder();
}
输出:发现6号节点已经删除
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='无用'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=5, name='关胜'}
HeroNode{no=6, name='李青'}
HeroNode{no=4, name='林冲'}
HeroNode{no=7, name='李逵'}
删除=====>
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='无用'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=5, name='关胜'}
HeroNode{no=4, name='林冲'}
HeroNode{no=7, name='李逵'}
问题:
即使左边找到节点已经删除,还是会回溯到右边进行删除!这是个问题,看后续学习看能解决不!
删除非叶子节点
tree.deleteByNo(3);
按照设计逻辑会把子树整个删掉
输出:
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='无用'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=5, name='关胜'}
HeroNode{no=6, name='李青'}
HeroNode{no=4, name='林冲'}
HeroNode{no=7, name='李逵'}
删除=====>
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='无用'}
对于上述改进,删除非叶子节点
删除5或者4
实体类改造:
/**
* 删除
*
* @param no 编号
*/
public void deleteByNo(int no) {
/*
思路:
改造点
*/
if (this.getLeft() != null && this.getLeft().getNo() == no) {
HeroNode heroNode = this.getLeft();
//1.叶子节点
if (heroNode.getLeft() == null && heroNode.getRight() == null) {
this.setLeft(null);
} else if (heroNode.getLeft() != null && heroNode.getRight() != null) {
HeroNode right = heroNode.getRight();
//非叶子节点,并且左右都有,左边节点代替删除节点
this.setLeft(heroNode.getLeft());
this.getLeft().setRight(right);
} else if (this.getLeft().getLeft() != null) {
//左子节点不为空,右节点为空
this.setLeft(heroNode.getLeft());
} else {
//左子节点为空,右节点不为空
this.setLeft(heroNode.getRight());
}
return;
}
//这个处理和上面一样
if (this.getRight() != null && this.getRight().getNo() == no) {
HeroNode heroNode = this.getRight();
//叶子节点
if (heroNode.getLeft() == null && heroNode.getRight() == null) {
this.setRight(null);
} else if (heroNode.getLeft() != null && heroNode.getRight() != null) {
HeroNode right = heroNode.getRight();
this.setRight(heroNode.getLeft());
this.getRight().setRight(right);
} else if (heroNode.getLeft() != null) {
this.setRight(heroNode.getLeft());
} else {
this.setRight(heroNode.getRight());
}
return;
}
if (this.getLeft() != null) {
this.getLeft().deleteByNo(no);
}
if (this.getRight() != null) {
this.getRight().deleteByNo(no);
}
}
测试:
public class BinaryTreeDemo {
public static void main(String[] args) {
HeroNode root = new HeroNode(1, "宋江");
HeroNode heroNode2 = new HeroNode(2, "无用");
HeroNode heroNode3 = new HeroNode(3, "卢俊义");
HeroNode heroNode4 = new HeroNode(4, "林冲");
HeroNode heroNode5 = new HeroNode(5, "关胜");
HeroNode heroNode6 = new HeroNode(6, "李青");
HeroNode heroNode7 = new HeroNode(7, "李逵");
HeroNode heroNode8 = new HeroNode(8, "刘丹");
HeroNode heroNode9 = new HeroNode(9, "吴孟达");
root.setLeft(heroNode2);
root.setRight(heroNode3);
heroNode3.setRight(heroNode4);
heroNode3.setLeft(heroNode5);
heroNode5.setLeft(heroNode6);
heroNode4.setRight(heroNode7);
heroNode5.setRight(heroNode8);
heroNode4.setLeft(heroNode9);
BinaryTree tree = new BinaryTree(root);
System.out.println("删除=====>");
tree.deleteByNo(5);
tree.preOrder();
}
输出:发现5号节点已删除,但是子树还在!
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='无用'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=5, name='关胜'}
HeroNode{no=6, name='李青'}
HeroNode{no=8, name='刘丹'}
HeroNode{no=4, name='林冲'}
HeroNode{no=9, name='吴孟达'}
HeroNode{no=7, name='李逵'}
删除=====>
HeroNode{no=1, name='宋江'}
HeroNode{no=2, name='无用'}
HeroNode{no=3, name='卢俊义'}
HeroNode{no=6, name='李青'}
HeroNode{no=8, name='刘丹'}
HeroNode{no=4, name='林冲'}
HeroNode{no=9, name='吴孟达'}
HeroNode{no=7, name='李逵'}
顺序存储二叉树
需求:
给定一个数组{1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历输出
/**
* 顺序存储二叉树方式的前序输出数组
*/
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int arr[] = {1, 2, 3, 4, 5, 6, 7};
System.out.println("原数组:"+ Arrays.toString(arr));
ArrBinaryTree tree=new ArrBinaryTree(arr);
tree.preOrder(0);
}
}
class ArrBinaryTree {
int arr[] = null;
public ArrBinaryTree(int[] arr) {
this.arr = arr;
}
/**
* 按照树的前序输出数组
*
* @param index 数组下标
*/
public void preOrder(int index) {
if (arr == null || arr.length == 0) {
return;
}
System.out.print(arr[index]+" ");
//左遍历
if ((2 * index + 1) < arr.length) {
preOrder(2 * index + 1);
}
//右遍历
if ((2 * index + 2) < arr.length) {
preOrder(2 * index + 2);
}
}
}
输出:
原数组:[1, 2, 3, 4, 5, 6, 7]
1 2 4 5 3 6 7
中序遍历和后序遍历只需要更改代码输出位置即可!
线索化二叉树
例如上述数列按照中序遍历时输出:{8,3,10,1,6,14}
6称为14的前驱节点,14称为6的后继节点,并不是按照树的父子节点来论的!
案例:
说明:node节点的left和right可能指向子树,也可能指向前驱、后继节点,具体如下
1.left指向的是左子树,也有可能指向前驱节点,例如1节点,left指向左子树,而10节点的left指向的就是前驱节点
2.right指向的是右子树,也可能指向后继节点,比如1节点right指向的是右子树,而10节点的right指向的是后继节点
样例代码:
public class ThreadedBinaryThreeDemo {
public static void main(String[] args) {
HeroNode root = new HeroNode(1);
HeroNode node3 = new HeroNode(3);
HeroNode node6 = new HeroNode(6);
HeroNode node8 = new HeroNode(8);
HeroNode node10 = new HeroNode(10);
HeroNode node14 = new HeroNode(14);
root.setLeft(node3);
root.setRight(node6);
node3.setLeft(node8);
node3.setRight(node10);
node6.setLeft(node14);
BinaryTree binaryTree=new BinaryTree(root);
binaryTree.threadedNodes(root);
System.out.println(node10.getRight());
}
}
class BinaryTree {
//根元素
private HeroNode root;
//上一个元素
private HeroNode pre;
public BinaryTree(HeroNode root) {
this.root = root;
}
//重点:线索化二叉树,这里好好理解下递归,不是很好理解
public void threadedNodes(HeroNode node) {
if (node == null) {
return;
}
//1.先线索化左子树
threadedNodes(node.getLeft());
//2.处理当前节点
//处理前驱节点
if (node.getLeft() == null) {
node.setLeft(pre);
node.setLeftType(1);
}
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightType(1);
}
pre = node;
//线索化右子树
threadedNodes(node.getRight());
}
}
class HeroNode {
private int no;
private HeroNode left;
private HeroNode right;
//左节点类型:0为子树, 1为前驱后继节点
private int leftType;
//左节点类型:0为子树, 1为前驱后继节点
private int rightType;
public HeroNode(int no) {
this.no = no;
}
public HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
public void setRight(HeroNode right) {
this.right = right;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
'}';
}
}
输出:符合预期
10的前驱节点是:HeroNode{no=3}
10的后继节点是:HeroNode{no=1}
理解:
线索化二叉树的遍历
说明:对前面的中序线索化的二叉树进行遍历
分析:因为线索化后,各个节点指向有所变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树。各个节点可以通过线性方式遍历
因此无需使用递归方式,这样也提高了遍历的效率,遍历的次序应当和中序遍历保持一致。
代码:
/**
* 遍历线索化二叉树
*/
public void threadedList() {
HeroNode node = root;
while (node != null) {
/*
循环找到type==1的节点
0为子树, 1为前驱后继节点
*/
while (node.getLeftType() == 0) {
node = node.getLeft();
}
//打印当前节点
System.out.println(node);
//如果当前节点的右指针指向的是后继节点,就一直输出
while (node.getRightType() == 1) {
node = node.getRight();
System.out.println(node);
}
node = node.getRight();
}
}
测试输出:
public static void main(String[] args) {
HeroNode root = new HeroNode(1);
HeroNode node3 = new HeroNode(3);
HeroNode node6 = new HeroNode(6);
HeroNode node8 = new HeroNode(8);
HeroNode node10 = new HeroNode(10);
HeroNode node14 = new HeroNode(14);
root.setLeft(node3);
root.setRight(node6);
node3.setLeft(node8);
node3.setRight(node10);
node6.setLeft(node14);
BinaryTree binaryTree = new BinaryTree(root);
binaryTree.threadedNodes(root);
System.out.println("10的前驱节点是:" + node10.getLeft());
System.out.println("10的后继节点是:" + node10.getRight());
System.out.println("线索化二叉树遍历--->");
binaryTree.threadedList();
}
输出:符合预期
10的前驱节点是:HeroNode{no=3}
10的后继节点是:HeroNode{no=1}
线索化二叉树遍历--->
HeroNode{no=8}
HeroNode{no=3}
HeroNode{no=10}
HeroNode{no=1}
HeroNode{no=14}
HeroNode{no=6}
思路:
树结构的实际应用
堆排序
堆排序基本思想
1.将待排序列构造成一个大顶堆
2.此时整个序列的最大值就是堆顶的根节点
3.将其以末尾的元素进行交换,此时末尾就是最大值。
4.然后将剩余n-1各元素重新构造成一个堆,这样就会得到n个元素的最次小值,如此反复执行,便能得到一个有序序列。
可以看到,在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
例子,如原始数组如{4,6,8,5,9},按照堆排序从小到大排序,(一般升序采用大顶堆,降序采用小顶堆)
1.
2.
3.
4.
步骤二:将堆顶元素和末尾元素进行交换,使末尾元素最大,然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素,如此反复交换,重建,交换
3.
简单总结:
1.将无序序列构建成一个堆,根据升序降序需求,选择大顶堆或者小顶堆。
2.将堆顶元素与末尾元素交换,将最大的元素沉到数组末端。
3.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
堆排序的代码实现未完成,后续学习!
public class HeapSort {
public static void main(String[] args) {
int arr[] = {4, 6, 8, 5, 9};
heapSort(arr);
}
public static void heapSort(int arr[]) {
int temp=0;
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustheap(arr, i, arr.length);
}
for (int j = arr.length - 1; j > 0; j--) {
temp=arr[j];
arr[j]=arr[0];
arr[0]=temp;
adjustheap(arr,0,j);
}
System.out.println("数组="+Arrays.toString(arr));
}
public static void adjustheap(int arr[], int i, int length) {
int temp = arr[i];
//int k = i * 2 + 1 k是i节点的左子节点
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) {
//左子节点小于右子节点,k指向右子节点
k++;
}
if (arr[k] > temp) {
arr[i] = arr[k];
i = k;
} else {
break;
}
//当for循环结束,已经将以i为父节点的树的最大值,放在最顶(局部)
arr[i] = temp;
}
}
}
输出:符合预期
数组=[4, 5, 6, 8, 9]
速率测试:
public static void main(String[] args) {
int[] arr = new int[80000];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 80000);
}
Date date1=new Date();
heapSort(arr);
Date date2=new Date();
System.out.println("堆排序耗时:"+(date2.getTime()-date1.getTime()));
System.out.println("排序结果:"+Arrays.toString(arr));
}
输出:
堆排序耗时:12
排序结果:[0, 1, 1, ...]
速率是非常的快!!!
霍夫曼树
基本介绍:
1.给定n个权值作为n个叶子节点,构造一颗二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称之为哈夫曼树
2.霍夫曼树是带权路径长度最短的树,权值较大的节点离根较近。
组成一个赫夫曼树步骤:
1.
原数组排序为{1,3,6,7,8,13,29}
2.
3.完成霍夫曼树
代码实现
package cn.com.tree;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 创建霍夫曼树
*/
public class HuffmanTree {
public static void main(String[] args) {
int arr[] = {13, 7, 8, 3, 29, 6, 1};
System.out.println("原数组:"+ Arrays.toString(arr));
Node root=createHuffmnTree(arr);
System.out.println("前序遍历霍夫曼树:");
root.preOrder();
}
public static Node createHuffmnTree(int arr[]) {
//1.遍历数组,每个元素创建一个node节点,并放入到list中
List<Node> nodes = new ArrayList<>();
for (int value : arr) {
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
//从小到大排序,每次都得排序
Collections.sort(nodes);
System.out.println("排序结果:"+nodes);
//1.取出权值最小的节点
Node leftNode = nodes.get(0);
//2.取出权值次小的节点
Node rightNode = nodes.get(1);
//3.创建一个新的节点,值是两个最小节点的和
Node parent = new Node(leftNode.getValue() + rightNode.getValue());
parent.setLeft(leftNode);
parent.setRight(rightNode);
//4.集合中删除两个已经处理过的节点
nodes.remove(leftNode);
nodes.remove(rightNode);
//5.将新节点放入
nodes.add(parent);
}
System.out.println("最后集合剩余:"+nodes);
return nodes.get(0);
}
}
/**
* 创建节点类
* 方便排序,对象实现collection集合排序
*/
class Node implements Comparable<Node> {
//节点值
private int value;
//左节点
private Node left;
//右节点
private Node right;
/**
* 前序遍历
*/
public void preOrder() {
System.out.print(this.getValue()+" ");
if (this.getLeft() != null) {
this.getLeft().preOrder();
}
if (this.getRight() != null) {
this.getRight().preOrder();
}
}
public Node(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
//node节点从小到大排序
@Override
public int compareTo(Node o) {
return this.value - o.getValue();
}
@Override
public String toString() {
return value+" ";
}
}
测试输出:
原数组:[13, 7, 8, 3, 29, 6, 1]
排序结果:[1 , 3 , 6 , 7 , 8 , 13 , 29 ]
排序结果:[4 , 6 , 7 , 8 , 13 , 29 ]
排序结果:[7 , 8 , 10 , 13 , 29 ]
排序结果:[10 , 13 , 15 , 29 ]
排序结果:[15 , 23 , 29 ]
排序结果:[29 , 38 ]
最后集合剩余:[67 ]
前序遍历霍夫曼树:
67 29 38 15 7 8 23 10 4 1 3 6 13
霍夫曼编码
基本介绍:
1.赫夫曼编码是一种编码方式,属于一种程序算法
2.赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一
3.赫夫曼编码被广泛用于数据文件压缩,其压缩率通常在20%-90%之间
4.赫夫曼码是可变字长编码(VLC)的一种,于1952年提出的一种编码方式,称为最佳编码
构造赫夫曼树
赫夫曼编码是无损压缩,恢复后不会造成数据的不同!
霍夫曼树用于数据压缩
1.生成赫夫曼树
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
System.out.println("原字符串长度:" + content.length() + " 转换后byte数组长度:" + contentBytes.length);
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes=" + nodes);
System.out.println("======创建霍夫曼树");
Node huffmanTreeRoot = createHuffmanTree(nodes);
System.out.println("前序遍历:");
huffmanTreeRoot.preOrder();
}
/**
* 将字符串byte数组转换为对应的node节点集合
*
* @param bytes
* @return
*/
public static List<Node> getNodes(byte[] bytes) {
List<Node> nodes = new ArrayList<>();
//key=字符,value=字符出现的次数
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) {
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* 穿件霍夫曼树
*
* @param nodes
* @return 根节点
*/
public static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
//每次都从小到大排序下
Collections.sort(nodes);
//最小的节点
Node leftNode = nodes.get(0);
//次小的节点
Node rightNode = nodes.get(1);
//注意:所有的元素都保存在叶子节点上,根节点上并不存储元素,只存储两个叶子节点的权值和
Node parent = new Node(null, leftNode.weight + rightNode.weight);
//父节点左右节点赋值
parent.left = leftNode;
parent.right = rightNode;
//原集合中删除左右节点
nodes.remove(leftNode);
nodes.remove(rightNode);
//把父节点加进去
nodes.add(parent);
}
return nodes.get(0);
}
}
class Node implements Comparable<Node> {
//存放数据本身,比如'a'对应的assic码值为97等
Byte data;
//权值,字符出现的次数
int weight;
//左节点
Node left;
//右节点
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
public Node(Node left, Node right) {
this.left = left;
this.right = right;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
/**
* 从小到大排序
*/
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
/**
* 前序遍历
*/
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
输出:
原字符串长度:40 转换后byte数组长度:40
nodes=[Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1}, Node{data=101, weight=4}, Node{data=117, weight=1}, Node{data=118, weight=2}, Node{data=105, weight=5}, Node{data=121, weight=1}, Node{data=106, weight=2}, Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}]
======创建霍夫曼树
前序遍历:
Node{data=null, weight=40}
Node{data=null, weight=17}
Node{data=null, weight=8}
Node{data=108, weight=4}
Node{data=null, weight=4}
Node{data=106, weight=2}
Node{data=111, weight=2}
Node{data=32, weight=9}
Node{data=null, weight=23}
Node{data=null, weight=10}
Node{data=97, weight=5}
Node{data=105, weight=5}
Node{data=null, weight=13}
Node{data=null, weight=5}
Node{data=null, weight=2}
Node{data=100, weight=1}
Node{data=117, weight=1}
Node{data=null, weight=3}
Node{data=121, weight=1}
Node{data=118, weight=2}
Node{data=null, weight=8}
Node{data=101, weight=4}
Node{data=107, weight=4}
2.生成赫夫曼编码
package cn.com.Huffman;
import java.util.*;
import java.util.stream.Stream;
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
System.out.println("原字符串长度:" + content.length() + " 转换后byte数组长度:" + contentBytes.length);
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes=" + nodes);
System.out.println("======创建霍夫曼树");
Node huffmanTreeRoot = createHuffmanTree(nodes);
System.out.println("前序遍历:");
huffmanTreeRoot.preOrder();
System.out.println("获取赫夫曼编码表================");
getCodes(huffmanTreeRoot);
System.out.println(huffmanCodes);
}
//创建map,key为字符对应的assic码值,value为对应的路径
static Map<Byte, String> huffmanCodes = new HashMap<>();
/**
* 重点!!!!!!
*
* @param node
* @return 赫夫曼编码表
*/
private static Map<Byte, String> getCodes(Node node) {
if (node == null) {
return null;
} else {
getCodes(node, "", new StringBuilder());
}
return huffmanCodes;
}
/**
* 重点!!!!!!
* 获取赫夫曼编码表
* 重点1:赫夫曼编码的内容,如i,l,i等字符对应的assic码都保存在叶子节点上,根节点上没有对应的data(字符对应的asscic值),只有weight(两个叶子节点的权值和)
* 重点2:赫夫曼树向左为code=0,向右code=1
*
* @param node 节点
* @param code 赫夫曼树向左为code=0,向右code=1
* @param sb 路径拼接对象
*/
private static void getCodes(Node node, String code, StringBuilder sb) {
StringBuilder stringBuilder1 = new StringBuilder(sb);
//拼接路径:赫夫曼树向左为code=0,向右code=1
stringBuilder1.append(code);
if (node != null) {
if (node.data == null) {
//该节点为非叶子节点,字符对应的assic编码会存储在也在节点上,根节点上没有对应的data(字符对应的asscic值),只有weight(两个叶子节点的权值和)
//左遍历
getCodes(node.left, "0", stringBuilder1);
//右遍历
getCodes(node.right, "1", stringBuilder1);
} else {
//叶子节点,存储的是字符对应的assic码
huffmanCodes.put(node.data, stringBuilder1.toString());
}
}
}
/**
* 将字符串byte数组转换为对应的node节点集合
*
* @param bytes
* @return
*/
public static List<Node> getNodes(byte[] bytes) {
List<Node> nodes = new ArrayList<>();
//key=字符,value=字符出现的次数
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) {
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* 穿件霍夫曼树
*
* @param nodes
* @return 根节点
*/
public static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
//每次都从小到大排序下
Collections.sort(nodes);
//最小的节点
Node leftNode = nodes.get(0);
//次小的节点
Node rightNode = nodes.get(1);
//注意:所有的元素都保存在叶子节点上,根节点上并不存储元素,只存储两个叶子节点的权值和
Node parent = new Node(null, leftNode.weight + rightNode.weight);
//父节点左右节点赋值
parent.left = leftNode;
parent.right = rightNode;
//原集合中删除左右节点
nodes.remove(leftNode);
nodes.remove(rightNode);
//把父节点加进去
nodes.add(parent);
}
return nodes.get(0);
}
}
class Node implements Comparable<Node> {
//存放数据本身,比如'a'对应的assic码值为97等
Byte data;
//权值,字符出现的次数
int weight;
//左节点
Node left;
//右节点
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
public Node(Node left, Node right) {
this.left = left;
this.right = right;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
/**
* 从小到大排序
*/
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
/**
* 前序遍历
*/
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
输出:
.......
获取赫夫曼编码表================
{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
3.按照赫夫曼编码字典进行编码,获取编码后传输的对应byte数组
/**
* 重点!!!!!!!
* 将字符串按照赫夫曼编码字典进行编码后,获取对应的byte数组
*
* @param bytes 原始字符串字符对应的assic码byte数组
* @param huffmanCodes 赫夫曼编码字典表:map key=字符对应的assic码,value=对应的赫夫曼树路径
* @return 编码后的赫夫曼编码byte数组
*/
public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
//获取赫夫曼编码对应的路径拼接:类似于10001101010....
sb.append(huffmanCodes.get(b));
}
System.out.println("赫夫曼路径拼接结果:" + sb.toString());
//创建的byte数组长度
int len;
if (sb.length() % 8 == 0) {
//刚好是byte 8位的倍数
len = sb.length() / 8;
} else {
//在原始基础上+1
len = sb.length() / 8 + 1;
}
byte[] huffmanCodeBytes = new byte[len];
int index = 0;
for (int i = 0; i < sb.length(); i += 8) {
String strByte;
if (i + 8 > sb.length()) {
//最后几位,不够8位
strByte = sb.substring(i);
} else {
//截取8为的二进制
strByte = sb.substring(i, i + 8);
}
//将二进制的string转byte
huffmanCodeBytes[index++] = (byte) Integer.parseInt(strByte, 2);
}
return huffmanCodeBytes;
}
测试:
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
System.out.println("原字符串长度:" + content.length() + " 转换后byte数组长度:" + contentBytes.length);
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes=" + nodes);
System.out.println("======创建霍夫曼树");
Node huffmanTreeRoot = createHuffmanTree(nodes);
System.out.println("前序遍历:");
huffmanTreeRoot.preOrder();
System.out.println("获取赫夫曼编码表================");
getCodes(huffmanTreeRoot);
System.out.println(huffmanCodes);
//重点:测试
System.out.println("赫夫曼编码转换为对应byte数组================");
byte[] zip = zip(contentBytes, huffmanCodes);
System.out.println("原字符串长度:" + contentBytes.length + " 转换后byte数组长度:" + zip.length);
System.out.println("转换后的byte数组长度:"+zip.length+" 压缩比为:"+(double)(contentBytes.length-zip.length)/contentBytes.length);
System.out.println("转换后的赫夫曼byte数组:"+Arrays.toString(zip));
}
输出:
E:\work\jdk\bin\java.exe "-javaagent:E:\work\idea\IntelliJ IDEA 2020.1\lib\idea_rt.jar=61626:E:\work\idea\IntelliJ IDEA 2020.1\bin" -Dfile.encoding=UTF-8 -classpath E:\work\jdk\jre\lib\charsets.jar;E:\work\jdk\jre\lib\deploy.jar;E:\work\jdk\jre\lib\ext\access-bridge-64.jar;E:\work\jdk\jre\lib\ext\cldrdata.jar;E:\work\jdk\jre\lib\ext\dnsns.jar;E:\work\jdk\jre\lib\ext\jaccess.jar;E:\work\jdk\jre\lib\ext\jfxrt.jar;E:\work\jdk\jre\lib\ext\localedata.jar;E:\work\jdk\jre\lib\ext\nashorn.jar;E:\work\jdk\jre\lib\ext\sunec.jar;E:\work\jdk\jre\lib\ext\sunjce_provider.jar;E:\work\jdk\jre\lib\ext\sunmscapi.jar;E:\work\jdk\jre\lib\ext\sunpkcs11.jar;E:\work\jdk\jre\lib\ext\zipfs.jar;E:\work\jdk\jre\lib\javaws.jar;E:\work\jdk\jre\lib\jce.jar;E:\work\jdk\jre\lib\jfr.jar;E:\work\jdk\jre\lib\jfxswt.jar;E:\work\jdk\jre\lib\jsse.jar;E:\work\jdk\jre\lib\management-agent.jar;E:\work\jdk\jre\lib\plugin.jar;E:\work\jdk\jre\lib\resources.jar;E:\work\jdk\jre\lib\rt.jar;E:\work\idea\idea_workspace\DataStructures\target\classes cn.com.Huffman.HuffmanCode
原字符串长度:40 转换后byte数组长度:40
nodes=[Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1}, Node{data=101, weight=4}, Node{data=117, weight=1}, Node{data=118, weight=2}, Node{data=105, weight=5}, Node{data=121, weight=1}, Node{data=106, weight=2}, Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}]
======创建霍夫曼树
前序遍历:
Node{data=null, weight=40}
Node{data=null, weight=17}
Node{data=null, weight=8}
Node{data=108, weight=4}
Node{data=null, weight=4}
Node{data=106, weight=2}
Node{data=111, weight=2}
Node{data=32, weight=9}
Node{data=null, weight=23}
Node{data=null, weight=10}
Node{data=97, weight=5}
Node{data=105, weight=5}
Node{data=null, weight=13}
Node{data=null, weight=5}
Node{data=null, weight=2}
Node{data=100, weight=1}
Node{data=117, weight=1}
Node{data=null, weight=3}
Node{data=121, weight=1}
Node{data=118, weight=2}
Node{data=null, weight=8}
Node{data=101, weight=4}
Node{data=107, weight=4}
获取赫夫曼编码表================
{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
赫夫曼编码转换为对应byte数组================
赫夫曼路径拼接结果:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
原字符串长度:40 转换后byte数组长度:17
转换后的byte数组长度:17 压缩比为:0.575
转换后的赫夫曼byte数组:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
发现原始字符串长度传输需要40个字节,但是赫夫曼压缩后传输只需要17个字节,压缩比达到了%57.5,相当可以了!
赫夫曼树数据解压
前提知识,代码里会用到将byte数组转二进制的代码:
1.原码:是最简单的机器数表示法,用最高位表示符号位,其他位存放该数的二进制的绝对值
2.反码:正数的反码还是等于原码;负数的反码就是它的原码除符号位外,按位取反
3.补码:正数的补码等于它的原码;负数的补码等于反码+1
具体可以参考:https://blog.csdn.net/qq_44543508/article/details/121624103
private static void byteToBitString(byte b) {
int temp = b;
/*
该方法返回的是二进制的补码
1.原码:是最简单的机器数表示法,用最高位表示符号位,其他位存放该数的二进制的绝对值
2.反码:正数的反码还是等于原码;负数的反码就是它的原码除符号位外,按位取反
3.补码:正数的补码等于它的原码;负数的补码等于反码+1
*/
String str = Integer.toBinaryString(temp);
System.out.println("str:" + str+" 长度:"+str.length());
}
测试:
byteToBitString((byte) 1);
byteToBitString((byte) -1);
输出:
str:1 长度:1
str:11111111111111111111111111111111 长度:32
结论:
发现正数1的输出为:1(自己本身)
负数-1的输出为:11111111111111111111111111111111
-1的原码是:1000 0001
-1的反码是:1111 1110(负数的反码就是它的原码除符号位外,按位取反)
-1的补码是:1111 1111(负数的补码等于反码+1)
Integer.toBinaryString(temp);返回的是二进制的补码
这里有个问题,正数返回的不够8位的二进制:
如Integer.toBinaryString(1) 返回1
Integer.toBinaryString(3) 返回11
Integer.toBinaryString(5) 返回101
负数的返回是32位的:
byteToBitString((byte) -5) 返回:11111111111111111111111111111011 (补码返回)
想要返回一个8位的,就会涉及一个负数截取,正数补位的动作
这么做:
private static void byteToBitString(byte b) {
int temp = b;
/*
该方法返回的是二进制的补码
1.原码:是最简单的机器数表示法,用最高位表示符号位,其他位存放该数的二进制的绝对值
2.反码:正数的反码还是等于原码;负数的反码就是它的原码除符号位外,按位取反
3.补码:正数的补码等于它的原码;负数的补码等于反码+1
*/
temp |=256;
String str = Integer.toBinaryString(temp);
System.out.println("str:" + str+" 长度:"+str.length());
}
正数按位或
temp |=256;
256:1 0000 0000
1 : 1
按位或:1 0000 0001
负数按位或
256:1 0000 0000
-1 : 1000 0001
按位或:1 1000 0001
这时,temp的长度都超过了8位,就可以进行截取了!
测试:
byteToBitString((byte) 1);
byteToBitString((byte) -1);
输出:
str:100000001 长度:9
str:11111111111111111111111111111111 长度:32
解压缩代码:
/**
* 将一个byte转成一个二进制的字符串
* 原始传来的二进制数组为[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
* 将其转为对应的二进制返回
*
* @param b 传入的byte
* @param flag 标志位,表示是否需要补位
* @return 按补码返回对应的二进制字符串
*/
private static String byteToBitString(boolean flag, byte b) {
int temp = b;
/*
该方法返回的是二进制的补码
1.原码:是最简单的机器数表示法,用最高位表示符号位,其他位存放该数的二进制的绝对值
2.反码:正数的反码还是等于原码;负数的反码就是它的原码除符号位外,按位取反
3.补码:正数的补码等于它的原码;负数的补码等于反码+1
*/
if (flag) {
/**
* 这里注意:在当初压缩时,最后一个byte,可能不是8位,如28==>11100,用下述Integer.toBinaryString(temp)转换时,会进行补0,为1 0001 1100
* 这样拼接出来的二进制字符串可能会有问题:如原始为:之前11100 ==> 之前0001 1100
* 后续处理逻辑是:逐个截取二进制字符串,在字典中找,这多出几位可能会有问题!
*/
temp |= 256;
}
String str = Integer.toBinaryString(temp);
if (flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
/**
* @param huffmanCodes
* @param hubBytes
* @return
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] hubBytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hubBytes.length; i++) {
byte b = hubBytes[i];
boolean flag = (i == hubBytes.length - 1);
//最后一位不需要补位,直接返回即可!
sb.append(byteToBitString(!flag, b));
}
System.out.println("转为二进制:" + sb.toString());
//将字典的key和value调换,因为解码是通过二进制字符串找对应的assi码
Map<String, Byte> map = new HashMap<>();
for (Byte key : huffmanCodes.keySet()) {
map.put(huffmanCodes.get(key), key);
}
System.out.println("原始的压缩字典值:" + huffmanCodes);
System.out.println("用于解压的字典值:" + map);
List<Byte> list = new ArrayList<>();
for (int i = 0; i < sb.length(); ) {
int cout = 0;
while (true) {
String str = sb.substring(i, i + cout);
if (map.get(str) != null) {
list.add(map.get(str));
System.out.println(str + ":" + map.get(str));
break;
}
cout++;
}
i += cout;
}
System.out.println(list.size());
byte[] bytes = new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
bytes[i] = list.get(i);
}
return bytes;
}
测试:
System.out.println("二进制转换===============");
byte[] decode = decode(huffmanCodes, zip);
System.out.println("解码后的报文:"+new String(decode));
输出:
二进制转换===============
转为二进制:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
原始的压缩字典值:{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
用于解压的字典值:{000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}
i like like like java do you like a java
和预期一样,解码成功
赫夫曼编码的最佳实现---文件压缩
压缩代码示例:
/**
* 压缩文件
*
* @param srcFile 源文件路径
* @param dstFile 目标文件路径
*/
private static void zipFile(String srcFile, String dstFile) {
//创建输出流
OutputStream ops = null;
ObjectOutputStream oos = null;
//创建文件输入流
FileInputStream fis = null;
try {
//创建文件输入流
fis = new FileInputStream(srcFile);
byte[] b = new byte[fis.available()];
//读取文件
fis.read(b);
//重点:压缩文件
byte[] zip = huffmanZip(b);
System.out.println("源文件大小:" + b.length + " 压缩后的大小:" + zip.length + " 压缩比:" + (double) (b.length - zip.length) / b.length);
//创建文件的输出流,存放压缩文件
ops = new FileOutputStream(dstFile);
//创建一个和文件输出流关联的ObjectOutputStream
oos = new ObjectOutputStream(ops);
//把赫夫曼编码后的字节数组写入压缩文件
oos.writeObject(zip);
//把赫夫曼编码写入到压缩文件
oos.writeObject(huffmanCodes);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取编码后的byte数组
* 方法中的代码在上面有,直接搜索即可,没有复制过来,要不太多了!
* @param contentBytes 原始的的byte数组
* @return
*/
private static byte[] huffmanZip(byte[] contentBytes) {
//原始的byte转node节点集合
List<Node> nodes = getNodes(contentBytes);
//创建霍夫曼树
Node huffmanTreeRoot = createHuffmanTree(nodes);
//获取霍夫曼编码表
huffmanCodes = getCodes(huffmanTreeRoot);
//根据霍夫曼编码表压缩文件
byte[] zip = zip(contentBytes, huffmanCodes);
return zip;
}
测试
public static void main(String[] args) {
zipFile("C:\\Users\\Administrator\\Desktop\\图片\\测试压缩.png", "C:\\Users\\Administrator\\Desktop\\图片\\压缩后.zip");
}
输出:
赫夫曼路径拼接结果:100101011101....
源文件大小:16024 压缩后的大小:14012 压缩比:0.1255616575137294
赫夫曼编码的最佳实现---文件解压
/**
* 解压文件
*
* @param zipFile 解压源文件路径
* @param dstFile 解压后文件存储位置
*/
private static void unZipFile(String zipFile, String dstFile) {
//定义文件输入流
InputStream is = null;
//定义对象输入流
ObjectInputStream ois = null;
//定义文件输出流
OutputStream os = null;
try {
is = new FileInputStream(zipFile);
//创建对象输入流
ois = new ObjectInputStream(is);
//读取byte数组
byte[] huffmanBytes = (byte[]) ois.readObject();
//读取赫夫曼编码
Map<Byte, String> codes = (Map<Byte, String>) ois.readObject();
//解码
byte[] bytes = decode(codes, huffmanBytes);
os = new FileOutputStream(dstFile);
os.write(bytes);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
os.close();
ois.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试:
unZipFile("C:\\Users\\Administrator\\Desktop\\图片\\压缩后.zip","C:\\Users\\Administrator\\Desktop\\图片\\解压后.png");
结论:完美解压,解压后的大小一样,没有任何损失!
总结: