ハフマンツリーとハフマンコーディング(Java実装)

ハフマンツリー

関連する概念

二分木の加重パス長:

二分木にn個の重み付きリーフノードがあり、ルートノードから各リーフノードまでのパスの長さと対応するリーフノードの重みの積の合計であるとします。記録:
ここに画像の説明を挿入
例:
4つの葉ノードが与えられた場合、それらの重みはそれぞれ{2、3、4、7}であり
ここに画像の説明を挿入
さまざまな形状の二分木を構築できます。さまざまな形状のハフマンツリー:特定の重みを持つ葉のセットが与えられたノード、重み付きパス長が最小の二分木は、ハフマンツリーと呼ばれ、最適な二分木とも呼ばれます。

ハフマンツリーの特徴:

  • 重みが大きいリーフノードはルートノードに近く、重みが小さいリーフノードはルートノードから遠くなります。(ハフマンツリーを構築するためのコアアイデア)
  • 次数0(リーフノード)と次数2(ブランチノード)のノードのみがあり、次数1のノードは存在しません。
  • n個のリーフノードを持つハフマンツリーのノードの総数は2n-1です。
  • ハフマンツリーは一意ではありませんが、WPLは一意です。
    ここに画像の説明を挿入

ハフマンツリーの作成

ハフマンツリーを形成する手順:

  1. 小さいものから大きいものへと並べ替えます。各データはノードであり、各ノードは最も単純な二分木と見なすことができます。
  2. ルートノードの重みが最小の2つの二分木を取り出します
  3. 新しい二分木を形成するために、新しい二分木のルートノードの重みは、前の2つの二分木のルートノードの重みの合計です。
  4. 次に、この新しい二分木をルートノードの重みで再度並べ替え、シーケンス内のすべてのデータが処理されてハフマンツリーが取得されるまで、1-2-3-4の手順を繰り返します。

図解例:{13、7、8、3、29、6、1}


{ 1、3、6、7、8、13、29 }を並べ替えて、ハフマンツリーコードの実装作成
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
ます

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

public class HuffmanTree {
    
    

    public static void main(String[] args) {
    
    
        int arr[] = {
    
     13, 7, 8, 3, 29, 6, 1 };
        Node root = createHuffmanTree(arr);
        //测试一把
        preOrder(root); //
    }

    //编写一个前序遍历的方法
    public static void preOrder(Node root) {
    
    
        if(root != null) {
    
    
            root.preOrder();
        }else{
    
    
            System.out.println("是空树,不能遍历~~");
        }
    }

    // 创建赫夫曼树的方法
    /**
     *
     * @param arr 需要创建成哈夫曼树的数组
     * @return 创建好后的赫夫曼树的root结点
     */
    public static Node createHuffmanTree(int[] arr) {
    
    
        // 第一步为了操作方便
        // 1. 遍历 arr 数组
        // 2. 将arr的每个元素构成成一个Node
        // 3. 将Node 放入到ArrayList中
        List<Node> nodes = new ArrayList<Node>();
        for (int value : arr) {
    
    
            nodes.add(new Node(value));
        }
        //我们处理的过程是一个循环的过程
        
        while(nodes.size() > 1) {
    
    
            //排序 从小到大
            Collections.sort(nodes);
          //  System.out.println("nodes =" + nodes);
            //取出根节点权值最小的两颗二叉树
            //(1) 取出权值最小的结点(二叉树)
            Node leftNode = nodes.get(0);
            //(2) 取出权值第二小的结点(二叉树)
            Node rightNode = nodes.get(1);
            //(3)构建一颗新的二叉树
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;
            //(4)从ArrayList删除处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //(5)将parent加入到nodes
            nodes.add(parent);
        }
        //返回哈夫曼树的root结点
        return nodes.get(0);

    }
}

// 创建结点类
// 为了让Node 对象持续排序Collections集合排序
// 让Node 实现Comparable接口
class Node implements Comparable<Node> {
    
    
    int value; // 结点权值
    char c; //字符
    Node left; // 指向左子结点
    Node right; // 指向右子结点

    //写一个前序遍历
    public void preOrder() {
    
    
        System.out.println(this);
        if(this.left != null) {
    
    
            this.left.preOrder();
        }
        if(this.right != null) {
    
    
            this.right.preOrder();
        }
    }
    public Node(int value) {
    
     this.value = value; }
    @Override
    public String toString() {
    
     return "Node [value=" + value + "]"; }

    @Override
    public int compareTo(Node o) {
    
    
        // TODO Auto-generated method stub
        // 表示从小到大排序
        return this.value - o.value;
    }

}

ハフマン符号化

基本的な紹介

ハフマン符号化は、ハフマン符号化(ハフマン符号化)とも呼ばれ、ハフマン符号化とも呼ばれます。符号化は方法であり、一種のプログラムアルゴリズムです。
ハフマン符号化は、ヘハフマンツリーの古典的な電気通信通信アプリケーションの1つです。
ハフマン符号化は、データファイルの圧縮に広く使用されています。圧縮率は通常20%から90%の間です。
ハフマンコードは可変ワード長コーディング(VLC)の一種です。ハフマンは1952年に最高のコーディングと呼ばれるコーディング方法を提案しました

関連用語

コードのプレフィックス:コードのグループ内のいずれかのコードが他のコードのプレフィックスではない場合、このコードにはプレフィックスプロパティがあると言われます。これは略してプレフィックスコードと呼ばれます。

  • プレフィックスエンコーディングは、デコード(デコード)中の一意性を保証します。
  • 等しい長さのコーディングにはプレフィックスがあります。
  • 可変長コーディングは、デコードにあいまいさを引き起こす可能性があります。つまり、プレフィックスがありません。たとえば、E(00)、T(01)、W(0001)の場合、デコード中にバイナリ文字列0001がETであるかWであるかを判別することはできません。

平均コード長:

  • 特定の文字セット(オブジェクトのセット)に対して、複数のエンコードスキームが存在する場合がありますが、最適なものを選択する必要があります。
  • 平均エンコード長:各(オブジェクト)文字cjの出現確率をpj、バイナリビット文字列長(コード長)をljに設定すると、∑lj・pjはオブジェクト(文字)のグループの平均エンコード長を表します。 。
  • 最適なプレフィックスコード:平均コード長∑lj・pjを最小化するプレフィックスコードは、最適なプレフィックスコードと呼ばれます

主成分分析

通信分野における情報処理方法1-固定長符号化:

私はJavaが好きですかJavaが好きですか//合計40文字(スペースを含む)
がASCIIコードに対応します:
105 32108105107101 32108105107101 32108105107101 32106 97118 97 32100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97
対応するバイナリ
01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01101111 01100000 01100001 001101111 01101001001001001001001001001001001001001001001001001001001001001101001001001001001001001101001001001001001001001101001001101001001101001001101001001101001001101001101001 00100000 01100001 00100000 01101010 01100001 01110110 01100001

情報はバイナリで送信され、全長は359(スペースを含む)です。

通信分野における情報処理手法-ハフマン符号化

私はJavaが好きですか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です。エンコードは次のとおりです
。o:1000 u:10010 d:100110 y:100111 i:101 a:110 k:1110 e:1111 j:0000 v:0001 l:001:01

上記のハフマン符号化によると、「私はJavaが好きですか、Javaが好きですか」という文字列は、符号化に対応しています(ここで使用する可逆圧縮に注意してください)。

1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110

長さは次のとおりです。133
説明:
元の長さは359、圧縮(359-133)/ 359 = 62.9%
このコードはプレフィックスコードを満たします。つまり、文字コードを他の文字コードのプレフィックスにすることはできません。マッチングにあいまいさを引き起こしません

注:このハフマンツリーは、並べ替え方法によって異なる場合があるため、対応するハフマンコードは完全に同じではありませんが、wplは同じであり、すべて最小です。

ハフマンアルゴリズムを使用して文字セットの最適なプレフィックスエンコーディングを見つけるためのアルゴリズム

  • 文字セット内の各文字を、リーフノードのみを持つ二分木に対応させます。リーフの重みは、対応する文字の使用頻度です----初期化
  • ハフマンアルゴリズムを使用して、ハフマンツリー構築アルゴリズムを構築します
  • ハフマンツリーの各ノードについて、左のブランチに0を、右のブランチに1をアタッチします(またはその逆)。ルートからリーフへのパス上の0と1のシーケンスは、対応する文字のコードです。 ---ハフマンコード
  • そして、最適なプレフィックスコードです

例:
送信された文字列
(1)Javaが好きですか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. ルートノードの重みが最小の2つの二分木を取り出します
  3. 新しい二分木を形成するために、新しい二分木のルートノードの重みは、前の2つの二分木のルートノードの重みの合計です。
  4. 次に、この新しい二分木をルートノードの重みで再度並べ替え、シーケンス内のすべてのデータが処理されてハフマンツリーが取得されるまで、1-2-3-4の手順を繰り返します。

ここに画像の説明を挿入

(4)ハフマンツリーに従って、各文字のコード(プレフィックスコード)を指定します。左側のパスは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

(5)上記のハフマンコードによると、「i like like java do you like a java」文字列に対応するコードは、(ここで使用するロスレス圧縮に注意してください)
10101001101111011110100110111101111010011011110111101000011000011100110011001111000011001111000100100100110111101111011100100001100001110です。

(6)長さは次のとおりです。133
説明:
元の長さは359、圧縮(359-133)/ 359 = 62.9%
このコードはプレフィックスコードを満たします。つまり、文字コードを他の文字コードのプレフィックスにすることはできません。マッチングにあいまいさを生じさせません。
ハフマンコーディングはロスレス処理ソリューションです。

コード

import java.util.*;

/**
 * @anthor longzx
 * @create 2021 02 04 11:25
 * @Description
 **/
public class HuffmanCode {
    
    

    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
    //   生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
    static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
    //2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();


    public static void main(String[] args) {
    
    
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        System.out.println(contentBytes.length); //40
        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes:"+nodes);

        //创建哈夫曼树
        System.out.println("哈夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        huffmanTreeRoot.preOrder();
        //生成哈夫曼编码
        getCodes(huffmanTreeRoot,"",stringBuilder);
        System.out.println("生成的哈夫曼编码:" + huffmanCodes);
    }

    /**
     * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
     * @param node  传入结点
     * @param code  路径: 左子结点是 0, 右子结点 1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
    
    
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code 加入到 stringBuilder2
        stringBuilder2.append(code);
        if(node != null) {
    
     //如果node == null不处理
            //判断当前node 是叶子结点还是非叶子结点
            if(node.data == null) {
    
     //非叶子结点
                //递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {
    
     //说明是一个叶子结点
                //就表示找到某个叶子结点的最后
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }


    /**
     *
     * @param bytes 接收字节数组
     * @return 返回的就是 List 形式   [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......],
     */
    private static List<Node> getNodes(byte[] bytes) {
    
    

        //1创建一个ArrayList
        ArrayList<Node> nodes = new ArrayList<Node>();
        //遍历 bytes , 统计 每一个byte出现的次数->map[key,value]
        Map<Byte, Integer> counts = new HashMap<>();
        for (byte b : bytes) {
    
    
            Integer count = counts.get(b);
            if (count == null) {
    
     // Map还没有这个字符数据,第一次
                counts.put(b, 1);
            } else {
    
    
                counts.put(b, count + 1);
            }
        }
        //把每一个键值对转成一个Node 对象,并加入到nodes集合
        //遍历map
        for(Map.Entry<Byte, Integer> entry: counts.entrySet()) {
    
    
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }


    //可以通过List 创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
    
    

        while(nodes.size() > 1) {
    
    
            //排序, 从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点 没有data, 只有权值
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            //将已经处理的两颗二叉树从nodes删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树,加入到nodes
            nodes.add(parent);

        }
        //nodes 最后的结点,就是赫夫曼树的根结点
        return nodes.get(0);

    }

}

//创建Node ,待数据和权值
class Node implements Comparable<Node>  {
    
    
    Byte data; // 存放数据(字符)本身,比如'a' => 97 ' ' => 32
    int weight; //权值, 表示字符出现的次数
    Node left;//
    Node right;
    public Node(Byte data, int weight) {
    
     this.data = data;this.weight = weight; }
    @Override
    public int compareTo(Node o) {
    
    
        // 从小到大排序
        return this.weight - o.weight;
    }
    public String toString() {
    
     return "Node [data = " + data + " weight=" + weight + "]"; }
    //前序遍历
    public void preOrder() {
    
    
        System.out.println(this);
        if(this.left != null) {
    
    
            this.left.preOrder();
        }
        if(this.right != null) {
    
    
            this.right.preOrder();
        }
    }
}

おすすめ

転載: blog.csdn.net/qq_41784749/article/details/113622210