数据结构与算法——赫夫曼树基本实现

目录

一、赫夫曼树

 1.1 基本介绍

1.2 赫夫曼树创建步骤图解

 1.3  代码实现

二、赫夫曼编码

2.1 基本介绍

2.1.1  通讯领域 - 定长编码 - 举例说明

2.1.2  通讯领域 - 变长编码 - 举例说明

2.2  通讯领域 - 赫夫曼编码 - 原理图示

2.3 赫夫曼编码 - 压缩

 2.3.1  创建赫夫曼树实现思路

 2.3.2  创建赫夫曼树

2.3.3 生成赫夫曼编码表

2.3.4   赫夫曼编码字节数组

2.4 赫夫曼编码 - 数据解压

2.4.1 字节转二进制字符串

2.4.2 赫夫曼解码


一、赫夫曼树


 1.1 基本介绍


    最优二叉树:也称哈夫曼树或者霍夫曼树、赫夫曼树,给定n个权值作为n个叶子结点(每个叶子结点会有权值),构造一颗二叉树,若该树的带权路径长度(wpl)达到最小


    赫夫曼树:带权路径长度最短的树,权值较大的结点离根较进。(值都在叶子结点上)
   

  •    路径: 在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路
  •    路径长度:  通路中分支的数目
  •    若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1 

          下图中,根结点到第三层的路径长度都是3-1=2

  •    结点的权:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
  •    带权路径长度:  从根结点到该结点之间的路径长度与该结点的权的乘积 

              还是这个图,看第三层最左侧的权为13(第二层对应的结点没有,默认为1),则带权路径长度为 13*(3-1)=26

  • 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  • WPL最小的就是赫夫曼树 

            下图中wpl最小的就是中间这个图,它就是赫夫曼树

1.2 赫夫曼树创建步骤图解

   很简单的很好懂的

构成赫夫曼树的步骤:

  • 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

          只不过每个结点的左右子节点为null而已

         比如一组数{13, 7, 8, 3, 29, 6, 1},将其排序之后成为{1,3,6,7,8,13,29}

  • 取出根节点权值最小的两颗二叉树

        也就是排序之后的前两个数,1和3

  • 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和  

         新的二叉树的根节点的权值为1+3=4,

  • 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复  1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

             此时数据变成{4,6,7,8,13,29},再重复步骤,也就是选择4与6,之和为10

      此时数据变成{7,8,10,13,29},再重复步骤,也就是选择7与8,和为15

     此时数据变成{10,13,15,29},再重复步骤,也就是选择,10与13,和为23

       

             此时数据变成{15,23,29},再重复步骤,也就是选择,15与23,和为38

 此时数据变成{29,38},再重复步骤,也就是选择29与38,和为67

此时数据只剩下一个68,最终构成的树为赫夫曼树,此时的wpl是最小的

 1.3  代码实现

public class HuffmanTree {
    public static void main(String[] args) {
        int arr[] = {13, 7, 8, 3, 29, 6, 1};
        Node rootNode = createHuffmanTree(arr);
        preOrder(rootNode);
    }


    /**
     * 前序遍历
     * @param root
     *
     */
    public static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("空树!不能遍历");
        }
    }


    /**
     * @param arr 每一个数都代表着一个树,只不过左右子结点为null
     * @return  赫夫曼树的root结点
     */
    public static Node createHuffmanTree(int[] arr) {
//       第一步为了操作方便,遍历数组封装成List集合
        List<Node> nodes = new ArrayList<>();
        for (int i = 0; i < arr.length; i++) {
            Node node = new Node(arr[i]);
            nodes.add(node);
        }

//      从小到大排序   排序的规则我们在下面的compareTo中定义的
        Collections.sort(nodes);
        while (nodes.size() > 1) {
            //            运行到这里说明tempList集合中中至少有两个元素
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            Node parentNode = new Node(leftNode.value + rightNode.value);
            parentNode.left=leftNode;
            parentNode.right=rightNode;
//            Iterator<Node> it = tempList.iterator();
//            it.remove(leftNode);
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parentNode);
//           再排序
            Collections.sort(nodes);
        }
//      这个是赫夫曼树的root结点
        return nodes.get(0);
    }
}

/**
 * 为了让Node对象持续排序 Collocations集合排序
 * 让Node 实现Comparable接口
 */
class Node implements Comparable<Node> {
    int value; //结点权值
    Node left; //右子结点
    Node right;//左子结点

    //  前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left!=null){
           this.left.preOrder();
        }
        if(this.right!=null){
            this.right.preOrder();
        }
    }

    //  比较的方法
    @Override
    public int compareTo(Node o) {
//          代表了对权值进行比较
//            表示从小到大排
        return this.value - o.value;
    }

    @Override
    public String toString() {
        return "Node{" + "value=" + value + '}';
    }

    public Node(int value) {
        this.value = value;
    }

    public Node(Node left, Node right) {
        this.left = left;
        this.right = right;
    }
}

下面的数据值刚好与我们推断的最后的结构的前序遍历吻合

二、赫夫曼编码

通信领域中信息的处理方式常见的有三种:定长编码、变长编码、赫夫曼编码

其中赫夫曼编码是无损压缩,比如将图片的编码变成图片本身的时候,清晰度不会出现问题

2.1 基本介绍

  •     赫夫曼编码也被翻译为哈夫曼编码,是一种编码方式,属于一种程序算法
  •     赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一。
  •     赫夫曼编码广泛的应用于数据文件压缩。其压缩率通常为20%~90%之间
  •     赫夫曼码是可变长编码(VLC)的一种。Huffman于1952年提出的一种编码方法,称之为最佳编码

2.1.1  通讯领域 - 定长编码 - 举例说明

按照二进制来传递信息,总的长度是  359   (包括空格)

i like like like java do you like a java       // 共40个字符(包括空格)  

对应Ascii码

105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97

对应的二进制

01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 

2.1.2  通讯领域 - 变长编码 - 举例说明

i like like like java do you like a java       // 共40个字符(包括空格)

    各个字符对应的个数

d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9   (最后一个是空格,表示空格出现了9次)

0=  ,  1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d

      说明:

  •   按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小

              比如 空格出现了9 次, 编码为0 ,其它依次类推. 按照上面给各个字符规定的编码,则我们在传输  "i like like like java do you like a java" 数据时,编码就是 10010110100...  

  •   字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编

              即不能匹配到重复的编码。 

             我们将一个字符串转换成上面的编码看起来是没问题的,但其实是有问题的。比如在解码的时候将编码10010110100...重新转换成字符串,那我们应该怎么将数据进行分组呢? 开头的1是一组还是10是一组?  很显然是分不开的

2.2  通讯领域 - 赫夫曼编码 - 原理图示

通信领域中信息的处理方式3-赫夫曼编码

      i like like like java do you like a java        共40个字符(包括空格)

 各个字符对应的个数 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.

    d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  

 构成赫夫曼树的步骤:   在上面已经写过了

  •    从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
  •    取出根节点权值最小的两颗二叉树
  •    组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  •    再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复  1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

如下所示:

    我们规定向左的路径为0,向右的路径为1

      如o: 1000   u: 10010  d: 100110

          y: 100111  i: 101 a : 110     k: 1110  

          e: 1111       j: 0000       v: 0001  l: 001        

          : 01

 按照上面的赫夫曼编码,我们的"i like like like java do you like a java"   字符串对应的编码为 (注意这里我们使用的无损压缩)

1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110

① 先匹配1,发现没有,再匹配10,发现没有,再匹配101  发现是i

② 先匹配0,发现没有,再匹配01,发现为空格 

③ 先匹配0,发现没有,再匹配00,发现没有,再匹配001 发现是l

 .............

 通过赫夫曼编码处理  长度为  133

         原来长度是  359 , 压缩了  (359-133) / 359 = 62.9% 此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性 赫夫曼编码是无损处理方案

补充说明:

     赫夫曼树根据排序方法不同,也可能不太一样,这样就会导致赫夫曼编码也不完全一样,但是wpl是一样的都是最小的。

       比如:如果我们让每次生成的心的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为下图,与刚刚的完全不同,但是都是赫夫曼树,也会导致编码不一样(但是长度一样)

2.3 赫夫曼编码 - 压缩

 2.3.1  创建赫夫曼树实现思路

  • Node { data (存放数据), weight (权值), left  和 right }   结点
  • 得到  "i like like like java do you like a java"   对应的 byte[] 数组
  • 编写一个方法,将准备构建赫夫曼树的Node 节点放到 List  

           形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......],  

            体现 d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  

  • 可以通过List 创建对应的赫夫曼树

 2.3.2  创建赫夫曼树

package com.example.demo05;

import java.util.*;

public class HuffmanCode {
    public static void main(String[] args) {
        String str = "i like like like java do you like a java";
        byte[] contentBytes = str.getBytes();

        List<Node> nodes = getNodes(contentBytes);
        Node huffmanTree = createHuffmanTree(nodes);
        preOrder(huffmanTree);
    }

    /**
     *
     * @param bytes  接收字节数组
     * @return List形式集合
     */
    private static List<Node> getNodes(byte[] bytes) {
//       1.创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<>();

//       2.遍历bytes,统计每一个byte出现的次数,  使用map集合进行统计 map[key,value]
        Map<Byte, Integer> contentBytesMap = new HashMap<>();
        for (byte b : bytes) {
            if (contentBytesMap.containsKey(b)) {
//              这就是有
                contentBytesMap.put(b, contentBytesMap.get(b) + 1);
            } else {
//           没有
                contentBytesMap.put(b, 1);
            }
        }
//       遍历map集合,把每一个键值对转换成一个node对象,并加入到nodes集合
         for(Map.Entry<Byte,Integer> entry: contentBytesMap.entrySet()){
             nodes.add(new Node(entry.getKey(), entry.getValue()));
         }

        return nodes;
    }

    /**
     * 通过传入的List集合,创建赫夫曼树
     * @param nodes
     * @return
     */
    private static Node createHuffmanTree(List<Node> nodes){
//      集合中至少有2个元素才能进入循环
        while (nodes.size() > 1){
//          排序,次数最少的在集合的最前面 从小到大
            Collections.sort(nodes);
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
//          新的根节点没有data,只有权值
            Node parentNode = new Node(null, leftNode.weight+rightNode.weight);
            parentNode.left = leftNode;
            parentNode.right = rightNode;
//          移除刚刚的两个结点
            nodes.remove(leftNode);
            nodes.remove(rightNode);
//          添加刚刚的新结点
            nodes.add(parentNode);
        }
//      集合中只剩下最后一个元素
        return nodes.get(0);
    }

    /**
     *
     * @param nodes  想要前序遍历的树
     */
    private static void preOrder(Node nodes){
        if (nodes !=null){
            nodes.preOrder();
        }else {
            System.out.println("树为空,不可遍历");
        }

    }
}


class Node implements Comparable<Node> {
    Byte data;  //存放数据(字符)本身,比如 'a' => 97   ' ' => 32
    int weight; // 权值
    Node left;  // 左子结点
    Node right; // 右子结点

    //  前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }

    @Override
    public int compareTo(Node o) {
//      我们的排序需要,小的在前面
        return this.weight - o.weight;
    }

    @Override
    public String toString() {
        return "Node{" + "data=" + data + ", weight=" + weight + '}';
    }

    public Node(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

}

2.3.3 生成赫夫曼编码表

//      测试生成对应的赫夫曼编码
        getCodes(huffmanTreeRoot,"",stringBuilder);
        System.out.println("生成的哈夫曼编码表"+huffmanCodesMap);

//  生成赫夫曼树对应的赫夫曼编码
//     思路:
//      1.将赫夫曼编码表存放在Map<Byte,String>形式
//             32->01 97->100 d->11000 ...... key为ASCII编码,value为赫夫曼编码
    static Map<Byte, String> huffmanCodesMap = new HashMap<>();
//      2. 在生成赫夫曼编码,需要去拼接路径,定义一个StringBuilder,存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();

    /**
     *  功能,将传入的node结点的所有叶子结点的赫夫曼编码得到并放入到Map集合中
     * @param node   传入结点
     * @param code   路径-> 可规定为 左为0 右为1
     * @param stringBuilder  用于拼接路径
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
       StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
       stringBuilder2.append(code);
       if(node !=null){ //node为空的时候不处理
          if(node.data ==null){
//             非叶子结点
//             向左
               if(node.left!=null){
                   getCodes(node.left,"0",stringBuilder2);
               }
//             向右
              if(node.right!=null){
                  getCodes(node.right,"1",stringBuilder2);
              }
          }else {
//             说明是一个叶子结点
              huffmanCodesMap.put(node.data,stringBuilder2.toString());
          }
       }
    }

2.3.4   赫夫曼编码字节数组

如今的过程:

  1.    字符串转换成字节数组
  2.    创建赫夫曼树
  3.    生成赫夫曼编码 
  4.    获取赫夫曼编码字节数组

 讲一下为什么我们要将“1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100”

赫夫曼编码转换成字节数组?

   原来的是“i like like like java do you like a java”,只有40个长度,但是你把它转化成赫夫曼编码后长度为133,远远地大于了最开始的时候的字符串的长度,那这样的haul我们使用赫夫曼编码显然没有意义

        String str = "i like like like java do you like a java";
        byte[] contentBytes = str.getBytes();
        byte[] zip = zip(contentBytes, huffmanCodesMap);
        System.out.println(Arrays.toString(zip));

/**
     * 编写一个方法,将字符串对应的byte[]数组,通过生成的赫夫曼表,返回一个赫夫曼编码即压缩后的byte数组
     *
     * @param bytes           原始的字符串对应的byte[]
     * @param huffmanCodesMap 生成的赫夫曼编码map
     * @return 赫夫曼处理后的byte数组
     * 即“1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100”
     * 对应的byte数组,将上面的字符串八位一组存放到byte数组中,否则太长了,
     * 如 huffmanCodesBytes[0] =  10101000(补码) -> byte[推导 10101000-> 反码为10101001-1 =>10100111(反码) => 符号位不变其他取反11011000(原码)]
     * 10101000(补码) 的原码  11011000,开头的1代表符号位表示-,1011000代表88  则 1101100 代表-88
     * 最终结果huffmanCodesBytes[0]=-88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodesMap) {
//      1.利用huffmanCodesMap编码表 将bytes转换成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodesMap.get(b));
        }
//        System.out.println(stringBuilder);   //1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
//        System.out.println(stringBuilder.length()); //133   赫夫曼编码可能不一样,因为在生成赫夫曼树的时候的排序可能不一样,但是!! 长度一定是一个样子的
//      2.统计 我们将要生成的byte数组huffmanCodesBytes的长度
        int length;
        if (stringBuilder.length() % 8 == 0) {
            length = stringBuilder.length() / 8;
        } else {
            length = stringBuilder.length() / 8 + 1;
        }
//      3. 创建存储压缩后的huffmanCodesBytes数组
        byte[] huffmanCodesBytes = new byte[length];

        int index = 0;  //记录huffmanCodesBytes下标

//        每8位对应一个byte,所以步长为8
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            String strByte;
            if (i + 8 > stringBuilder.length()) {
//               说明我们下面要截取的不够八位了,那我摩恩有多少取多少就可以了
                strByte = stringBuilder.substring(i);
                huffmanCodesBytes[index] = (byte) Integer.parseInt(strByte, 2);
            } else {
//               左开右闭,所以这里写i+8没有任何问题
                strByte = stringBuilder.substring(i, i + 8);
                huffmanCodesBytes[index] = (byte) Integer.parseInt(strByte, 2);
            }
            index++;
        }
        return huffmanCodesBytes;
    }

       原来40个单位长度,如今是17个单位长度   压缩率为(40-17)/40 ×100% =53.4%

2.4 赫夫曼编码 - 数据解压

将huffmanCodesBytes字节数组[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]  

转义为 下面的数字字符串

1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

再将上面的数字字符串 根据赫夫曼编码表 转义为真正的数据   "i like like like java do you like a java"

2.4.1 字节转二进制字符串

    /**
     *
     * @param flag 表示是否需要补高位 true表示补高位,如果是最后一个字节则不需要补高位
     * @param b    将byte转为二进制的字符串 ,true表示需要
     * @return b对应的二进制的字符串(补码的形式返回)
     */
    private static String byteToString(boolean flag ,byte b){
        int temp = b;
//      如果是正数,存在补高位的问题
        if(flag){
            temp |= 256;  //假设temp为1   temp按位与256  1 0000 0000 | 0000 0001 = 1 0000 0001
        }
        //返回的是二进制对应的补码
        String str = Integer.toBinaryString(temp);
        if (flag){
//           str.substring(str.length()-8)相当于取后面的8位
             return  str.substring(str.length()-8);
         }else {
             return  str;
         }
    }

2.4.2 赫夫曼解码

    /**
     * 解码
     *
     * @param huffmanCodesMap 赫夫曼表
     * @param huffmanBytes    要转义的字节数组
     * @return 就是原来字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodesMap, byte[] huffmanBytes) {
//      1.先得到huffmanBytes字节数组对应的二进制字符串,即“1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100”
        StringBuilder stringBuilder = new StringBuilder();
//        将byte数组转换成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            boolean flag = (i == huffmanBytes.length - 1);
//          保证最后一个不补高位
            stringBuilder.append(byteToString(!flag, huffmanBytes[i]));
        }
//      2. 根据赫夫曼编码表和我们刚刚转义的字符串进行匹配 获取最终结果
//           将赫夫曼编码表进行调换,因为我们要反向查询 key value调换
        Map<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCodesMap.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
//        创建集合,存放byte
        List<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1; //向右遍历的计数器+
            Byte b = null;
            boolean flag = true;
            while (flag){
                String key = stringBuilder.substring(i,i+count);
                b=map.get(key);
                if(b!=null){
//                   匹配到了
                    flag = false; // 结束循环
                }else {
//                   没有匹配到
                    count++;
                }
            }
            list.add(b);
            i = i+count;  //因为我们每次循环完成还需要+1
        }
//         循环结束后,list中存放了所有的单个字符
//           将list中的数据放入到byte数组
        byte[] b = new byte[list.size()];
        for(int i=0 ; i<b.length;i++){
            b[i]= list.get(i);
        }
        return b;
    }

猜你喜欢

转载自blog.csdn.net/weixin_51351637/article/details/129941022
今日推荐