【数据结构】Java实现哈夫曼编码

哈夫曼树

哈夫曼树是一种特殊的二叉树,它的每个叶子节点都对应着一个字符,并且树的形状是根据字符出现的频率来构建的。频率越高的字符离根节点越近,频率越低的字符离根节点越远。这样,出现频率高的字符可以用较短的编码表示,而出现频率低的字符可以用较长的编码表示。

public class Huffman implements Comparable<Huffman> {
    private Integer weight; // 权重
    private Character name; // 编码字符
    private String encode; // 编码值
    private Huffman lChildren;  // 左子树
    private Huffman rChildren;  // 右子树
    public Huffman() {
    }
    public Huffman(Integer weight, Character name, String encode, Huffman lChildren, Huffman rChildren) {
        this.weight = weight;
        this.name = name;
        this.encode = encode;
        this.lChildren = lChildren;
        this.rChildren = rChildren;
    }
    @Override
    public int compareTo(Huffman o) {
        return this.getWeight() - o.getWeight();
    }

    @Override
    public String toString() {
        return "Huffman{" +
                "weight=" + weight +
                ", name=" + name +
                ", encode=" + encode +
                ", lChildren=" + lChildren +
                ", rChildren=" + rChildren +
                '}';
    }
    
    public Integer getWeight() {
        return weight;
    }
    public Character getName() {
        return name;
    }
    public Huffman getLChildren() {
        return lChildren;
    }
    public Huffman getRChildren() {
        return rChildren;
    }
}

哈夫曼树的构建过程如下:

  1. 统计字符的出现频率,将每个字符与其频率构成一个节点。
  2. 将所有节点按照频率从小到大进行排序。
  3. 取出频率最小的两个节点,创建一个新的节点作为它们的父节点,该节点的频率为两个子节点频率之和。
  4. 将新创建的节点放回节点列表中,并重新排序。
  5. 重复步骤3和步骤4,直到只剩下一个节点,即为哈夫曼树的根节点。

/**
 * 根据字符频率
 *
 * @param str 需要编码数据
 * @return
 */
public Huffman createHuffmanRootNode(String str) {
    Huffman root = null;
    HashMap<Character, Integer> map = new HashMap<Character, Integer>();
    for (int i = 0; i < str.length(); i++) {
        if (map.containsKey(str.charAt(i))) {
            Integer val = map.get(str.charAt(i));
            map.put(str.charAt(i), val + 1);
        } else {
            map.put(str.charAt(i), 1);
        }
    }
    LinkedList<Huffman> listNode = new LinkedList<>();
    for (Map.Entry<Character, Integer> en : map.entrySet()) {
        Huffman huffman = new Huffman(en.getValue(), en.getKey(), "", null, null);
        listNode.add(huffman);
    }
    Collections.sort(listNode);
    root = createTree(listNode);
    return root;
}

/**
 * 直接根据权重
 *
 * @param str    待编码字符串
 * @param weight 对应权重集
 * @return
 */
public Huffman createHuffmanRootNode(String str, Integer[] weight) {
    HashMap<Character, Integer> map = new HashMap<Character, Integer>();
    for (int i = 0; i < str.length(); i++) {
        map.put(str.charAt(i), weight[i]);
    }
    LinkedList<Huffman> listNode = new LinkedList<>();
    for (Map.Entry<Character, Integer> en : map.entrySet()) {
        Huffman huffman = new Huffman(en.getValue(), en.getKey(), "", null, null);
        listNode.add(huffman);
    }
    Collections.sort(listNode);
    return createTree(listNode);
}

/**
 * 1, 如果该树只有一个节点直接设置为左树
 * 2, 找出最小的两个节点构建成树 并且构建的新树根节点权重为两子节点的和
 * 3, 重新排序节点列表
 *
 * @param listNode
 * @return
 */
private Huffman createTree(LinkedList<Huffman> listNode) {
    Huffman root = null;
    while (listNode.size() > 0) {
        if (listNode.size() == 1) {
            Huffman left = listNode.removeFirst();
            return new Huffman(left.getWeight(), null, "", left, null);
        }
        Huffman left = listNode.removeFirst();
        Huffman right = listNode.removeFirst();
        if (listNode.size() == 0) {
            root = new Huffman(left.getWeight() + right.getWeight(), null, "", left, right);
        } else {
            Huffman huffman = new Huffman(left.getWeight() + right.getWeight(), null, "", left, right);
            listNode.add(huffman);
            Collections.sort(listNode);
        }
    }
    return root;
}

哈夫曼编码

哈夫曼编码是一种前缀编码方式,即没有任何一个编码是另一个编码的前缀。通过哈夫曼树的结构,我们可以为每个字符生成对应的编码。生成编码的过程如下:

  1. 从哈夫曼树的根节点开始,沿着左子树走一步表示编码为0,沿着右子树走一步表示编码为1。
  2. 遍历哈夫曼树的每个叶子节点,生成对应字符的编码。

哈夫曼编码的特点是没有编码是其他编码的前缀,这样可以避免在解码时出现歧义。

/**
 * 获取编码表
 */
public HashMap<Character, String> getCode(Huffman root) {
    HashMap<Character, String> huffmanCode = new HashMap<>();
    getHuffmanCode(root, huffmanCode, "");
    return huffmanCode;
}
private static void getHuffmanCode(Huffman root, HashMap<Character, String> huffmanCode, String code) {
    if (null == root) return;
    if (null == root.getLChildren() && null == root.getRChildren()) {
        huffmanCode.put(root.getName(), code);
    }
    if (null != root.getLChildren()) {
        getHuffmanCode(root.getLChildren(), huffmanCode, code+"0");
    }
    if (null != root.getRChildren()) {
        getHuffmanCode(root.getRChildren(), huffmanCode, code+"1");
    }
}
/**
 * 编码
 *
 * @param root 数
 * @param str  待编码字符串
 * @return 编码字符串
 */
public String encode(Huffman root, String str) {
    StringBuffer encode = new StringBuffer();
    HashMap<Character, String> code = this.getCode(root);
    for (int i = 0; i < str.length(); i++) {
        encode.append(code.get(str.charAt(i)));
    }
    return encode.toString();
}
/**
 * 解码
 *
 * @param root  哈夫曼树的根节点
 * @param code  待解码的编码字符串
 * @return 解码后的字符串
 */
public String decode(Huffman root, String code) {
    StringBuilder decode = new StringBuilder();
    Huffman currentNode = root;
    for (int i = 0; i < code.length(); i++) {
        char bit = code.charAt(i);
        if (bit == '0') {
            currentNode = currentNode.getLChildren();
        } else if (bit == '1') {
            currentNode = currentNode.getRChildren();
        }
        if (currentNode.getLChildren() == null && currentNode.getRChildren() == null) {
            decode.append(currentNode.getName());
            currentNode = root; // 重置当前节点为根节点,继续下一个字符的解码
        }
    }
    return decode.toString();
}

哈夫曼编码实现文件压缩

利用哈夫曼编码可以实现文件的压缩和解压缩。压缩过程如下:

  1. 统计文件中每个字符的出现频率。
  2. 根据字符频率构建哈夫曼树。
  3. 生成每个字符对应的哈夫曼编码。
  4. 将原始文件中的每个字符用对应的哈夫曼编码替代。
  5. 将编码后的内容写入压缩文件,同时保存哈夫曼树的结构和字符频率信息。

解压缩过程如下:

  1. 读取压缩文件中的哈夫曼树和字符频率信息。
  2. 根据哈夫曼树重建哈夫曼树的结构。
  3. 读取压缩文件中的编码内容。
  4. 根据哈夫曼树将编码内容解码为原始字符。
  5. 将解码后的字符写入解压缩文件,即得到原始文件。
public static void main(String[] args) {
    Huffman huffman = new Huffman();
    String str = txt2String(new File("D:\\JAVA\\DataStructuresAndAlgorithms\\src\\com\\DataStructures\\Tree\\Huffman\\Huffman.java"));
    Huffman rootNode = huffman.createHuffmanRootNode(str);
//        System.out.println(rootNode);
    HashMap<Character, String> code = huffman.getCode(rootNode);
    System.out.println(code);
    String encode = huffman.encode(rootNode, str);
    System.out.println(encode);
    String decode = huffman.decode(rootNode, encode);
//        System.out.println(decode);
    // 将编码字符写入文件
    try (FileOutputStream fos = new FileOutputStream("D:\\JAVA\\DataStructuresAndAlgorithms\\src\\com\\DataStructures\\Tree\\Huffman\\output.bin")) {
        // 将二进制字符串转换为字节数组
        byte[] bytes = binaryStringToBytes(encode);
        // 将字节数组转为二进制字符串
        String str2 = bytesToBinaryString(bytes);
        // 写入字节数组到文件
        fos.write(bytes);
        System.out.println("数据写入成功!");
        System.out.println(str2);
        System.out.println(huffman.decode(rootNode, str2));
    } catch (IOException e) {
        e.printStackTrace();
    }
}



/**
 * 文件转字符串
 * @param file
 * @return
 */
public static String txt2String(File file)
{
    StringBuilder result = new StringBuilder();
    try
    {
        //构造一个BufferedReader类来读取文件
        BufferedReader br = new BufferedReader(new FileReader(file));
        String s = null;
        while((s = br.readLine())!=null)
        {//使用readLine方法,一次读一行
            result.append(System.lineSeparator()+s);
        }
        br.close();
    }
    catch(Exception e)
    {
        e.printStackTrace();
    }
    return result.toString();
}

/**
 * string转为字节数据
 * @param binaryString
 * @return
 */
private static byte[] binaryStringToBytes(String binaryString) {
    int length = binaryString.length();
    int bytesLength = length / 8; // 字节数组的长度
    if (length % 8 != 0) {
        bytesLength++; // 如果二进制字符串长度不能被8整除,需要额外的一个字节存储剩余的二进制字符
    }

    byte[] bytes = new byte[bytesLength];

    int index = 0; // 字节数组中的索引位置
    for (int i = 0; i < length; i += 8) {
        int endIndex = i + 8;
        if (endIndex > length) {
            endIndex = length; // 如果末尾位置超过二进制字符串长度,将末尾位置设置为字符串末尾
        }

        String byteString = binaryString.substring(i, endIndex);
        bytes[index] = (byte) Integer.parseInt(byteString, 2);
        index++;
    }

    return bytes;
}

/**
 * 把字节数据还原为string
 * @param bytes
 * @return
 */
private static String bytesToBinaryString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (byte b : bytes) {
        String binaryString = Integer.toBinaryString(b & 0xFF);
        // 每个字节的二进制表示长度都是8位,在不足8位的情况下在前面补0
        while (binaryString.length() < 8) {
            binaryString = "0" + binaryString;
        }
        sb.append(binaryString);
    }
    return sb.toString();
}

通过哈夫曼编码,我们可以有效地减小文件的大小,节省存储空间。这是因为出现频率高的字符使用了较短的编码,而出现频率低的字符使用了较长的编码,从而整体上减小了编码后的文件大小。


总结起来,哈夫曼树和哈夫曼编码是一种高效的数据压缩算法。通过构建哈夫曼树和生成对应的哈夫曼编码,我们可以实现文件的压缩和解压缩。哈夫曼编码的特点是高频字符使用短编码,低频字符使用长编码,这种编码方式避免了解码时的歧义。通过哈夫曼编码,我们可以大幅度减小文件的大小,提高存储和传输效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值