我所知道的算法之哈夫曼編碼

上一篇文章中提到數據結構:哈夫曼樹,今天接着學習由哈夫曼提出編碼方式,一種程序算法。簡稱:哈夫曼編碼java

在線轉碼工具: https://www.mokuge.com/tool/a...node

1、什麼是哈夫曼編碼?

與哈夫曼樹同樣,會不會有小夥伴對哈夫曼編碼很陌生,有疑惑算法

問題疑惑

1.哈夫曼編碼是作什麼的?有什麼用?
2.爲何要使用哈夫曼編碼?使用別的編碼行不行?數組

哈夫曼編碼是作什麼的?有什麼用?

哈夫曼(Huffman)編碼算法

它是基於二叉樹構建編碼壓縮結構的,它是數據壓縮中經典的一種算法:根據文本字符出現的頻率,從新對字符進行編碼數據結構

那麼就會有小夥伴說:什麼是數據壓縮?什麼是字符編碼?app

字符編碼壓縮介紹

在咱們的世界裏,咱們能夠看到漂亮的圖像、好聽的聲音、震撼的視頻等等這些精彩內容。可是對於計算機說,它的世界裏只有二進制的 0 和 1ide

image.png

所以在數字電子電路中,邏輯門的實現直接應用了二進制,所以現代的計算機和依賴計算機的設備裏都用到二進制。工具

那麼在二進位電腦系統中,存儲的單位有:bit、kb、mb、gb學習

bit 電腦記憶體中最小的單位,每一bit 只表明0 或 1 的數位訊號測試

image.png

Byte由八個 bits 所組成,可表明:字元(A~Z)、數字(0~9)、或符號(,.?!%&+-*/)

image.png

當記憶體容量過大時,位元組(byte)這個單位就不夠用,所以就有千位元組的單位KB出現,如下乃個記憶體計算單位之間的相關性:

  • 1 Byte = 8 Bits
  • 1 KB = 1024 Bytes
  • 1 MB = 1024 KB
  • 1 GB = 1024 MB

咱們如今在計算機上看到的一切的圖像、聲音、視頻等內容,都是由二進制的方式進行存儲

image.png

簡單來說,咱們把信息轉化爲二進制的過程能夠稱之爲編碼

編碼方式從長度上來分大體能夠分爲兩個大類:

  • 定長編碼代表:段與段之間長度相同,沒說明是多長。
  • 變長編碼代表:段與段之間的長度不相同,不定義具體有多長。

請注意!這裏並無標明是多長!這會致使沒法區分編碼信息與文件內容的分離!形成亂碼!

天然語言的分隔問題
民可以使由之不可以使知之 ——_出自《論語 第八章 泰伯篇》_

這麼一串十個字要如何去分隔並解釋呢?

斷法解釋一:

民可以使由之,不可以使知之。

解釋:你能夠去驅使你的民衆,但不可以讓他們知道爲何(不要去教他們知識)

斷法解釋二:

民可,使由之;不可,使知之。

解釋:民衆能夠作的,放手讓他們去作;不會作的,教會他們如何去作(又或:不能夠去作的,讓他們明白爲什麼不能夠)

顯然,以上的文字是以某種定長或變長的方式組合在一塊兒的,可是關於它們如何分隔的信息則被丟棄了,因而在解釋時就存在產生歧義可能了。

舉個栗子,假如咱們對 「pig」 進行自定義編碼,使用定長編碼,爲了方便,採用了十進制,原理與二進制是同樣的。

image.png

假設咱們如今有文件,內容是:00080008

假如定長 2 位是惟一的編碼方案,用它去解碼就會獲得:「pipi」
假如定長 4 位是惟一的編碼方案,用它去解碼就會獲得:「ii」

image.png

若是文件的內容並無暗示它使用了何種編碼!就會出現解釋時存在產生歧義

這就比如孔夫子寫下「民可以使由之不可以使知之」時

並無暗示它是5|5分隔民可以使由之|不可以使知之

仍是暗示它是2|3|2|3分隔民可|使由之|不可|使知之)。

其實當咱們有字節這一基本單位後,咱們就能夠說得更具體:如定長一字節或者定長二字節

好比說ASCII碼它是最先也是最簡單的一種字符編碼方案:使用定長一字節來表示一個字符

示例編碼認識差別

其實如今咱們的計算機的世界裏編碼總已經有不少種格式

好比常見的: ASCII 、 Unicode 、 GBK 、 GB2312 、 GB18030 、 UTF-8 、 UTF-16 等等。

image.png

ASCII編碼介紹

咱們都知道在計算機的世界裏只有二進制0和1,經過不一樣的排列組合,可使用 0 和 1 就能夠表示世界上全部的東西

是否是有咱們中國"太極"的感受。

——「太極生兩儀,兩儀生四象,四象生八卦

而在計算機中:一字節 = 八位二進制數(二進制有0和1兩種狀態)
所以一字節能夠組合出二百五十六種狀態。

若是這二百五十六種狀態每個都對應一個符號,就能經過一字節的數據表示二百五十六個字符

美國人因而就制定了一套編碼(其實就是個字典),描述英語中的字符和這八位二進制數的對應關係,這被稱爲 ASCII 碼。

image.png

隨着時代的發展,不只美國人要對他們的英文進行編碼,咱們中國人也要對漢字進行編碼。

而早期的 ASCII 碼的方案只有一個字節,對咱們漢字文化而言是遠遠不夠的

編碼的發展

這點可憐的空間拿到中國來,它能編碼的漢字量也就小學一年級的水平。

這也就致使了不一樣的需求推進了發展,各類編碼方案都出來了,彼此之間的轉換也帶來了諸多的問題。採用某種統一的標準就勢在必行了,因而乎天上一聲霹靂,Unicode登場!

Unicode早期是做爲定長二字節方案出來的。它的理論編碼空間一下達到了64k

不過對於能夠只須要使用到一個字節的ASCII 碼就能夠表示的美國人來說,讓他們使用 Unicode ,多少仍是不是很願意的。

好比 「he」 兩個字符,用 ASCII 只須要保存成 6865 ( 16 進制),如今則成了 00680065 ,前面多的那兩個 0 (用做站位)。

基本上能夠說是毫無心義,用 Unicode 編碼的文本,原來可能只有 1KB 大小,如今要變成 2KB ,體積成倍的往上漲。

但是更糟糕的事還在後頭,咱們中文但是字符集裏面的大頭。

1.「 茴字有四種寫法」——上大人孔乙己
2.聽說有些新近整理的漢語字典收錄的漢字數量已經高達10萬級別!

若是仍是定長的方案,眼瞅着就要奔着四字節而去了,容量與效率的矛盾在這時候開始激化。

容量與效率的矛盾
  • 所謂容量,這裏指用幾個字節表示一個字符,顯然用的字節越多,編碼空間越大,能表示更多不一樣的字符,也即容量越大。
  • 所謂效率,當表示一個字符用字節越多,所佔用的存儲空間也就越大,換句話說,存儲(乃至檢索)的效率下降了。

image.png

若是說效率是,那麼容量就是
(_我沒還沒忘記自小學語文老師就開始教導的,寫做文要遙相呼應_)

莫爾斯電碼圖

看如圖所示你就會發現,字母e只用了一個點(dot)來編碼

image.png

其它字母可能以爲不公平,爲啥咱們就要錄入那麼多個點和劃(dash)才行呢?這裏面實際上是有統計規律支撐的。e出現的機率是最大的

z你能想到什麼?
zoo大概不少人能想到,厲害一點可能還能想到zebra(斑馬),Zuckerberg(扎克伯格),別翻字典!你還能想到更多不?
e你能想到什麼?
含有的 e的單詞則多了去了。zebra中不就有個e嗎,Zuckerberg中還兩個e呢

因此咱們但願頻率越高的詞,編碼越短,這樣最終才能最大化壓縮存儲文本數據的空間

爲何要使用哈夫曼編碼?使用別的編碼行不行?

上面提到常見的信息處理方式:定長編碼與變長編碼

假設當前有一句話:i like like like java do you like a java(共40個字符,包括空格)

那麼按照定長編碼處理,那麼會變成什麼呢?

image.png

咱們發現最後存在計算機中(二進制)長度是:三百五十九

那麼按照變長編碼處理,那麼會變成什麼呢?

image.png

假設咱們將這串10010110100...發給別人,可是沒有說明解碼的方式,這時就會出現解釋時存在產生歧義

好比說可能翻譯成:"a a a aa a ",因此咱們須要說明字符編碼不能是其餘字符編碼的前綴,即不能匹配重複的編碼

那麼按照哈夫曼處理,那麼會變成什麼呢?

image.png
image.png
image.png
image.png

相比定長編碼二進制的長度:三百五十九,哈夫曼二進制長度爲:一百三十三

那麼相比定長編碼二進制的長度優化了多少呢?:(359-133)/359=62.9%

咱們再將哈夫曼二進制轉碼對應的byte字符

image.png

那麼相比原字符長度優化了多少呢?:(40-17)/40 = 57.5%

一下將原字符40位變成17位, 這樣的狀況下,是否是咱們想要的

2、解析哈夫曼編碼執行思路與過程

上面咱們採用"i like like like java do you like a java",作例子分析,那麼咱們如今分析分析哈夫曼編碼思路

  • 將字符串裏的字符進行統計個數並轉爲Byte[]數組
  • 根據字符的出現次數構建一顆哈夫曼樹、次數爲權值
  • 根據哈夫曼樹進行自定義哈夫曼編碼實現
  • 將原字符的全部哈夫曼編碼構建成編碼串
  • 根據編碼串進行補碼->反碼->原碼構建新字符

3、統計字符出現次數並構建哈弗曼樹

圖解思路分析

  • 獲取傳輸的字符串:"i like like like java do you like a java"
  • 獲取各個字符對應的個數:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9
  • 按照上面的字符出現的次數構建成一顆哈夫曼樹,出現的個數做爲權值

構建哈夫曼樹的步驟思路

1.將數列進行從小到大排序
2.新數列的每一個數當作最簡單根節點的二叉樹
3.取出權值最小的兩顆二叉樹組成新的二叉樹,爲兩最小的樹之和
4.將新二叉樹以跟節點的權重值再次從小到大排序,不斷重複步驟

image.png

構建哈夫曼樹的代碼思路

  • Node{data:存放數據,weight:權重,left和right}
  • 獲得"i like like like java do you like a java"對應byte[]數組
  • 編寫方法,將準備構建赫夫曼樹的Node節點放到List:Node[data=97, weight= 5],Node[data=32,weight = .....
  • 經過List建立對應的哈夫曼樹

體現獲取各個字符對應的個數:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9

好比說字符:a 應該是Node[data='a', weight= 5],data爲何會是97而不是a?

由於底層:字母等於ASCII數字

構建哈夫曼樹的代碼實現

那麼第一步:建立節點Node{data:存放數據,weight:權重,left和right}

class Nodedata implements Comparable<Nodedata>{

    Byte data; //存放數據(字符)自己,好比'a' =>97 '' =>32
    int weight; //權值,表示字符出現的次數
    Nodedata left;
    Nodedata right;
    public Nodedata(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }
    @Override
    public int compareTo(Nodedata o) {
        return this.weight - o.weight;
    }
    @Override
    public String toString() {
        return "Nodedata[" +"data=" + data + ", weight=" + weight +']';
    }
}

第二步:獲得"i like like like java do you like a java"對應byte[]數組

public static void main(String[] args) {
     
     //獲得`"i like like like java do you like a java"`對應byte[]數組
     String content = "i like like like java do you like a java";
     byte[] contentBytes = content.getBytes();
     System.out.println(contentBytes.length);
}

第三步:編寫方法,將準備構建赫夫曼樹的Node節點放到List

private static List<Nodedata> getNodes(byte[] bytes){
    //1.建立一個ArrayList
    List<Nodedata> nodeslist = new ArrayList<Nodedata>();
    //2.遍歷bytes,統計每個byte出現的次數->map[key,value]
    Map<Byte,Integer> map = new HashMap<>();
    for(byte b :bytes){
        Integer items = map.get(b);
        if(items == null){
            map.put(b,1);
        }else{
            map.put(b, items + 1);
        }
    }
    //3.把每一個鍵值對轉爲Node 對象,並放入nodeslist集合裏
    for(Map.Entry<Byte,Integer> temp :map.entrySet()){
        nodeslist.add(new Nodedata(temp.getKey(),temp.getValue()));
    }
    return nodeslist;
}

第四步:經過List建立對應的哈夫曼樹

private static Nodedata createHaffman(List<Nodedata> nodedataList){
    while(nodedataList.size() >1){
        //排序從小到大
         Collections.sort(nodedataList);
         /**
         *  操做思路 
         *  1.取出兩個權值最小的節點二叉樹 
         *  2.根據兩個權值最小的二叉樹構建成一個新的二叉樹
         *  3.刪除原先兩個權值最小的節點二叉樹
         *  4.將新的二叉樹放入隊列,並構建新的隊列
         *  5.新的隊列進行從小到大排序
         */
         //取出第一個權值最小的二叉樹
         Nodedata leftNode = nodedataList.get(0);
         //取出第二個權值最小的二叉樹
         Nodedata rightNode = nodedataList.get(1);
         //根據兩個權值最小的二叉樹構建成一個新的二叉樹 同時構建鏈接
         Nodedata parentNode  = new Nodedata(null,leftNode.weight + rightNode.weight);
         parentNode.left = leftNode;
         parentNode.right = rightNode;
         //刪除原先兩個權值最小的節點二叉樹
         nodedataList.remove(leftNode);
         nodedataList.remove(rightNode);
         //將新的二叉樹放入隊列,並構建新的隊列
         nodedataList.add(parentNode);
     }
    return nodedataList.get(0);
}

測試檢查是否知足代碼思路

檢查是否Byte[]長度:40

public static void main(String[] args) {
     
     //獲得`"i like like like java do you like a java"`對應byte[]數組
     String content = "i like like like java do you like a java";
     byte[] contentBytes = content.getBytes();
     System.out.println("byte[]數組長度爲:"+contentBytes.length);
}

運行結果以下:
byte[]數組長度爲:40

檢查字符個數:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9

public static void main(String[] args) {
     
     //將準備構建哈弗曼樹的Node節點放到List:Node[data=97, weight= 5],Node[data=32,weight = .....
    List<Nodedata> datalist = getNodes(contentBytes);
    for(Nodedata data : datalist){
        System.out.println(data);
    }
}
運行結果以下:
Nodedata[data=32, weight=9]
Nodedata[data=97, weight=5]
Nodedata[data=100, weight=1]
Nodedata[data=101, weight=4]
Nodedata[data=117, weight=1]
Nodedata[data=118, weight=2]
Nodedata[data=105, weight=5]
Nodedata[data=121, weight=1]
Nodedata[data=106, weight=2]
Nodedata[data=107, weight=4]
Nodedata[data=108, weight=4]
Nodedata[data=111, weight=2]

image.png

Nodedata 節點添加前序遍歷,添加哈弗曼樹方法

class Nodedata implements Comparable<Nodedata>{

     //省略實體類代碼
     //前序遍歷方法
     public void preOrder(){
        System.out.println(this);
        if(this.left != null){
           this.left.preOrder();
        }
        if(this.right != null){
           this.right.preOrder();
        }
    }
}
private static void preOrder(Nodedata root){
    if(root != null){
        root.preOrder();
    }else{
        System.out.println("哈弗曼樹爲空!");
    }
}

檢查前序遍歷哈弗曼樹,查看是否圖一致:40->17->8->4->4->2->2->9...

image.png

public static void main(String[] args) {
     
     //獲取哈弗曼樹的根節點
     Nodedata root = createHaffman(datalist);
     //遍歷哈弗曼樹
     preOrder(root);
}
運行結果以下:
Nodedata[data=null, weight=40]
Nodedata[data=null, weight=17]
Nodedata[data=null, weight=8]
Nodedata[data=108, weight=4]
Nodedata[data=null, weight=4]
Nodedata[data=106, weight=2]
Nodedata[data=111, weight=2]
Nodedata[data=32, weight=9]
Nodedata[data=null, weight=23]
Nodedata[data=null, weight=10]
Nodedata[data=97, weight=5]
Nodedata[data=105, weight=5]
Nodedata[data=null, weight=13]
Nodedata[data=null, weight=5]
Nodedata[data=null, weight=2]
Nodedata[data=100, weight=1]
Nodedata[data=117, weight=1]
Nodedata[data=null, weight=3]
Nodedata[data=121, weight=1]
Nodedata[data=118, weight=2]
Nodedata[data=null, weight=8]
Nodedata[data=101, weight=4]
Nodedata[data=107, weight=4]

那麼咱們前面分析了如何將字符轉爲哈弗曼樹的思路與代碼

下面看看如何將根據哈夫曼樹進行構成自定義哈夫曼編碼

4、根據哈夫曼樹自定義哈夫曼編碼

  • 根據哈夫曼樹咱們來自定義編碼規則:向左爲0、向右爲1
  • 這樣根據上面如圖所示,定義的字符編碼以下:[o: 0011 u: 11001 d: 11000 y: 11010 i: 101 a: 100 k: 1111 e: 1110 j: 0010 V: 11011 I: 000 (空格):01]

image.png

有沒有發現哈弗曼樹的節點編碼它是前綴編碼:不是是其餘字符編碼的前綴,即匹配不到重複的編碼

根據哈夫曼樹自定義哈夫曼編碼代碼思路分析

  • 使用StringBuilder記錄所通過的路徑:'a'的路徑是:1->1->0
  • 使用Key:Value 鍵值的方式存放對應的字符:路徑;好比:(a:1000)、(j:0010)、(o:0011)
  • 字符特色:data !=null (屬於葉子節點)
  • 假設查詢字符v的路徑,發現左遞歸不符合後還須要進行右遞歸判斷

根據哈夫曼樹自定義哈夫曼編碼代碼實現

//將哈夫曼編碼以Key:Vale形式存放
//好比說32->01 97->100 100->11000 等等[形式]
static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
//2.在生成赫夫曼編碼表示,須要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
static StringBuilder stringBuilder = new StringBuilder();

/**
 * 1.功能:將傳入的node結點的全部葉子結點的赫夫曼編碼獲得,並放入到huffmanCodes集合 * @param node 傳入節點
 * @param code 路徑:左節點是0 右節點是1
 * @param stringBuilder 用於拼接路徑
 */
 private static void getCodes(Nodedata node,String code,StringBuilder stringBuilder){
    StringBuilder str = new StringBuilder(stringBuilder);
    //將code加入到str 總
    str.append(code);
    if(node!=null){
        //字符特色:`data !=null `(屬於葉子節點)
        if(node.data ==null){//(屬於非葉子節點)
         //向左遞歸
         getCodes(node.left,"0",str);
         //向右遞歸
         getCodes(node.right,"1",str);
    }else{
         //`使用Key:Value 鍵值`的方式存放對應的`字符:路徑;`好比:`(a:110)、(j:0000)、(o:1000)`
         huffmanCodes.put(node.data,str.toString());
     }
  }    
}

咱們優化一下代碼,實現傳入根節點就能夠返回哈夫曼編碼

private static Map<Byte,String> getCodes(Nodedata node){
     //根節點爲空則不作處理
     if(node == null){
        return null;
     }
     //向左遞歸
     getCodes(node.left,"0",stringBuilder);
     //向右遞歸
     getCodes(node.right,"1",stringBuilder);
     return huffmanCodes;
}

咱們進行運行測試看看,是否欲思路一致生成編碼

public static void main(String[] args) {

    //將哈夫曼樹生成哈夫曼編碼
    //getCodes(root,"",stringBuilder);
    
    //執行優化代碼
    getCodes(root);
}

運行結果以下:
生成的哈夫曼編碼表:{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}

注意:不一樣的排序方式會生成不一樣的哈弗曼樹,所形成的哈夫曼編碼也不同,但WPL是一致

5、將原字符的全部哈夫曼編碼構建成編碼串

咱們將"i like like like java do you like a java"字符串,即生成對應的哈夫曼編碼數據(以下)

image.png

將原字符串轉爲編碼串思路分析

  • 將字符串"i like like like java do you like a java"轉爲對應的Byte[]數組
  • 使用StringBuilder追加存儲byte字符的對應字符路徑

將原字符串轉爲編碼串思路分析代碼實現

private static void 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.printf("stringBuilder=" + stringBuilder.toString());
     System.out.print("\n stringBuilder的長度=" + stringBuilder.length());
}

代碼測試

public static void main(String[] args) {

    //獲得`"i like like like java do you like a java"`對應byte[]數組
    String content = "i like like like java do you like a java";
    byte[] contentBytes = content.getBytes();
    System.out.println("byte[]數組長度爲:"+contentBytes.length);
    //將準備構建哈弗曼樹的Node節點放到List:Node[data=97, weight= 5],Node[data=32,weight = .....
    List<Nodedata> datalist = getNodes(contentBytes);
    //獲取哈弗曼樹的根節點
    Nodedata root = createHaffman(datalist);
    //將哈夫曼樹生成哈夫曼編碼Key:Value
    getCodes(root);
    //將Byte[]數組轉爲自定義的哈夫曼編碼字符路徑
    zip(contentBytes,huffmanCodes);
}

運行結果以下:
byte[]數組長度爲:40
stringBuilder=1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
stringBuilder的長度=133

咱們將"i like like like java do you like a java"

轉爲編碼串相比以前定長編碼優化了多少呢?:(359-133)/359=62.9%

image.png

6、將編碼串進行補碼->反碼->原碼構建新字符

那麼如何將長度一百三十三的二進制串轉爲新的字符呢?

在講編碼串進行補碼->反碼->原碼構建新字符前,先解釋瞭解瞭解不一樣的進制

相同的數字有不一樣的樣子

在計算機的世界中就分兩種人:認識二進制和不認識二進制

上面咱們介紹到咱們看到的漂亮的圖像、好聽的聲音、震撼的視頻等等這些精彩內容。可是對於計算機說,它的世界裏只有二進制的 0 和 1

image.png

  • 全部數字在計算機底層都以二進制形式存在
  • 對於整數有不一樣表達方式:二進制、十進制、八進制、十六進制

image.png
image.png
image.png

二進制與十進制的說明

咱們使用Byte字符的二進制作演示,首先咱們直觀一點舉例正數說明

image.png
image.png
image.png
image.png
image.png

以此類推....那麼十進制轉二進制該怎麼作呢?來看看規律

image.png
image.png
image.png

有沒有發現?從右開始往左是2^0(二的零次方)、2^1(二的一次方)、2^2(二的平方)、2^3(二的三次方)

image.png

根據前面知識點,你知道:01101110的十進制是多少麼?

image.png

那麼咱們說Byte表示 -128 ~127 ,那麼0和1 是怎麼表示正數和負數的呢?

image.png

咱們如今知道二進制的最高位:0 正數 1-負數 ,那麼對於整數還要提提三個碼:原碼、反碼、補碼

image.png

那麼咱們思考一下:在計算機中: -14 是什麼樣的呢?

接下來看看負數的解析圖:十進制:1四、二進制:00001110

image.png

那麼咱們如今反推一下:10111011 轉爲十進制是多少呢?

  • 咱們剛剛提到計算機底層都以補碼方式存儲
  • 咱們剛剛觀察知道最高位: 0 - 正數、1-負數
  • 而負數則是補碼的形式,那麼思路反推:補碼->反碼->原碼

image.png

接下來我相信你們都瞭解了什麼是原碼、反碼、補碼

上面咱們將字符串"i like like like java do you like a java" 轉爲了自定義的哈弗曼編碼串
image.png

接下來咱們建立Byte[] huffmanCodeBytes,處理哈夫曼編碼串,以八位爲一組對應一個Byte 放入huffmanCodeBytes中

image.png
image.png

轉爲新字符代碼的思路分析

  • 每八位對應一個Byte,因此133 % 8,使用循環每次 i + 8
  • 由於每次 i + 8 因此須要使用變量index 記錄對應的第幾位Byte
  • 使用(byte)Integer.parInt(string,radix)轉碼

轉爲新字符代碼的代碼實現

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));
     }
     //2.1010100010111111...思路:補碼->反碼->原碼->轉成Byte
     int len;
     if(stringBuilder.length() %8 == 0){
         len = stringBuilder.length() /8;
     }else{
         len = stringBuilder.length() /8 + 1;
     }
     //3.建立存儲壓縮後的byte數組
     byte[] huffmanCodeBytes = new byte[len];
     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;
}
public static void main(String[] args) {
    //獲得`"i like like like java do you like a java"`對應byte[]數組
    String content = "i like like like java do you like a java";
    byte[] contentBytes = content.getBytes();
    System.out.println("byte[]數組長度爲:"+contentBytes.length);
    
    //將準備構建哈弗曼樹的Node節點放到List:Node[data=97, weight= 5],Node[data=32,weight = .....
    List<Nodedata> datalist = getNodes(contentBytes);
    
    //獲取哈弗曼樹的根節點
    Nodedata root = createHaffman(datalist);
    
    //將哈夫曼樹生成哈夫曼編碼
    getCodes(root);
    
    byte[] huffmanCodeBy =zip(contentBytes,huffmanCodes);
    System.out.println(Arrays.toString(huffmanCodeBy));
}

運行結果以下:
byte[]數組長度爲:40
[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

image.png

那麼相比原字符長度優化了多少呢?:(40-17)/40 = 57.5%

一下將原字符40位變成17位

7、封裝直接返回新符號Byte[]方法

根據前面的分析,咱們調用的方法比較多也比較臃腫,每次測試須要

  • 將字符串轉爲Byte數組,並統計個數
  • 根據字符出現次數做爲權重,構建哈弗曼樹
  • 根據哈弗曼樹生成自定義哈弗曼編碼
  • 將原字符的全部哈夫曼編碼構建成編碼串
  • 根據編碼串進行補碼->反碼->原碼構建新字符

如今呢,咱們將封裝這些方法方便咱們調用,咱們的目的是獲取到新字符組

//使用一個方法將須要的方法瘋轉起來,方便調用
private static  byte[] haffmanZip(byte[] bytes){
    //將準備構建哈弗曼樹的Node節點放到List:Node[data=97, weight= 5],Node[data=32,weight = .....
     List<Nodedata> datalist = getNodes(bytes);
     //根據字符出現次數做爲權重,構建哈弗曼樹
     //獲取哈弗曼樹的根節點 
     Nodedata root = createHaffman(datalist);
     //根據哈夫曼樹進行自定義哈夫曼編碼實現
     Map<Byte,String> huffmanCodes = getCodes(root);
     //將原字符的全部哈夫曼編碼構建成編碼串
     //根據編碼串進行補碼->反碼->原碼構建新字符
     byte[] huffmanCodeBy =zip(bytes,huffmanCodes);
     return huffmanCodeBy;
}
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("byte[]數組長度爲:"+contentBytes.length);
    byte[] huffmanCodeBy =haffmanZip(contentBytes);
    System.out.println(Arrays.toString(huffmanCodeBy));
    System.out.println("壓縮後的byte[]數組長度爲:"+huffmanCodeBy.length);
}
運行結果以下:
byte[]數組長度爲:40
[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
壓縮後的byte[]數組長度爲:17

8、將新字符解碼轉爲原字符串

咱們已經將"i like like like java do you like a java"包括空格在內的四十個字符

壓縮成[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]只有十七位的新字符。

那麼假如咱們將這串新字符發給對方

對方如何解碼成"i like like like java do you like a java"呢?

image.png

[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]是怎麼來的?

  • 將長度爲133的編碼串 % 8 再進行補碼->反碼->原碼

那麼咱們如今獲取到新字符後須要使用逆向思惟獲取原補碼

咱們可使用Integer.toBinaryString()方法將byte轉成二進制

/**
 *將一個byte轉成-個二進制的字符串 
 * @param b
 * @return
 */
private static String byteToBitString(byte b) {
    //使用變量保存b
     int temp = b;//將b轉成int
     String str = Integer.toBinaryString(temp);
     return str;
}

咱們使用 -1 進行測試這個方法看看

//測試一把byteToBitString方法
System.out.println(byteToBitString((byte)-1));

運行結果以下:
str=11111111111111111111111111111111

很遺憾,咱們發現返回的是int 的32位補碼。注意是:補碼

按理說咱們應該是從133 % 8獲取的新字符,如今應該也是轉爲8位的補碼,而不是32位

/**
 *將一個byte轉成-個二進制的字符串 * @param b
 * @return
 */
private static String byteToBitString(byte b) {
    //使用變量保存b
 int temp = b;//將b轉成int
 String str = Integer.toBinaryString(temp);
 return str.substring(str.length() -8);
}

那麼如今就能夠了嗎?其實並否則還有一個補位的問題!好比說我如今輸入 1

image.png

當是正數的時候,當前stu =1 則須要補位、不然是沒法完成截取操做的

/**
 *將一個byte轉成-個二進制的字符串 * @param b
 * @return
 */
private static String byteToBitString(byte b) {
     //使用變量保存b
     int temp = b;//將b轉成int
     //若是是正數咱們還存在補高位
     temp |= 256;
     String str = Integer.toBinaryString(temp);
     return str.substring(str.length() -8);
}

運行結果以下:
00000001

???

可能會有小夥伴會問到 temp |=256 是什麼意思???爲何要|=256

image.png

這就涉及到知識點二進制的運算符了,兩個二進制對應位爲0時該位爲0,不然爲1

image.png

這樣咱們輸入十進制:1 的時候,才能進行截取長度,不然不知足

/**
 *將一個byte轉成-個二進制的字符串
 * @param flag 標誌是否須要補高位、若是是最後一個字節則無需補高位
 * @param b 傳入的byte
 * @return 返回b對應的二進制串(按補碼返回)
 */
 private static String byteToBitString(boolean flag,byte b) {
     //使用變量保存b
     int temp = b;//將b轉成int
     if(flag){
        //若是是正數咱們還存在補高位
        temp |= 256;
     }
     String str = Integer.toBinaryString(temp);
     if(flag){
       return str.substring(str.length() -8);
     }else{
       return str;
     }
}

根據八位爲一組的思路,就會發現最後一個byte字節就無需補高位

將新字符解碼轉爲原字符串思路分析

  • 找到記錄原字符byte轉爲自定義哈夫曼編碼Map
  • 使用StringBuilder記錄新字符組的補碼
  • 根據自定義哈夫曼編碼Map反獲取(編碼:字符)組成新map
  • 根據StringBuilder匹配新map獲取對應字符

第一步:使用StringBuilder記錄新字符組的補碼

/**
 *  @param huffmanCodes 哈夫曼編碼表map
 * @param huffmanBytes 哈夫曼獲得的字節數組
 * @return就是原來的字符串對應的數組
 */
private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){
    //1.使用StringBuilder記錄新字符組的補碼
    StringBuilder stringBuilder =new StringBuilder();
    //循環記錄
    for (int i=0;i<huffmanBytes.length;i++){
         byte b = huffmanBytes[i];
         //最後一個字符無需補位
         boolean flag = (i == huffmanBytes.length -1?true:false);
         stringBuilder.append(byteToBitString(!flag,b));
     }
     System.out.println("新字符組的補碼串:"+stringBuilder.toString());
     System.out.println("新字符組的補碼串長度:"+stringBuilder.length());
     return null;
 }
public static void main(String[] args) {

    /獲得`"i like like like java do you like a java"`對應byte[]數組
    String content = "i like like like java do you like a java";
    byte[] contentBytes = content.getBytes();
    System.out.println("原byte[]數組長度爲:"+contentBytes.length);
    byte[] huffmanCodeBy =haffmanZip(contentBytes);
    System.out.println("n開始進行哈夫曼編碼壓縮==============================n");
    System.out.println("壓縮後的新字符數組:"+Arrays.toString(huffmanCodeBy));
    System.out.println("壓縮後的byte[]數組長度爲:"+huffmanCodeBy.length);
    decode(huffmanCodes,huffmanCodeBy);
}

運行結果以下:
原byte[]數組長度爲:40
原字符數組通過自定義編碼後的編碼串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
原字符數組通過自定義編碼後的編碼串長度:133

開始進行哈夫曼編碼壓縮==============================

壓縮後的新字符數組:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
壓縮後的byte[]數組長度爲:17
新字符組的補碼串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
新字符組的補碼串長度:133

第二步:找到記錄原字符byte轉爲自定義哈夫曼編碼Map、根據自定義哈夫曼編碼Map反獲取(編碼:字符)組成新map

/**
 * * @param huffmanCodes 哈夫曼編碼表map
 * @param huffmanBytes 哈夫曼獲得的字節數組
 * @return就是原來的字符串對應的數組
 */
private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){
     //1.使用StringBuilder記錄新字符組的補碼
     StringBuilder stringBuilder =new StringBuilder();
     //循環記錄
     for (int i=0;i<huffmanBytes.length;i++){
        byte b = huffmanBytes[i];
        //最後一個字符無需補位
        boolean flag = (i == huffmanBytes.length -1?true:false);
        stringBuilder.append(byteToBitString(!flag,b));
     }
     //找到記錄原字符byte轉爲自定義哈夫曼編碼Map、根據自定義哈夫曼編碼Map反獲取原字符
     Map<String,Byte> map =new HashMap<String, Byte>();
     for(Map.Entry<Byte,String> item:huffmanCodes.entrySet()){
          map.put(item.getValue(),item.getKey());
     }
     System.out.println("根據自定義哈夫曼編碼Map反獲取(編碼:字符)組成新map = "+map);
     return null;
 }
 
運行結果以下:

根據自定義哈夫曼編碼Map反獲取(編碼:字符)組成新map = {000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}

第三步:根據StringBuilder匹配新map獲取對應字符

image.png
image.png

/**
 * * @param huffmanCodes 哈夫曼編碼表map
 * @param huffmanBytes 哈夫曼獲得的字節數組
 * @return就是原來的字符串對應的數組
 */
private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){
    //1.使用StringBuilder記錄新字符組的補碼
     StringBuilder stringBuilder =new StringBuilder();
     //循環記錄
     for (int i=0;i<huffmanBytes.length;i++){
        byte b = huffmanBytes[i];
        //最後一個字符無需補位
        boolean flag = (i == huffmanBytes.length -1?true:false);
        stringBuilder.append(byteToBitString(!flag,b));
     }
        //找到記錄原字符byte轉爲自定義哈夫曼編碼Map、根據自定義哈夫曼編碼Map反獲取原字符
     Map<String,Byte> map =new HashMap<String, Byte>();
     for(Map.Entry<Byte,String> item:huffmanCodes.entrySet()){
         map.put(item.getValue(),item.getKey());
     }
        //根據StringBuilder匹配新map獲取對應字符
     List<Byte> list =new ArrayList<>();
     for (int j=0;j<stringBuilder.length();){    
        int count = 1;
        boolean flag = true;
        Byte b = null;
        while(flag){
          String key =stringBuilder.substring(j,j+count);
          b = map.get(key);
          if(b == null){
               count++;
          }else{
               flag = false;
          }
        }
        list.add(b);
        j +=count;
     }
      //當for循環結束後,list就存放了字符串"i like like like java do you like a java"的全部字符
     //把集合裏的數據放入byte[]中返回 byte[] b = new byte[list.size()];
     for (int k =0;k<b.length; k++){
            b[k]=list.get(k);
     }
     return b;
}
public static void main(String[] args) {

    /獲得`"i like like like java do you like a java"`對應byte[]數組
    String content = "i like like like java do you like a java";
    byte[] contentBytes = content.getBytes();
    System.out.println("原byte[]數組長度爲:"+contentBytes.length);
    byte[] huffmanCodeBy =haffmanZip(contentBytes);
    System.out.println("n開始進行哈夫曼編碼壓縮==============================n");
    System.out.println("壓縮後的新字符數組:"+Arrays.toString(huffmanCodeBy));
    System.out.println("壓縮後的byte[]數組長度爲:"+huffmanCodeBy.length);
    byte[] source = decode(huffmanCodes,huffmanCodeBy);
    System.out.println("新字符數組通過根據新map獲取對應字符:"+new String(source));
}

運行結果以下:

根據自定義哈夫曼編碼Map反獲取(編碼:字符)組成新map = {000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}
新字符數組通過根據新map獲取對應字符:i like like like java do you like a java

9、最佳實踐文件壓縮與解壓

咱們將這個圖片文件,進行壓縮實踐看看

image.png

思路:讀取文件-->獲得哈夫曼編碼表-->完成壓縮

/**
 *@paramsrcFile 但願壓縮文件的全路徑
 *@param dstFile 壓綜後將文件放到哪一個目錄
*/
public static void zipFile(String srcFile, String dstFile) {
     //建立輸出流
     OutputStream os = null;
     //對象輸出流
     ObjectOutputStream oos;
     //建立文件的輸入流
     FileInputStream is = null;
     try {
            //建立文件的輸入流
         is = new FileInputStream(srcFile);
         //建立一個和源文 件大小同樣的byte[ ]
         byte[] b = new byte[is.available()];
         //讀取文件
         is.read(b);
         //直接對文件壓縮
         byte[] bytes = haffmanZip(b);
         //建立文件的輸出流,存放壓縮文件
         os = new FileOutputStream(dstFile);
         //建立一個和文件流關聯的ObjectOutputStream;
         oos = new ObjectOutputStream(os);
         //以對象流的方寫入哈夫曼編碼,爲了方便恢復
         oos.writeObject(huffmanCodes);
     } catch (Exception e) {
            System.out.println(e.getMessage());
     } finally {
         try {
            is.close();
            oos.close();
            os.close();
         }catch (Exception e) {
            System.out.println(e.getMessage());
         }
    }
}
public static void main(String[] args) {
    //測試壓編文件
    String srcFile = "d://src. bmp";
    String dstFile = "d://dst.zip";
    zipFile(srcFile, dstFile);
    System. out . println("壓編文件ok~~");
}

image.png

會出現沒法打開並解壓的方式,爲何呢?

由於咱們自定義屬於本身的的壓縮方式,因此解壓也要用咱們本身的方式去解壓

源文件大小:598kb
壓縮後的文件大小:76kb

那麼接下來咱們編寫本身的解壓方式把

思路:讀取壓縮文件(數據和哈弗曼編碼表)-->完成解壓

//編寫一個方法,完成對壓縮文件的解壓
/**
 * @param zipFile 準備解壓的文件
 * @param dstFile 將文件解壓到哪一個路徑
*/
public static void unZipFile(String zipFile, String dstFile) {
     //定義文件輸入流
     InputStream is = null;
     //對象輸出流
     ObjectInputStream ois = null;
     //建立輸出流
     OutputStream os = null;
     try {
         //建立文件的輸入流
         is = new FileInputStream(zipFile);
         //建立一個和文件流關聯的ObjectOutputStream;
         ois = new ObjectInputStream(is);
         //讀取byte數組 huffmanbytes
         byte[] huffmanBytes = (byte[])ois.readObject();
         //獲取哈夫曼編碼表
         Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();
         //解碼
         byte[] bytes =decode(huffmanCodes,huffmanBytes);
         //將恢復的原byte數組寫入文件
         os = new FileOutputStream(dstFile);
         os.write(bytes);
     } catch (Exception e) {
            System.out.println(e.getMessage());
     } finally {
         try {
            os.close();
            ois.close();
            is.close();
         }catch (Exception e) {
            System.out.println(e.getMessage());
         }
    }
}
public static void main(String[] args) {
    
    //測試解壓文件
    String zipFile = "d://dst. zip";
    String dstFile = "d://src2. bmp";
    unZipFile(zipFile, dstFile) ;
}

image.png

相關文章
相關標籤/搜索