【数据结构与算法】赫夫曼树的介绍和程序实现,和赫夫曼编码的介绍、原理剖析、程序实现

1. 赫夫曼树的介绍和程序实现

1.1 赫夫曼树的介绍

n个叶子节点(树的所有叶子节点)构建成一个树有很多种方式,每个叶子节点都有一个权重值W,每个叶子到root节点的路径长度为L(层数) - 1。所有叶子节点带权路径长度 ( W ∗ ( L − 1 ) ) (W * (L - 1)) (W(L1))之和(wpl: weighted path length)最小的树称为赫夫曼树。其权重值越大的节点离root节点越近。如下所示

赫夫曼树

1.2 赫夫曼树创建思路和程序实现

需求:一个数列{13, 7, 8, 3, 29, 6, 1}的各个元素对应树的叶子节点的权重值,将其转成一颗赫夫曼树

赫夫曼树创建思路

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

赫夫曼树

程序实现

package huffmantree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class HuffmanTree {

    public static void main(String[] args) {
        int[] array = {13, 7, 8, 3, 29, 6, 1};
        Node root = createHuffmanTree(array);

        preOrder(root);
    }

    // 创建赫夫曼树的方法
    public static Node createHuffmanTree(int[] array) {
        // 将数组的各个元素,转换成一颗颗简单的树
        List<Node> nodes = new ArrayList<Node>();
        for (int weight : array) {
            nodes.add(new Node(weight));
        }

        // 当nodes的元素个数为1时,表示赫夫曼树创建完成
        while (nodes.size() > 1) {
            // 从小到大排序
            Collections.sort(nodes);

            // 取出root节点权重值最小的两颗二叉树
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);

            // 构建一颗新的二叉树
            Node parentNode = new Node(leftNode.weight + rightNode.weight);
            parentNode.left = leftNode;
            parentNode.right = rightNode;

            // 从ArrayList删除处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 将parentNode加入到nodes
            nodes.add(parentNode);
        }

        // 返回赫夫曼树的root结点
        return nodes.get(0);
    }


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

// 节点类
// 实现Comparable接口,用于Collections.sort
class Node implements Comparable<Node> {
    public int weight;
    public Node left;
    public Node right;

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

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

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

    @Override
    public String toString() {
        return "Node [weight = " + weight + "]";
    }
}

运行程序,结果如下:

Node [weight = 67]
Node [weight = 29]
Node [weight = 38]
Node [weight = 15]
Node [weight = 7]
Node [weight = 8]
Node [weight = 23]
Node [weight = 10]
Node [weight = 4]
Node [weight = 1]
Node [weight = 3]
Node [weight = 6]
Node [weight = 13]

2. 赫夫曼编码的介绍、原理剖析、程序实现

2.1 赫夫曼编码的介绍

赫夫曼编码(Huffman Coding)是用赫夫曼树进行编码的一种方式, 是可变字长编码(VLC)的一种,是一种程序算法。赫夫曼编码广泛地用于数据和文件压缩。其压缩率通常在20%~90%之间

2.2 赫夫曼编码的原理剖析

2.2.1 定长编码

我们先看下传统的定长编码,对于字符串i like like like java do you like a java,包含空格共40个字符

字符串对应的ASCII码(包含空格的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

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.2.2 变长编码

再来看变长编码,对于字符串i like like like java do you like a java,包含空格共40个字符

计算各个字符的个数:空格-9、a-5、i-5、e-4、k-4、l-4、o-2、v-2、j-2、u-1、y-1、d-1

按字符出现次数的多少进行编码,出现次数越多,编码越小:0-空格、1-a、10-i、11-e、100-k、101-l、110-o、111-v、1000-j、1001-u、1010-y、1011-d

按字符的编码对字符串进行编码,就是10010110100......。但是此种编码方式对编码后的结果解码成字符串就会出现问题,会造成多义性。因为一个字符的编码是另一个字符的编码的前缀,例如a的编码1是i的编码10的前缀

所以需要一种编码方式,让字符的编码不能是其他字符编码的前缀,这就是前缀编码

2.2.3 赫夫曼编码

赫夫曼编码就是一种前缀编码

再来看赫夫曼编码,对于字符串i like like like java do you like a java,包含空格共40个字符

计算各个字符的个数:空格-9、a-5、i-5、e-4、k-4、l-4、o-2、v-2、j-2、u-1、y-1、d-1

按照字符出现的次数构建一颗赫夫曼树, 次数作为权重值。注意根据排序算法的不同,相同次数的字符的先后顺序可能不一样,所以构建出来的赫夫曼树也会不一样,但赫夫曼树的wpl是一样的。赫夫曼树如下所示
赫夫曼树
赫夫曼树的介绍和程序实现请参考

根据赫夫曼树,给各个字符编码, 向左的路径为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

按照赫夫曼编码,字符串对应的编码(无损压缩)为1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110。编码后的长度为133

2.2.4 使用赫夫曼编码进行解码

当我们使用赫夫曼编码对前面的字符串进行编码后,现在使用赫夫曼编码对编码后的数据进行解码

前面我们得到了赫夫曼编码,可以得到赫夫曼编码对应的编码byte[],即[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]。可以使用这些解码得到原来的字符串

2.3 使用赫夫曼编码对文件进行压缩和解压

将一个图片文件,使用赫夫曼编码进行无损压缩。然后使用使用赫夫曼编码进行解码,再次打开图片可以正常显示

注意

  • 视频、ppt等文件本身是压缩过的,使用赫夫曼编码压缩率不高
  • 如果一个文件中的内容,重复的数据不多,压缩效果不明显
  • 赫夫曼编码是按字节来处理的,因此可以处理所有的文件

2.4 使用赫夫曼编码实现字符串和文件的压缩和解压

程序如下:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.*;

public class HuffmanCode {

    // 用于保存生成的赫夫曼编码。key为字符,value为对应的编码
    private static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();

    public static void main(String[] args) {

        String content = "i like like like java do you like a java";
        // 获取每个字符的Byte组成的数组
        byte[] contentBytes = content.getBytes();

        // 压缩数据
        byte[] huffmanCodesBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的byte数组是: " + Arrays.toString(huffmanCodesBytes) + ", 长度是: " + huffmanCodesBytes.length);


        // 解压数据
        byte[] unzipBytes = decode(huffmanCodes, huffmanCodesBytes);
        System.out.println("原来的字符串为: " + new String(unzipBytes));


        // 压缩文件
        huffmanCodes.clear();   // 再次测试时清除数据
        String srcFile = "C:\\Users\\dell\\Desktop\\java.jpg";
        String targetFile = "C:\\Users\\dell\\Desktop\\java.zip";

        zipFile(srcFile, targetFile);
        System.out.println("压缩文件成功");


        // 解压文件
        String unzipTargetFile = "C:\\Users\\dell\\Desktop\\java2.jpg";
        unZipFile(targetFile, unzipTargetFile);
        System.out.println("解压文件成功");
    }


    // 对每个字符的Byte组成的数组,统计每个字符出现的次数,然后转换成Node集合
    private static List<Node> getNodes(byte[] contentBytes) {

        // 统计每个字符出现的次数
        Map<Byte, Integer> charDataCounts = new HashMap<>();
        for (Byte charData : contentBytes) {
            Integer charDataCount = charDataCounts.get(charData);
            if (charDataCount == null) {
                charDataCounts.put(charData, 1);
            } else {
                charDataCounts.put(charData, charDataCount + 1);
            }
        }

        // 转换成Node集合
        ArrayList<Node> nodes = new ArrayList<Node>();
        for (Map.Entry<Byte, Integer> entry : charDataCounts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }


    // 通过nodes创建赫夫曼树的方法
    private static Node createHuffmanTree(List<Node> nodes) {

        // 当nodes的元素个数为1时,表示赫夫曼树创建完成
        while (nodes.size() > 1) {
            // 从小到大排序
            Collections.sort(nodes);

            // 取出root节点权重值最小的两颗二叉树
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);

            // 构建一颗新的二叉树。它的root节点没有charData, 只有weight
            Node parentNode = new Node(null, leftNode.weight + rightNode.weight);
            parentNode.left = leftNode;
            parentNode.right = rightNode;

            // 从ArrayList删除处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 将parentNode加入到nodes
            nodes.add(parentNode);
        }

        // 返回赫夫曼树的root结点
        return nodes.get(0);
    }


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

    // 进行赫夫曼编码路径的拼接
    // 参数node为当前进入的节点
    // 参数code为0表示进入的是左节点,为1表示进入的是右节点
    // 参数stringBuilder1用于拼接到新的StringBuilder上
    private static void getCodes(Node node, String code, StringBuilder stringBuilder1) {
        // 每进入下一层节点。都会拼接前面的stringBuilder,生成一个新的stringBuilder
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder1);

        // 表示进入当前节点成功
        if (node != null) {
            //将code拼接到stringBuilder2
            stringBuilder2.append(code);

            // 如果当前节点是非叶子节点,继续递归进行赫夫曼编码路径的拼接
            if (node.charData == null) {
                getCodes(node.left, "0", stringBuilder2);
                getCodes(node.right, "1", stringBuilder2);
            } else {
                // 如果是叶子节点,表示一个字符的赫夫曼编码已经得到
                huffmanCodes.put(node.charData, stringBuilder2.toString());
            }
        }
    }


    // 在生成赫夫曼编码时,用stringBuilder进行路径的拼接
    private static StringBuilder stringBuilder1 = new StringBuilder();

    // 重载getCodes方法
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        } else {
            // 进入左节点,code为0
            getCodes(root.left, "0", stringBuilder1);
            // 进入右节点,code为1
            getCodes(root.right, "1", stringBuilder1);

            // 在getCodes递归中向huffmanCodes添加值
            return huffmanCodes;
        }
    }


    // 对每个字符的Byte组成的数组, 然后使用赫夫曼编码进行转换,得到一个拼接的编码字符串
    // 再将拼接的编码字符串按每8位截取,转换成int,再转换成byte,形成一个byte数组
    private static byte[] zip(byte[] contentBytes, Map<Byte, String> huffmanCodes) {

        // 用于保存拼接的编码字符串
        StringBuilder stringBuilder = new StringBuilder();
        // 每个字符的Byte组成的数组
        for (byte contentByte : contentBytes) {
            stringBuilder.append(huffmanCodes.get(contentByte));
        }

        // 计算stringBuilder由多少个8位组成,不够8位也算
        int huffmanCodeBytesLength;
        if (stringBuilder.length() % 8 == 0) {
            huffmanCodeBytesLength = stringBuilder.length() / 8;
        } else {
            huffmanCodeBytesLength = stringBuilder.length() / 8 + 1;
        }

        // 创建用于存储压缩后的数据的byte数组. 多添加1位,用于保存最后截取的substring的长度(范围1-8)
        byte[] huffmanCodeBytes = new byte[huffmanCodeBytesLength + 1];
        int huffmanCodeByteIndex = 0;
        // 用于保存临时截取的字符串
        String tmpSubString;
        // 循环处理每8位,步长为8
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            // 最后一部分直接截取所有的
            if (i + 8 >= stringBuilder.length()) {
                tmpSubString = stringBuilder.substring(i);
                // 保存最后截取的substring的长度, 并转换成byte
                huffmanCodeBytes[huffmanCodeBytesLength] = (byte) tmpSubString.length();
            } else {
                tmpSubString = stringBuilder.substring(i, i + 8);
            }

            /* 将类似“10101000”的字符串转换成byte
             原理:
             第一步:先将10101000按二进制转换成十进制的int
                将补码10101000补到int的32位000......00010101000,
                由于是正数,所有补码和原码一样,原码转换成十进制的int就是168
             第二步:将32位int转换成8位的byte
                对补码进行截取,只取后8位(能完整的保存我们的tmpSubString, 不足8位的前面补0了,就是正数)
                然后将截取后的8位补码转换成原码。0开头的是正数,正数的补码就是原码;1开头的是负数,负数转原码,先-1在取反
                10101000(补码) -1 =》 10100111 再取反=> 11011000(原码) 将二进制转换成10进制=> -88
            */
            huffmanCodeBytes[huffmanCodeByteIndex] = (byte) Integer.parseInt(tmpSubString, 2);
            huffmanCodeByteIndex++;
        }

        return huffmanCodeBytes;
    }


    // 将压缩的所有方法封装起来,便于调用
    private static byte[] huffmanZip(byte[] contentBytes) {
        List<Node> nodes = getNodes(contentBytes);

        Node huffmanTreeRoot = createHuffmanTree(nodes);

        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);

        byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);

        return huffmanCodeBytes;
    }


    // 将压缩后的byte,转换成字符串形成的二进制补码
    // last2ZipByteFlag为true表示是不是byte数组的第n-1个byte。因为第n-1个byte的字符串形式的二进制补码可能不够8位,要特殊处理

    /*
        这里zipByte分以下几种情况:
        情况1:zipByte是前n-2个,二进制补码是8位,可能是正数
        情况2:zipByte是前n-2个,二进制补码是8位,可能是负数
        情况3:zipByte是第n-1个, 二进制补码可能是8位(可能是正数)
        情况4:zipByte是第n-1个, 二进制补码可能是8位(可能是负数)
        情况5:zipByte是第n-1个, 二进制补码可能不够8位(一定是正数)
         */
    private static String byteToBitString(byte zipByte, boolean last2ZipByteFlag, byte lastZipByte) {
        /* 将byte先转成int
            负数:
                8位byte(类似10101000),转32位的int,在补码的前面加1(111......11110101000)
                两个补码 -1不影响最终的结果,取反码后也不影响最终的结果,两个原码对应的十进制数都是-88
                对32位的int的补码进行截取,和8位byte的补码一样
            正数:
                8位byte(类似01001101),转32位的int,在补码的前面加0(000......00001001101)
                正数的补码和原码相等,所有两个原码对应的十进制数都是77
                对32位的int的补码进行截取,和8位byte的补码一样
         */
        int zipInt = zipByte;

        // 对于情况1的zipByte,Integer.toBinaryString会去除前面的所有0,造成不够8位的情况
        // 所以需要在将倒数第9位的补码变成1,这样截取的时候就不会有问题
        if (!last2ZipByteFlag && zipInt >= 0) {
            // 按位取或。256的补码是100000000,按位取或会将zipInt倒数第9位的补码变成1
            zipInt = zipInt | 256;
        }

        // 获取zipInt对应的二进制补码
        String bitString = Integer.toBinaryString(zipInt);

        // 如果是byte数组的第n-1个byte,要做特殊处理,否则截取最后8位二进制补码返回
        if (last2ZipByteFlag) {
            // 情况4处理,截取最后8位二进制补码返回
            if (zipInt < 0) {
                return bitString.substring(bitString.length() - 8);
            } else {
                // 情况3和5,对二进制补码前面补0,补到lastZipByte的长度
                int lastZipInt = lastZipByte;
                StringBuilder stringBuilder = new StringBuilder(bitString);
                while (stringBuilder.length() < lastZipInt) {
                    stringBuilder.insert(0, "0");
                }
                return stringBuilder.toString();
            }

        } else {
            return bitString.substring(bitString.length() - 8);
        }
    }


    // 将压缩后的数据用赫夫曼编码进行解码
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {

        // 用于拼接压缩后的数据的编码
        StringBuilder stringBuilder = new StringBuilder();
        // 循环将zipByte转换成二进制编码,然后进行拼接. 只遍历前n-1个zipByte
        for (int i = 0; i < huffmanBytes.length - 1; i++) {
            byte zipByte = huffmanBytes[i];
            // 判断是不是第n-1个zipByte
            boolean last2ZipByteFlag = (i == huffmanBytes.length - 2);

            // 如果是第n-1个,则将最后一个byte取出,用于查看第n-1个对应的二进制补码的字符串的长度
            byte lastZipByte = 0;
            if (last2ZipByteFlag) {
                lastZipByte = huffmanBytes[huffmanBytes.length - 1];
            }

            stringBuilder.append(byteToBitString(zipByte, last2ZipByteFlag, lastZipByte));
        }

        // 将赫夫曼编码进行反转,类似由o-1000、u-10010变成1000-o、10010-u
        Map<String, Byte> reverseHuffmanCodes = new HashMap<String, Byte>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            reverseHuffmanCodes.put(entry.getValue(), entry.getKey());
        }

        // 创建集合,用于存放解码后的字符byte
        List<Byte> unzipByteList = new ArrayList<>();
        // 不停的延长字符串,从reverseHuffmanCodes匹配
        // 匹配不到就继续延长
        // 匹配到了表示解码一个字符byte成功,然后从下一个字符初始化形成字符串继续不停的延长进行匹配
        //i 可以理解成就是索引,扫描 stringBuilder
        for (int i = 0; i < stringBuilder.length(); ) {
            // 进行匹配的字符串长度
            int matchLength = 1;
            // 未匹配到就是true,匹配到了就是false
            boolean matchFlag = true;
            // 匹配到的字符byte
            Byte unzipByte = null;

            while (matchFlag) {
                String matchString = stringBuilder.substring(i, i + matchLength);
                // 进行匹配
                unzipByte = reverseHuffmanCodes.get(matchString);
                // 未匹配的,延长字符串,继续匹配
                if (unzipByte == null) {
                    matchLength++;
                } else {
                    // 匹配到从下一个字符初始化形成字符串继续不停的延长进行匹配
                    unzipByteList.add(unzipByte);
                    matchFlag = false;
                    i += matchLength;
                }
            }

        }

        // 将unzipByteList的数据,放到byte数组种,便于转换成字符串
        byte[] unzipBytes = new byte[unzipByteList.size()];
        for (int i = 0; i < unzipBytes.length; i++) {
            unzipBytes[i] = unzipByteList.get(i);
        }

        return unzipBytes;
    }


    // 对文件进行压缩
    public static void zipFile(String srcFile, String targetFile) {

        // 创建文件输出流
        FileOutputStream fileOutputStream = null;
        ObjectOutputStream objectOutputStream = null;
        // 创建文件输入流
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(srcFile);
            // 创建一个和srcFile大小一样的byte数组
            byte[] fileByte = new byte[fileInputStream.available()];
            // 将文件的数据保存到fileByte中
            fileInputStream.read(fileByte);

            // 对文件数据进行压缩
            byte[] huffmanBytes = huffmanZip(fileByte);

            fileOutputStream = new FileOutputStream(targetFile);
            objectOutputStream = new ObjectOutputStream(fileOutputStream);
            // 将压缩后的数据保存到输出文件
            objectOutputStream.writeObject(huffmanBytes);
            // 再将赫夫曼编码保存到输出文件
            objectOutputStream.writeObject(huffmanCodes);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                objectOutputStream.close();
                fileOutputStream.close();
                fileInputStream.close();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }

    }


    // 将压缩后的文件进行解压,然后进行保存
    public static void unZipFile(String zipFile, String targetFile) {

        // 创建文件输入流
        FileInputStream fileInputStream = null;
        ObjectInputStream objectInputStream = null;
        // 创建文件输出流
        FileOutputStream fileOutputStream = null;

        try {
            fileInputStream = new FileInputStream(zipFile);
            objectInputStream = new ObjectInputStream(fileInputStream);
            // 读取压缩后的数据
            byte[] huffmanBytes = (byte[]) objectInputStream.readObject();
            // 读取赫夫曼编码
            Map<Byte, String> huffmanCodes = (Map<Byte, String>) objectInputStream.readObject();

            // 进行解码
            byte[] unzipBytes = decode(huffmanCodes, huffmanBytes);

            // 将解压后的数据保存到输出文件
            fileOutputStream = new FileOutputStream(targetFile);
            fileOutputStream.write(unzipBytes);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                fileOutputStream.close();
                objectInputStream.close();
                fileInputStream.close();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }

        }
    }


}


// 节点类
// 实现Comparable接口,用于Collections.sort
// Node保存data和weight
class Node implements Comparable<Node> {
    // 用Byte存放字符。例如'a'对应97, ' '对应32
    public Byte charData;
    // 权重值,表示字符出现的次数
    public int weight;
    public Node left;
    public Node right;

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

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

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

    @Override
    public String toString() {
        return "Node [charData = " + charData + ", weight = " + weight + "]";
    }


}

运行程序,结果如下:

压缩后的byte数组是: [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28, 5], 长度是: 18
原来的字符串为: i like like like java do you like a java
压缩文件成功
解压文件成功

猜你喜欢

转载自blog.csdn.net/yy8623977/article/details/126835278