赫夫曼编码与解码

赫夫曼编码

基本介绍

  1. 赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
  2. 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
  3. 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
  4. 赫夫曼码是可变字长编码(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
    • 按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了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
    • 按照上面给各个字符规定的编码,则我们在传输 “i like like like java do you like a java” 数据时,编码就是 10010110100
    • 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码(这个在赫夫曼编码中,我们还要进行举例说明, 不捉急)

下面我们来看看通信领域信息处理方式3,赫夫曼编码

赫夫曼编码
  1. 给一个字符串“”i like like like java do you like a java”共40个字符(包括空格)

  2. 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值,如图:
    在这里插入图片描述

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

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

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

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

赫夫曼编码示例

要求:将给出的一段文本,比如 “i like like like java do you like a java” , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理 ,最后处理结果形式如 “1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110”

赫夫曼编码类

有详细注释,各个方法都有详细说明

public class HufmanCode {
    // 赫夫曼编码表,ket保存的是该字节,value保存的对应的编码如:"10001"
    public HashMap<Byte, String> hufmanCodeMap = new HashMap<Byte, String>();

    /**
     * 赫夫曼编码
     * @param data
     */
    public byte[] hufumanZip(byte[] data) {
        // 先对数据转成节点集合
        List<HufcodeNode> nodeList = getNodeList(data);
        // 构建赫夫曼树
        HufcodeNode hufmanTree = getHufmanTree(nodeList);
        // 编码初始化赫夫曼编码表
        getCodes(hufmanTree);
        // 对数据进行编码
        byte[] zip = zip(data, hufmanCodeMap);
        return zip;
    }

    /**
     * 把数据根据赫夫曼编码表,转成对于的赫夫曼编码数据
     * @param data 要压缩的数据
     * @param hufmanCode 赫夫曼编码表
     * @return  返回压缩后的数据
     */
    private byte[] zip(byte[] data, HashMap<Byte, String> hufmanCode) {
        if (data == null || data.length == 0) {
            return null;
        }
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : data) {
            String code = hufmanCode.get(b);
            stringBuilder.append(code);
        }
//        System.out.println(stringBuilder.toString());
        // 定义编码的长度
        int length;
        if (stringBuilder.length() % 8 == 0) {
            length = stringBuilder.length()/8;
        }else {
            length = stringBuilder.length()/8 + 1;
        }

        // 创建byte数组,存储压缩后的数组
        byte[] huffmanCodeBytes = new byte[length];
        int index = 0;//记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i += 8) { //因为是每8位对应一个byte,所以步长 +8
            String strByte;
            if(i+8 > stringBuilder.length()) {//不够8位
                strByte = stringBuilder.substring(i);
            }else{
                strByte = stringBuilder.substring(i, i + 8);
            }
            //将strByte 转成一个byte,放入到 huffmanCodeBytes
            huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }

    /**
     * 把数据转成节点集合
     * @param data
     * @return
     */
    private List<HufcodeNode> getNodeList(byte[] data) {
        if (data == null || data.length == 0) {
            return null;
        }
        HashMap<Byte, Integer> hashMap = new HashMap<>();
        for (Byte b : data) {
            Integer count = hashMap.get(b);
            if (count == null) {
                // 没有就往map里面添加
                hashMap.put(b, 1);
            }else {
                // 有就覆盖,把value加1
                hashMap.put(b, count + 1);
            }
        }
        // 在遍历map,把他的数据封装成HufcodeNode,并存放到list中
        List<HufcodeNode> nodeList = new ArrayList<>();
        for (Map.Entry<Byte, Integer> entry : hashMap.entrySet()) {
            HufcodeNode hufcodeNode = new HufcodeNode();
            hufcodeNode.setData(entry.getKey());
            hufcodeNode.setWeight(entry.getValue());

            nodeList.add(hufcodeNode);
        }
        return nodeList;
    }

    /**
     * 构造一颗赫夫曼树
     * @param nodeList
     * @return
     */
    private HufcodeNode getHufmanTree(List<HufcodeNode> nodeList) {
        if (nodeList == null || nodeList.size() == 0) {
            return null;
        }

        while (nodeList.size() > 1) {
            // 先排序
            Collections.sort(nodeList);
            // 去除集合前2个数
            HufcodeNode hufcodeNode = nodeList.get(0);
            HufcodeNode hufcodeNode1 = nodeList.get(1);
            // 创建一个新节点,
            HufcodeNode hufcodeNode2 = new HufcodeNode();
            hufcodeNode2.setWeight(hufcodeNode.getWeight() + hufcodeNode1.getWeight());
            hufcodeNode2.left = hufcodeNode;
            hufcodeNode2.right = hufcodeNode1;
            // 移除取出的节点
            nodeList.remove(hufcodeNode);
            nodeList.remove(hufcodeNode1);
            // 加入到集合
            nodeList.add(hufcodeNode2);
        }
        return nodeList.get(0);
    }


    //为了调用方便,我们重载 getCodes
    private  void getCodes(HufcodeNode root) {
        StringBuilder stringBuilder = new StringBuilder();
        if(root == null) {
            return ;
        }
        //处理root的左子树
        getHufmanCode(root.left, "0", stringBuilder);
        //处理root的右子树
        getHufmanCode(root.right, "1", stringBuilder);
    }

    /**
     * 获取赫夫曼编码
     * @param node 赫夫曼树
     * @param path 如果往左子节点走,path就是"0",往右path就是"1"
     * @param stringBuilder 用于拼接路径
     * @return
     */
    private void getHufmanCode(HufcodeNode node, String path, StringBuilder stringBuilder) {

        if (node == null) {
            return;
        }
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        stringBuilder2.append(path);

        // 判断该节点有左子节点,则继续递归
        if (node.left != null) {
            // 如果左子节点,则递归
            getHufmanCode(node.left, "0", stringBuilder2);
        }
        // 如果该节点有右子节点,则递归
        if (node.right != null) {
            getHufmanCode(node.right, "1", stringBuilder2);
        }

        // 如果代码走到这,说明已经是叶子节点了,stringBuilder2中的路径就是该节点对应的编码
        hufmanCodeMap.put(node.getData(), stringBuilder2.toString());
    }
}

测试类:

public class HufmanCodeTest {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] bytes = content.getBytes();
        System.out.println("压缩前的字节数:" + bytes.length);
        HufmanCode hufmanCode = new HufmanCode();
        byte[] zip = hufmanCode.hufumanZip(bytes);
        System.out.println("压缩后的字节数:" + zip.length);

        // 解码
//        HufmanDecode hufmanDecode = new HufmanDecode();
//        byte[] decode = hufmanDecode.decode(hufmanCode.hufmanCodeMap, zip);
//        System.out.println(new String(decode) + "-----------");
    }
}

运行结果

压缩前的字节数:40
压缩后的字节数:19

从结果我们可以看出,赫夫曼编码对原文件压缩已经压缩了一半多了

赫夫曼解码
  1. 使用赫夫曼编码来解码数据,具体要求是:
  2. 前面我们得到了赫夫曼编码和对应的编码byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
  3. 现在要求使用赫夫曼编码, 进行解码,又重新得到原来的字符串"i like like like java do you like a java

具体实现看以下代码:

/**
 * 解码
 */
public class HufmanDecode {

    /**
     *
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 就是原来的字符串对应的数组
     */
    public byte[] decode(Map<Byte,String> huffmanCodes, byte[] huffmanBytes) {
        //1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
        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<String,Byte>();
        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...
                //递增的取出 key 1
                String key = stringBuilder.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符
                b = map.get(key);
                if(b == null) {//说明没有匹配到
                    count++;
                }else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i += count;//i 直接移动到 count
        }

        //当for循环结束后,我们list中就存放了所有的字符  "i like like like java do you like a java"
        //把list 中的数据放入到byte[] 并返回
        byte b[] = new byte[list.size()];
        for(int i = 0;i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    /**
     * 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
     * @param b 传入的 byte
     * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
     * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
     */
    private static String byteToBitString(boolean flag, byte b) {
        //使用变量保存 b
        int temp = b; //将 b 转成 int
        //如果是正数我们还存在补高位
        if(flag) {
            temp |= 256; //按位与 256  1 0000 0000  | 0000 0001 => 1 0000 0001
        }
        String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
        if(flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }
}

测试:

public class HufmanCodeTest {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] bytes = content.getBytes();
        System.out.println("压缩前的字节数:" + bytes.length);
        HufmanCode hufmanCode = new HufmanCode();
        byte[] zip = hufmanCode.hufumanZip(bytes);
        System.out.println("压缩后的字节数:" + zip.length);

        // 解码
        HufmanDecode hufmanDecode = new HufmanDecode();
        byte[] decode = hufmanDecode.decode(hufmanCode.hufmanCodeMap, zip);
        System.out.println( "使用赫夫曼解压恢复原来数据结果:"+ new String(decode));
    }
}

运行结果

压缩前的字节数:40
压缩后的字节数:19
使用赫夫曼解压恢复原来数据结果:i like like like java do you like a java
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
以下是C++实现赫夫曼编码解码的完整代码,注释比较详细,希望对您有帮助。 ```c++ #include <iostream> #include <string> #include <queue> #include <map> #include <fstream> using namespace std; // 哈夫曼树节点结构体 struct HuffmanNode { char ch; // 字符 int weight; // 权重 HuffmanNode *left, *right; // 左右子节点 HuffmanNode(char c, int w) : ch(c), weight(w), left(nullptr), right(nullptr) {} HuffmanNode(int w) : ch('\0'), weight(w), left(nullptr), right(nullptr) {} }; // 用于哈夫曼树节点的比较,用于优先队列 struct cmp { bool operator()(HuffmanNode* a, HuffmanNode* b) { return a->weight > b->weight; } }; // 建立哈夫曼树 HuffmanNode* buildHuffmanTree(map<char, int>& freq) { priority_queue<HuffmanNode*, vector<HuffmanNode*>, cmp> pq; // 将字符和对应的频率存入优先队列 for (auto& p : freq) { pq.push(new HuffmanNode(p.first, p.second)); } while (pq.size() > 1) { // 取出两个权重最小的节点作为左右子节点,合并成一个新节点 HuffmanNode* left = pq.top(); pq.pop(); HuffmanNode* right = pq.top(); pq.pop(); HuffmanNode* parent = new HuffmanNode(left->weight + right->weight); parent->left = left; parent->right = right; // 将新节点加入优先队列 pq.push(parent); } return pq.top(); } // 哈夫曼编码表 map<char, string> HuffmanCodeTable; // 生成哈夫曼编码表 void generateHuffmanCodeTable(HuffmanNode* root, string code) { if (root == nullptr) { return; } if (root->left == nullptr && root->right == nullptr) { HuffmanCodeTable[root->ch] = code; return; } generateHuffmanCodeTable(root->left, code + '0'); generateHuffmanCodeTable(root->right, code + '1'); } // 哈夫曼编码 string huffmanEncode(string text) { string code; for (char c : text) { code += HuffmanCodeTable[c]; } return code; } // 哈夫曼解码 string huffmanDecode(string code, HuffmanNode* root) { HuffmanNode* p = root; string text; for (char c : code) { if (c == '0') { p = p->left; } else { p = p->right; } if (p->left == nullptr && p->right == nullptr) { text += p->ch; p = root; } } return text; } int main() { // 统计字符频率 map<char, int> freq; ifstream fin("input.txt"); char c; while (fin >> noskipws >> c) { freq[c]++; } fin.close(); // 建立哈夫曼树 HuffmanNode* root = buildHuffmanTree(freq); // 生成哈夫曼编码表 generateHuffmanCodeTable(root, ""); // 读取要编码的文本 fin.open("input.txt"); string text; while (fin >> noskipws >> c) { text += c; } fin.close(); // 哈夫曼编码 string code = huffmanEncode(text); // 将编码结果写入文件 ofstream fout("output.txt"); fout << code; fout.close(); // 读取要解码的文本 fin.open("output.txt"); code.clear(); while (fin >> noskipws >> c) { code += c; } fin.close(); // 哈夫曼解码 string decodedText = huffmanDecode(code, root); // 将解码结果写入文件 fout.open("decoded.txt"); fout << decodedText; fout.close(); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值