赫夫曼编码

2021/9/30 6:12:39

本文主要是介绍赫夫曼编码,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

简介

  1. 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。
  2. 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间

通信领域中信息的处理方式

  1. 定长编码
    i like like like java do you like a java (包括空格)

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 (对应Ascii码)

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 (对应的二进制)
如果按照二进制来传递信息,总的长度是359,这样对应的码太长了,通常不会这样做
2) 变长编码
i like like like java do you like a java

按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推.0= , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d, d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 (各个字符对应的个数)

传送的编码为:10010110100... ,但是这种编码不是前缀编码。字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码。比如10不知道是l的编码还是u的编码还是它就是i的编码
3) 赫夫曼编码
i like like like java do you like a java
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值, 向左的路径编码为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
长度为:133

注意

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

代码

结点对象

//为了让Node对象支持Collections集合排序,实现Comparable接口
class Node implements Comparable<Node> {

  Byte data;  //存放字符本身
  int weight; //权值
  Node left;
  Node right;

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

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

  @Override
  public int compareTo(@NotNull Node o) {
    //从小到大排序
    return this.weight - o.weight;
  }

}

将结点放入到集合中

/**
   * @param bytes 接收字节数组
   * @return 返回的就是 List 形式   [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......],
   */
  private static List<Node> getNodes(byte[] bytes) {
    //原始字符集对应的字节数组
    System.out.println("bytes = " + Arrays.toString(bytes));
    //1.创建一个ArrayList
    List<Node> nodes = new ArrayList<>();
    //2.遍历bytes,统计每个字符出现的次数
    Map<Byte,Integer> countMap = new HashMap<>();
    for (byte b : bytes) {
      countMap.merge(b, 1, Integer::sum);
    }
    //3.存放到集合中
    for (Map.Entry<Byte,Integer> entry : countMap.entrySet()) {
      nodes.add(new Node(entry.getKey(),entry.getValue()));
    }
    return nodes;
  }

根据集合创建赫夫曼树

/**
   * 根据传入的集合创建赫夫曼树,并返回根节点
   * @param nodes 里面是一个个的结点对象
   * @return 赫夫曼树的根节点
   */
  private 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);
  }

得到赫夫曼编码

//用来存放赫夫曼编码表
  static Map<Byte,String> huffmanCodes = new HashMap<>();
  //在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
  static StringBuilder stringBuilder = new StringBuilder();

  /**
   *    * 将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
   *    * @param node 传入的结点
   *    * @param code 路径:左子结点是0,右子结点是1
   *    * @param stringBuilder 用于拼接路径
   */
  private static void getCodes(Node node,String code,StringBuilder stringBuilder) {
    StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
    stringBuilder1.append(code);
    if (node != null) {
      if (node.data == null) {  //非叶子节点
        getCodes(node.left,"0",stringBuilder1); //向左递归
        getCodes(node.right,"1",stringBuilder1);  //向右递归
      } else {
        //说明找到叶子节点
        huffmanCodes.put(node.data,stringBuilder1.toString());
      }
    }
  }

  //为了调用方便,重载getCodes
  private static Map<Byte,String> getCodes(Node root) {
    if (root == null) {
      return null;
    }
    getCodes(root.left,"0",stringBuilder);
    getCodes(root.right,"1",stringBuilder);
    return huffmanCodes;
  }

测试一下

     String str = "i like like like java do you like a java";
    byte[] bytes = str.getBytes();
    List<Node> nodes = getNodes(bytes);
    Node huffmanTree = createHuffmanTree(nodes);
    Map<Byte, String> codes = getCodes(huffmanTree);
    System.out.println(codes);

将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]

 /**
   * @param bytes 原始的字符串对应的byte[]
   * @param huffmanCodes 赫夫曼编码表
   * @return 返回赫夫曼编码处理后的 byte[]
   * 将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
   *  返回的是 字符串 "101010001011111111001000101..."
   *  对应的 byte[] huffmanCodeBytes  ,即 8位对应一个 byte,放入到 huffmanCodeBytes
   *   huffmanCodeBytes[0] =  10101000(补码) => byte  [推导  10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ]
   *    huffmanCodeBytes[1] = -88
   */
  private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes) {
    StringBuilder stringBuilder = new StringBuilder();
    for (byte b : bytes) {
      stringBuilder.append(huffmanCodes.get(b));
    }
    int length = (stringBuilder.length() + 7) / 8;
    /**
     *  下面的代码等同于  int length = (stringBuilder.length() + 7) / 8;
     *   if (stringBuilder.length() % 8 == 0) {
     *       length = stringBuilder.length() % 8;
     *     } else {
     *       length = stringBuilder.length() % 8 + 1;
     *     }
     *
     */
    byte[] codeBytes = new byte[length];
    int index = 0;
    String strByte;
    for (int i = 0; i < stringBuilder.length(); i += 8) {
      if (i + 8 > stringBuilder.length()) {
        strByte = stringBuilder.substring(i);
      } else {
        strByte = stringBuilder.substring(i,i + 8);
      }
      codeBytes[index] = (byte)Integer.parseInt(strByte,2);
      index++;
    }
    return codeBytes;
  }

测试

     String str = "i like like like java do you like a java";
    byte[] bytes = str.getBytes();
    List<Node> nodes = getNodes(bytes);
    Node huffmanTree = createHuffmanTree(nodes);
    Map<Byte, String> codes = getCodes(huffmanTree);
    System.out.println(codes);
    byte[] zip = zip(bytes, huffmanCodes);
    System.out.println("zip = " + Arrays.toString(zip));

为了方便,将前面的方法封装起来

/**
   * 将前面的方法封装起来
   * @param bytes 原始字符串对应的字节数组
   * @return  经过 赫夫曼编码处理后的字节数组(压缩后的数组)
   */
  private static byte[] huffmanZip(byte[] bytes) {
    Node huffmanTree = createHuffmanTree(bytes);
    Map<Byte, String> codes = getCodes(huffmanTree);
    return zip(bytes, codes);
  }

测试

     String str = "i like like like java do you like a java";
    byte[] bytes = str.getBytes();
    byte[] bytes1 = huffmanZip(bytes);
    System.out.println("bytes1 = " + Arrays.toString(bytes1));

解码

/**
   * 将一个byte 转成一个二进制的字符串
   * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
   * @param b 传入的 byte
   * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
   */
  private static String byteToBitString(boolean flag,byte b) {
    int temp = b;
    if (flag) {
      temp |= 256;
    }
    String str = Integer.toBinaryString(temp);
    if (flag) {
      return str.substring(str.length() - 8);
    } else {
      return str;
    }
  }
/**
   * 解压数据
   * @param huffmanCodes 赫夫曼编码表
   * @param huffmanBytes 赫夫曼编码得到的字节数组
   * @return 原来的字符串对应的数组
   */
  private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes) {
    StringBuilder stringBuilder = new StringBuilder();
    //将byte数组转成二进制的字符串
    for (int i = 0; i < huffmanBytes.length; i++) {
      byte b = huffmanBytes[i];
      //判断是不是最后一个字节
      boolean flag = (i == huffmanBytes.length - 1);
      stringBuilder.append(byteToBitString(!flag,b));
    }

    //把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
    Map<String,Byte> map = new HashMap<>();
    for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
      map.put(entry.getValue(),entry.getKey());
    }

    List<Byte> list = new ArrayList<>();
    for (int i = 0; i < stringBuilder.length();) {
      int count = 1;
      boolean flag = true;
      Byte b = null;
        while (flag) {
          String key = stringBuilder.substring(i,i + count);
          b = map.get(key);
          if (b == null) {
            count++;
          } else {
            flag = false;
          }
        }
        i += count;
        list.add(b);
    }
    byte[] bytes = new byte[list.size()];
    for (int i = 0; i < bytes.length; i++) {
      bytes[i] = list.get(i);
    }
    return bytes;
  }

测试

    String str = "i like like like java do you like a java";
    byte[] bytes = str.getBytes();
    byte[] bytes1 = huffmanZip(bytes);
    System.out.println("bytes1 = " + Arrays.toString(bytes1));
    byte[] decode = decode(huffmanCodes, bytes1);
    System.out.println(new String(decode));

文件压缩并解压

压缩

 /**
   * 将文件压缩
   * @param srcPath 原文件路径
   * @param dtsPath 压缩文件路径
   */
  public static void zipFile(String srcPath,String dtsPath) {
    FileInputStream is = null;
    OutputStream os = null;
    ObjectOutputStream oos = null;
    try {
      is = new FileInputStream(srcPath);
      byte[] b = new byte[is.available()];
      is.read(b);
      byte[] huffmanBytes = huffmanZip(b);
      os = new FileOutputStream(dtsPath);
      //以对象流的方法存储,便于读取
      oos = new ObjectOutputStream(os);
      oos.writeObject(huffmanBytes);
      oos.writeObject(huffmanCodes);
    }catch (Exception e){
      System.out.println(e.getMessage());
    } finally {
      try {
        assert oos != null;
        oos.close();
        os.close();
        is.close();
      } catch (Exception e){
        System.out.println(e.getMessage());
      }
    }
  }

解压

/**
   * 解压文件
   * @param zipPath 压缩文件路径
   * @param srcPath 解压文件路径
   */
  public static void unZipFile(String zipPath,String srcPath) {
    InputStream is = null;
    ObjectInputStream ois = null;
    OutputStream os = null;
    try {
      is = new FileInputStream(zipPath);
      ois = new ObjectInputStream(is);
      byte[] huffmanBytes = (byte[]) ois.readObject();
      Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();
      byte[] bytes = decode(huffmanCodes,huffmanBytes);
      os = new FileOutputStream(srcPath);
      os.write(bytes);
    }catch (Exception e) {
      System.out.println(e.getMessage());
    }finally {
      try {
        assert os != null;
        os.close();
        ois.close();
        is.close();
      }catch (Exception e){
        System.out.println(e.getMessage());
      }
    }
  }

测试

   String srcPath = "E:\\java\\idea\\place\\DataStructure\\src\\com\\fly\\huffmancode\\test.bmp";
    String dstPath = "E:\\java\\idea\\place\\DataStructure\\src\\com\\fly\\huffmancode\\test2.zip";
    zipFile(srcPath,dstPath);
    String zipPath = "E:\\\\java\\\\idea\\\\place\\\\DataStructure\\\\src\\\\com\\\\fly\\\\huffmancode\\\\test2.zip";
    String src2Path = "E:\\\\java\\\\idea\\\\place\\\\DataStructure\\\\src\\\\com\\\\fly\\\\huffmancode\\\\test3.bmp";
    unZipFile(zipPath,src2Path);

测试文件


执行后


注意事项

  1. 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件
  2. 赫夫曼编码是按字节来处理的,因此可以处理所有的文件
  3. 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.


这篇关于赫夫曼编码的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程