IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 赫夫曼编码 -> 正文阅读

[数据结构与算法]赫夫曼编码

赫夫曼编码

赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。

赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

通信领域中信息的处理方式1-定长编码

i like like like java do you like a java // 共40个字符(包括空格)
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 // 共40个字符(包括空格)

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=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…
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码(这个在赫夫曼编码中,我们还要进行举例说明, 不捉急)

通信领域中信息的处理方式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 // 各个字符对应的个数 。按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.(图后)

注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:

在这里插入图片描述
传输的 字符串

  1. i like like like java do you like a java

  2. d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数

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

步骤:
构成赫夫曼树的步骤:

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

  2. 取出根节点权值最小的节点

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

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

  5. 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为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

  6. 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
    1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133

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

赫夫曼编码压缩文件注意事项

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

public class HuffmanCode {

    public static Map<Byte, String> huffmanCodes = new ConcurrentHashMap<>();

    public static void main(String[] args) throws IOException {
        String s = "i like like like java do you like a java hello world";
        // 压缩
        byte[] bytes = huffmanZip(s.getBytes());
        // 解压缩
        byte[] decode = decode(huffmanCodes, bytes);

        System.out.println(new String(decode));

        String src = "D:\\Users\\jun.mao\\Desktop\\20220328_PrintGoodsCode\\Integer.txt";
        String desc = "D:\\Users\\jun.mao\\Desktop\\20220328_PrintGoodsCode\\Integer.zip";
        String reverse = "D:\\Users\\jun.mao\\Desktop\\20220328_PrintGoodsCode\\reverse.txt";
        zipFile(src, desc);
        unzipFile(desc, reverse);
    }

    /**
     * 压缩文件
     *
     * @param src
     * @param desc
     * @throws IOException
     */
    public static void zipFile(String src, String desc) throws IOException {
        OutputStream os = null;
        ObjectOutputStream oos = null;
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(src);
            byte[] bytes = new byte[fis.available()];
            fis.read(bytes);
            byte[] bytes1 = huffmanZip(bytes);
            os = new FileOutputStream(desc);
            oos = new ObjectOutputStream(os);
            oos.writeObject(bytes1);
            oos.writeObject(huffmanCodes);
        } catch (Exception e) {

        } finally {
            os.close();
            oos.close();
            fis.close();
        }
    }

    /**
     * 解压文件
     *
     * @param src
     * @param desc
     * @throws IOException
     */
    public static void unzipFile(String src, String desc) throws IOException {
        InputStream is = null;
        ObjectInputStream ios = null;
        OutputStream os = null;
        try {
            is = new FileInputStream(src);
            ios = new ObjectInputStream(is);
            os = new FileOutputStream(desc);
            byte[] bytes = (byte[]) ios.readObject();
            Map<Byte, String> map = (Map<Byte, String>) ios.readObject();
            byte[] decode = decode(map, bytes);
            os.write(decode);
        } catch (Exception e) {

        } finally {
            is.close();
            os.close();
            ios.close();
        }
    }

    /**
     * 使用赫夫曼编码压缩字节数组
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    public static byte[] huffmanZip(byte[] bytes) {
        // 1.获取每个字节出现的次数,即权值
        Map<Byte, Integer> number = getNumber(bytes);
        number.forEach((k, v) -> System.out.print((char) (int) k + "=" + v + " "));
        System.out.println();
        // 2.根据 number 创建的赫夫曼树
        HuffmanNode huffmanTree = createHuffmanTree(number);
        // 先根遍历
        huffmanTree.preOrder();
        // 3.根据赫夫曼树得到对应的赫夫曼编码
        getCodes(huffmanTree);
        System.out.println();
        huffmanCodes.forEach((k, v) -> System.out.print((char) (int) k + "=" + v + " "));
        System.out.println();
        // 4.根据生成的赫夫曼编码,得到压缩后的赫夫曼编码字节数组
        return zip(bytes, huffmanCodes);
    }

    /**
     * 将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
     *
     * @param bytes        这时原始的字符串对应的 byte[]
     * @param huffmanCodes 生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的 byte[]
     * 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返回的是 字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * => 对应的 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) {

        // 1.利用 huffmanCodes将bytes转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        // 遍历bytes 数组
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }

        System.out.println("转成赫夫曼编码对应的二进制:" + stringBuilder.toString());

        // 将 "1010100010111111110..." 转成 byte[]

        // 统计返回  byte[] huffmanCodeBytes 长度
        // 一句话 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];
        // 记录是第几个byte
        int index = 0;
        // 因为是每8位对应一个byte,所以步长 +8
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            String strByte;
            // 不够8位
            if (i + 8 > stringBuilder.length()) {
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            // 将转成赫夫曼编码对应的二进制转成一个二进制byte,放入到 huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        System.out.println("压缩后的数组:" + Arrays.toString(huffmanCodeBytes));
        return huffmanCodeBytes;
    }

    /**
     * 根据字节数组得到每个字节出现的次数
     *
     * @param bytes
     * @return
     */
    public static Map<Byte, Integer> getNumber(byte[] bytes) {

        Map<Byte, Integer> map = new HashMap<>();
        for (byte c : bytes) {
            if (map.containsKey(c)) {
                map.put(c, map.get(c) + 1);
            } else {
                map.put(c, 1);
            }
        }
        return map;
    }

    /**
     * 创建赫夫曼树,以权值做value,以字节做data
     *
     * @param map
     * @return
     */
    public static HuffmanNode createHuffmanTree(Map<Byte, Integer> map) {

        List<HuffmanNode> list = new ArrayList<>();
        for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
            list.add(new HuffmanNode(entry.getValue(), entry.getKey()));
        }

        while (list.size() > 1) {
            Collections.sort(list);
            HuffmanNode left = list.get(0);
            HuffmanNode right = list.get(1);
            HuffmanNode parent = new HuffmanNode(left.value + right.value, null);
            parent.left = left;
            parent.right = right;
            list.remove(left);
            list.remove(right);
            list.add(parent);
        }
        return list.get(0);
    }

    public static void getCodes(HuffmanNode node) {
        if (node != null) {
            StringBuilder builder = new StringBuilder();
            getCodes(node.left, "0", builder);
            getCodes(node.right, "1", builder);
        }
    }

    /**
     * 得到赫夫曼编码,就是各个节点的路径
     *
     * @param node    赫夫曼树
     * @param code    规定编码 ,向左的路径为0,向右的路径为1
     * @param builder 路径
     */
    public static void getCodes(HuffmanNode node, String code, StringBuilder builder) {
        StringBuilder builder2 = new StringBuilder(builder);
        builder2.append(code);
        if (node != null) {
            // 非叶子节点,也就是通过叶子节点构造出来的
            if (node.data == null) {
                getCodes(node.left, "0", builder2);
                getCodes(node.right, "1", builder2);
            } else {
                huffmanCodes.put(node.data, builder2.toString());
            }
        }
    }

    public void preOrder(HuffmanNode node) {
        if (node == null) {
            System.out.println("当前树为空!");
            return;
        } else {
            node.preOrder();
        }
    }

    /**
     * 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考Java基础 二进制的原码,反码,补码
     *
     * @param b 传入的 byte
     * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
     */
    public static String byte2BitString(byte b) {
        int temp = b;
        // 如果是正数我们还存在补高位 |=256 正数相加,负数不变
        // 按位与 256  1 0000 0000  | 0000 0001 => 1 0000 0001
        temp |= 256;

        /**
         * -88 对应的原码:11011000 最高位代表的是符号位,1为负数,0为正数
         * -88 对应的反码:10100111 符号位不变,其他位取反
         * -88 对应的补码:10101000 反码+1
         * 返回的是temp对应的二进制的补码  -88 11111111111111111111111110101000
         * toBinaryString :负数高位补1,正数高位补0,相当于没补,0不显示
         */
        String s = Integer.toBinaryString(temp);

        /**
         * 为啥减8?因为只有后8位才是有效的!负数高位补1,需要去除,正数在!=256相当于+256,此时大于256,高位是多于的,需要去除
         */
        return s.substring(s.length() - 8);
    }

    /**
     * 对压缩数据的解码
     *
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 就是原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {

        // 1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
        StringBuilder stringBuilder = new StringBuilder();
        // 将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            stringBuilder.append(byte2BitString(huffmanBytes[i]));
        }
        // 把字符串安装指定的赫夫曼编码进行解码
        // 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
        Map<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }

        // 创建要给集合,存放byte
        List<Byte> list = new ArrayList<>();
        //i 可以理解成就是索引,扫描 stringBuilder
        for (int i = 0; i < stringBuilder.length(); ) {
            // 小的计数器
            int count = 1;
            boolean flag = true;
            Byte b = null;

            while (flag) {
                // 1010100010111...
                // i 不动,让count移动,递增的取出 key,指定匹配到一个字符
                String key = stringBuilder.substring(i, i + count);
                b = map.get(key);
                // 说明没有匹配到
                if (b == null) {
                    count++;
                } else {
                    // 匹配到
                    flag = false;
                }
            }
            list.add(b);
            // i 直接移动到 count
            i += count;
        }
        // 当for循环结束后,我们list中就存放了所有的字节
        // 把list 中的数据放入到byte[] 并返回
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }
}

class HuffmanNode implements Comparable<HuffmanNode> {

    public int value;
    public Byte data;
    public HuffmanNode left;
    public HuffmanNode right;


    public void preOrder() {
        System.out.print(this.value + " ");
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }

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

    public HuffmanNode(int value, Byte data) {
        this.value = value;
        this.data = data;
    }

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

    @Override
    public int compareTo(HuffmanNode o) {
        return this.value - o.value;
    }
}

 =11 a=5 d=2 e=5 h=1 i=5 j=2 k=4 l=7 o=4 r=1 u=1 v=2 w=1 y=1 
52 21 10 5 5 11 31 14 7 7 3 1 2 4 17 8 4 4 2 2 9 4 2 1 1 2 1 1 5 
 =01 a=1111 d=10101 e=000 h=111000 i=001 j=11010 k=1011 l=100 o=1100 r=111001 u=111010 v=11011 w=111011 y=10100 
转成赫夫曼编码对应的二进制:0010110000110110000110000110110000110000110110000111010111111011111101101011100011010011001110100110000110110000111110111010111111011111101111000000100100110001111011110011100110010101
压缩后的数组:[44, 54, 24, 108, 48, -40, 117, -5, -10, -72, -45, 58, 97, -80, -5, -81, -33, -68, 9, 49, -17, 57, -107]
i like like like java do you like a java hello world

原码、反码、补码

一、前言
1、计算机在任何情况下都只能识别二进制
2、计算机在底层存储数据的时候,一律存储的是“二进制的补码形式”

计算机采用补码形式存储数据的原因是:补码形式效率最高。
3、什么是补码呢?

实际上是这样的,二进制有:原码 反码 补码

注:一个二进制数,首位0表示该数是正数,首位是1表示该数是负数。
二、正文

在java中int占4个字节32位。

对于一个正数来说:二进制原码、反码、补码是同一个,完全相同。

int i = 1;
对应的二进制原码:00000000 00000000 00000000 00000001
对应的二进制反码:00000000 00000000 00000000 00000001
对应的二进制补码:00000000 00000000 00000000 00000001

对于一个负数来说:二进制原码、反码、补码是什么关系呢?
int i = -1;
对应的二进制原码:10000000 00000000 00000000 00000001
对应的二进制反码(符号位不变,其它位取反):11111111 11111111 11111111 11111110
对应的二进制补码(反码+1):11111111 11111111 11111111 11111111

位与、位或、异或、位移

进行位运算时用的是补码,计算机用的是实际存储的二进制跑的位运算

位与(&)

参与运算的两个数据,按照二进制位进行“与运算”。
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;

即:两位同时为1,则值为1。否则为0
例如:-5 & 5 = 1011 & 0101 = 0001=1

位或(|)

参与运算的两个数据,按照二进制位进行“或运算”。
运算规则:0&0=0; 0&1=1; 1&0=1; 1&1=1;
即:参与运算的两个数据只要有一个值为1 那么值为1
例如:-5 | 5 = 1011| 0101 = 1111 = -1

异或 (^)

参与 运算的两个数据,按照二进制位进行“异或运算”。
运算规则: 0&0=0; 0&1=1; 1&0=1; 1&1=0;
即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
例如:-5 ^ 5 = 1011^0101 = 1110 = -2

左位移(<<):正数或者负数左移,低位都是用0补

a<<b 表示把a转为二进制后左移b位(在后面添加 b个0)。例如8的二进制表示为00001000,8左移2位后(后面加2个零):00001000<<2 =00100000=32,可以看出,a<<b的值实际上就是a乘以2的b次方,因为在二进制数后面添加一个0就相当该数乘以2,2个零即2的2次方 等于4。通常认为a<<1比a*2更快,因为前者是更底层一些的操作。因此程序中乘以2的操作尽量用左移一位来代替。
定义一些常量可能会用到<<运算。你可以方便的用1<<16 -1 来表示65535(unsingned int 最大值16位系统)。很多算法和数据结构要求数据模块必须是2的幂,此时就可以用<<来定义MAX_N等常量。

右位移(>>):正数右移,高位用0补,负数右移,高位用1补

和<<相似,a>>b表示二进制右移b位(去掉末b位),相当于a除以2的b次方(取整)。我们经常用>>1来代替 /2(div 2),比如二分查找、堆的插入操作等等。想办法用>>代替除法运算可以使程序的效率大大提高。最大公约数的二进制算法用除以2操作来代替慢的出奇的%(mod)运算,效率可以提高60%。
int a =8;
a/4 ==a>>2;

无符号右移(>>>):无论正负高位均用0补,即负数会变成正数。

-5>>>2
-5的补码:
11111111111111111111111111111011
无符号右移两位:00111111111111111111111111111110

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-05-14 10:09:04  更:2022-05-14 10:09:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 1:25:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码