创建节点类
//创建Node节点类,带权值
//它可以使继承他的类进行比较大小,只需要调用实现类的compareTo方法即可
class Node implements Comparable<Node>{
public Byte data;//存放数据本身,字符a转化成ASCII码97,空格就是32
public int weight;//代表权值,表示字符出现的次数
Node left;//指向左子节点
Node right;//指向右子节点
//构造器,用来初始化数据
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
//通过调用Comparable接口中的compareTo方法,可以比较Node节点中weight权值的大小
@Override
public int compareTo(Node node) {
//要比较的顺序是“从小到大”
//如果返回值=0,则this.weight==node.weight
//如果返回值>0,则this.weight>node.weight
return this.weight-node.weight;
}
//用来输出
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
//前序遍历
public void preOrder(){
System.out.println(this);
if(this.left!=null){
this.left.preOrder();
}
if(this.right!=null){
this.right.preOrder();
}
}
}
创建赫夫曼编码类
public class HuffmanCode {
public static void main(String[] args) {
//1-给个字符串
String str="i like like like java do you like a java";
//2-把字符串string的字符取出来,放在byte数组里面(这些字符还要变成ASCII码,然后放进Node节点,Node节点放进List)
byte[] bytes = str.getBytes();
//在压缩前,看看长度,长度为40
System.out.println("压缩前byte数组的长度为:"+bytes.length);
/*//3-把byte数组里的字符和出现次数,放进map存储,然后用map数据创建Node节点,再把Node加到List
List<Node> nodesList=getNode(bytes);
System.out.println("处理后的Node节点的List:"+nodesList);
//4-根据List来创建一个赫夫曼树,返回值是树的根节点root,有了根节点,顺着遍历就得到整个树了
System.out.println("赫夫曼树为:");
Node huffmanTreeRoot=createHuffmanTree(nodesList);
//树的根节点有了,要想得到整个树,用一个前序遍历的方法就行了,把root根节点传进去即可
System.out.println("赫夫曼树的前序遍历:");
huffmanTreeRoot.preOrder();
//5-遍历赫夫曼树,生成赫夫曼的编码
//把根节点传进去,最后得到的是string类型的路径编码huffmanCode
// getCodes(huffmanTreeRoot,"",stringBuilder);
//因为下面写了getCodes方法的重载,只需要给方法传一个根节点root,其他不用管,这样更方便
Map<Byte,String> huffmanCodes=getCodes(huffmanTreeRoot);
System.out.println("生成的赫夫曼编码表:"+huffmanCodes);
//6-得到赫夫曼编码之后的数据,也就是把所有字符的编码路径拼接到一起,进行压缩
byte[] huffmanCodeBytes=zip(bytes,huffmanCodes);
//字符串Array相关的调用方法也要深入学习掌握
System.out.println("huffmanCodeBytes="+ Arrays.toString(huffmanCodeBytes));*/
byte[] huffmanCodesBytes=huffmanZip(bytes);
System.out.println("压缩后的结果:"+Arrays.toString(huffmanCodesBytes));
System.out.println("压缩后byte数组的长度为:"+huffmanCodesBytes.length);
//测试byteToBitString方法
// System.out.println(byteToBitString((byte)1));
decode(huffmanCode, huffmanCodesBytes);
}
//完成数据的解压
//思路:
//1-把压缩后的字节huffmanCodesBytes[-88,-65,...]先转换成赫夫曼编码对应的二进制的字符串“1010100...”
//2-赫夫曼编码对应的二进制的字符串“1010100...”转成字符串“i like like...”
//
//编写一个方法,完成对压缩数据的解码
/**huffmanCodes 赫夫曼编码
* huffmanBytes 赫夫曼编码得到的字节数组
* 返回的是原来字符串对应的数组
* @MethodName: decode
* @Author: AllenSun
* @Date: 2019/11/11 22:40
*/
private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){
//1-先得到huffmanBytes对应的二进制的字符串“1010100...”,需要StringBuilder拼接
StringBuilder stringBuilder1 = new StringBuilder();
//把byte数组转成二进制的字符串
for (int i = 0; i < huffmanBytes.length; i++) {
byte b=huffmanBytes[i];
//调用前面写过的方法,先判断是不是最后一个字节,是的话就不用补高位
boolean flag=(i==huffmanBytes.length-1);
stringBuilder1.append(byteToBitString(!flag,b));
}
System.out.println("赫夫曼字节数组对应的二进制字符串为:"+stringBuilder1.toString());
return null;
}
/**把一个byte转成一个二进制的字符串,涉及到java基础里的原码、反码、补码
* @MethodName: byteToBitString
* @Author: AllenSun
* @Date: 2019/11/11 22:38
*/
private static String byteToBitString(boolean flag, byte b){
//使用变量保存b
int temp=b;//把b转换成int
//如果是正数,我们还存在补高位,flag是true就补高位,是false就不用补高位。如果是最后一个字节,也不需要补高位
if(flag){
temp |=256;//按位与256
}
//toBinaryString此方法返回int变量的二进制表示的字符串
String str=Integer.toBinaryString(temp);//返回的是temp对应的二进制的补码
if(flag){
return str.substring(str.length()-8);
} else {
return str;
}
}
/**使用一个方法,把前面的方法封装起来,便于我们的调用
* 传入一个原始的字符串对应的字节数组
* 返回一个经过赫夫曼编码处理后的字节数组(也就是返回一个“压缩后的”数组)
* @MethodName: huffmanZip
* @Author: AllenSun
* @Date: 2019/11/9 18:06
*/
private static byte[] huffmanZip(byte[] bytes){
List<Node> nodesList=getNode(bytes);
//4-根据List来创建一个赫夫曼树,返回值是树的根节点root,有了根节点,顺着遍历就得到整个树了
Node huffmanTreeRoot=createHuffmanTree(nodesList);
//5-遍历赫夫曼树,生成赫夫曼的编码
//把根节点传进去,最后得到的是string类型的路径编码huffmanCode
// getCodes(huffmanTreeRoot,"",stringBuilder);
//因为下面写了getCodes方法的重载,只需要给方法传一个根节点root,其他不用管,这样更方便
Map<Byte,String> huffmanCodes=getCodes(huffmanTreeRoot);
//6-得到赫夫曼编码之后的数据,也就是把所有字符的编码路径拼接到一起,进行压缩
byte[] huffmanCodeBytes=zip(bytes,huffmanCodes);
return huffmanCodeBytes;
}
/**写一个“创建一个Node节点的链表List”方法
* 1-接收一个字节数组,统计每个字节出现的次数
* 2-然后把字符的值和出现的次数存到map里去
* 3-再遍历map,把遍历出来的键值对用来初始化创建Node节点
* 4-最后把Node节点加到List里去
* @MethodName: getNode
* @Author: AllenSun
* @Date: 2019/11/9 15:00
*/
//从byte数组里把Node节点取出来,然后放到List里(为什么要放到List里去:因为数组取出插入数据不如链表方便)
private static List<Node> getNode(byte[] bytes){
//1-创建一个ArrayList
ArrayList<Node> nodesList=new ArrayList<Node>();
//2-遍历bytes,统计存储每个byte字符出现的次数--存放在map里,Byte就是数据,integer就是出现的次数
//所有说,当你要一个数据时,要先想好,用什么数据结构来存放它,并且对数据结构可以调用的方法了如指掌
Map<Byte,Integer> counts=new HashMap<>();
for (byte b : bytes) {//遍历所有的bytes
Integer count=counts.get(b);//把byte的字符取出来放进Map
//如果count一开始就是空的,说明是第一次取,计数为1
if(count==null){
counts.put(b,1);//第一次,把data值和weight值放进map
} else {
counts.put(b,count+1);//若不是第一次,
}
}
//3-把字符的ASCII码放进Node的data,遍历出来的次数map放进权值weight,这样就创建了一个节点,把节点Node对象,放到List里去
//上面已经有了Map的键值对,接下来就是把键值对(byte,count)放到对应的Node(data,value)里去
//怎么把字符转换成ASCII码???
//首先遍历一下Map,entrySet就是用来遍历Map的方法,直接调用
for (Map.Entry<Byte,Integer> entry:counts.entrySet()){
//把遍历出来的键值对放到Node里,在把Node加到节点的List里去
nodesList.add(new Node(entry.getKey(),entry.getValue()));
}
//4-最后返回一个我们需要的nodesList
return nodesList;
}
/**根据已有的节点List,把这些节点Node取出来,创建成一个赫夫曼树
* @MethodName: createHuffmanTree
* @Author: AllenSun
* @Date: 2019/11/9 15:14
*/
//创建赫夫曼的方法
private static Node createHuffmanTree(List<Node> nodes) {
//循环创建
while(nodes.size()>1){
//1-首先把数组排序,从小到大,直接使用一个集合
Collections.sort(nodes);
System.out.println("nodes=" + nodes);
//2-取出根节点权值最小的两棵二叉树
//(1)取出权值最小的节点(二叉树)
Node leftNode = nodes.get(0);
//(2)取出权值第二小的节点(二叉树)
Node rightNode = nodes.get(1);
//(3)创建一棵新的二叉树,根节点只有权值,没有data
Node parent = new Node(null,leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//(4)从ArrayList删除处理过的二叉树,不然下次没法从List最前面两位取节点
nodes.remove(leftNode);
nodes.remove(rightNode);
//(5)把parent加入nodes,加入以后重新排序sort,就可以继续了
nodes.add(parent);
}
//nodesList最后剩下的节点,也就是权值最大的,就是赫夫曼树的根节点root
return nodes.get(0);
}
/**赫夫曼树的前序遍历
* 前面创建树的时候,已经得到了树的根节点,但是还没有得到赫夫曼树的全部节点
* 前序遍历根据根节点,对赫夫曼树进行前序遍历
* @MethodName: preOrder
* @Author: AllenSun
* @Date: 2019/11/9 15:31
*/
//前序遍历赫夫曼树的方法,给我一个根节点,我帮你遍历出来整棵树
private static void preOrder(Node root){
if(root!=null){
root.preOrder();
} else {
System.out.println("赫夫曼树为空,没有数据");
}
}
/**已经得到赫夫曼树了,生成赫夫曼编码
* 思路:
* 1-先想好用什么数据结构比较合适,这里用Map<Byte,String>的键值对存放赫夫曼编码表,key是ascii码,value为路径编码
* 例如:32-01,97-100
* 2-在遍历赫夫曼树的时候,会得到0和1来拼接路径,用字符串String来保存某个叶子节点路径编码
* 3-
* @MethodName: getCodes
* @Author: AllenSun
* @Date: 2019/11/9 15:58
*/
//1-map用来存路径编码
static Map<Byte,String> huffmanCode=new HashMap<>();
//2-stringBuilder用来把0和1拼接成路径编码,然后交给map
//StringBuilder类实现字符串的拼接append功能,不需要用“+”来拼接了
static StringBuilder stringBuilder=new StringBuilder();
//把传入的node节点的所有叶子结点的黑曼编码得到,并放到huffmanCode这个集合里去
//node就是先从根节点root开始,code代表路径值,左边就是0,右边就是1,然后把code拼接到stringBuilder上去
private static void getCodes(Node node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//把code值加入到stringBuilder2
stringBuilder2.append(code);
if(node!=null){//如果node是空的,就不处理
//判断当前node是不是叶子结点,如果是叶子结点就可以结束回去了,不是叶子节点就继续往下走
if(node.data==null){
//如果data不是空的就是叶子节点,因为创建赫夫曼树时设定parent节点的data为null,只有权值weight
//如果data为空,那就是非叶子结点,List里取出来的有data的节点都在叶子结点上
//所以,如果data为null,那就要继续递归下去
getCodes(node.left,"0",stringBuilder2);//向左走,把0拼接到stringBuilder2
getCodes(node.right,"1",stringBuilder2);//向右走,把1拼接到stringBuilder2
//这样,结束的时候,每个node都有自己的stringBuilder2
} else {
//说明这是一个叶子节点,到头了,拼接结束,把拼接得到的stringBuilder2编码路径放到huffmanCode里
huffmanCode.put(node.data,stringBuilder2.toString());
}
}
}
/**为了方便,重载getCodes方法,专门用来处理根节点的
* 上面那个getCodes方法,是用来处理根节点下面的所有节点的
* @MethodName: getCodes
* @Author: AllenSun
* @Date: 2019/11/9 16:53
*/
private static Map<Byte,String> getCodes(Node root){
//如果根节点是空的,那就是空树,什么都不返回
if(root==null){
return null;
}
//如果根节点不是空的,那就递归调用上面的getCodes方法,分别处理左子树和右子树
getCodes(root.left,"0",stringBuilder);
getCodes(root.right,"1",stringBuilder);
return huffmanCode;
}
/**编写一个方法,把字符串对应的byte数组,通过生成的赫夫曼编码表huffmanCodes,返回一个赫夫曼编码压缩后的byte
* bytes 就是原始的字符串对应的byte数组
* huffmanCodes 生成的赫夫曼编码map
* 返回的是赫夫曼变啊处理后的byte数组,也就是字符串“”
* 每8位对应一个byte,放入huffmanCodeBytes,
* huffmanCodeBytes[0]=10101000(补码)=》一个byte
* @MethodName: zip
* @Author: AllenSun
* @Date: 2019/11/9 17:09
*/
private static byte[] zip(byte[] bytes, Map<Byte,String> huffmanCodes){
//1-使用huffmanCodes把bytes转成赫夫曼编码对应的字符串
StringBuilder stringBuilder=new StringBuilder();
//遍历bytes数组
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
//把“1010100...”转成byte[]
//统计返回的byte[] huffmanCodeBytes长度length有多大
// int len=(stringBuilder.length()+7)/8;
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
String strByte;
if(i+8>stringBuilder.length()){
//说明到最后剩的编码不够8位了
strByte=stringBuilder.substring(i);
}else {
strByte=stringBuilder.substring(i,i+8);
}
//把strByte转成一个byte,放到huffmanCodeBytes数组里去
huffmanCodeBytes[index]=(byte)Integer.parseInt(strByte,2);
index++;
}
return huffmanCodeBytes;
}
}