貪心演算法-構造哈夫曼數及生成哈夫曼編碼,編程實現

哈夫曼樹

1.概念:

  • 給定n個權值最為n個葉子的節點,構建成一顆二叉樹。如果次樹的帶權路徑長度最小,則稱此二叉樹為最優二叉樹,也叫哈夫曼樹。
  • WLP帶權路徑長度
    • 公式:

    • Wk:第k個葉子的節點權值

    • Lk:根節點到第k個葉子的節點長度

例如下列二叉樹:

  • 給定4個葉子節點,權值分別為{2,3,5,8},可以構造出4中形狀不同的二叉樹,但它們的帶權路徑長度可能不同。

如上圖可看出:1、最後兩個樹的帶權路徑長度相同且也是最小的,所以可看作哈夫曼樹

2、權值最小的節點越靠下,越大越靠近根節點

2.構建哈夫曼樹

(1)、在{2,3,5,8}這幾個節點看作葉子節點(即後面沒有子節點)

(2)、在這幾個節點中選出權值最小和次小的兩個節點。構成一個新二叉樹(最小為左位元組的、次小為右子節點),新二叉樹的權值為這兩個權值之和.

(3)、刪除已經使用過的節點。將新創建的節點加入到還沒被使用過的節點集合中,找出最小和次小的節點權值。又構成一顆新的二叉樹。

(4)、重複(2)的操作

(5)、重複上面操作:刪除已使用的節點,將新的節點加入未使用節點的集合中,發現只有一個節點,其權值為18.則此哈夫曼樹創建完成,根節點權值為18.

程式碼如下:


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

/**
 * 構造哈夫曼樹
 */
public class test1 {
    public static void main(String[] args) {
        int[] arr={2,3,5,8};
        //調用自定義的哈夫曼樹方法,生成哈夫曼樹
        hafmantree(arr);
    }

    /**
     * ,構造哈夫曼數方法
     *
     * @param arry
     */
    public static void hafmantree(int[] arry) {
        //創建集合用於存放節點值
        List<Node> nlis = new ArrayList<Node>();
        //遍歷集合
        for (int value : arry) {
            //將每個數組元素看作Node節點對象,並存入list集合內
            nlis.add(new Node(value));
        }
       /*
       由於集合中存入的是一個個的Node對象,而Node對象已經被我們聲明成了按照從小到大的可排序對象。
       所以這裡我們為可以通過Collections工具類中的sort方法進行排序
        */
        //循環條件:只有一個節點,即最終根節點
        while (nlis.size() > 1) {
            Collections.sort(nlis);
            //查看集合中還未被使用的節點
            System.out.println(nlis);
            //到了這裡說明集合中的所有節點(權值)都是按照從小到大的順序
            //獲取最小的節點值(二叉樹左節點):子節點
            Node lileft = nlis.get(0);
            //獲取第2小的節點值(二叉樹右節點):子節點
            Node lireght = nlis.get(1);
       /*創建新的Node節點對象(二叉樹):
            此節點對象是最小的兩個節點之和所生成的最新的節點。三個節點可以看成一個二叉樹,即:
             根節點(insternode對象)、左子節點(lileft.value)、右子節點(lireght.value)
        */
            Node insternode = new Node(lileft.value + lireght.value);
            //此二叉樹的左節點
            insternode.left = lileft;
            //此二叉樹的右節點
            insternode.right = lireght;
            //刪除已經被處理過的節點
            nlis.remove(lileft);
            nlis.remove(lireght);
            //將最新的節點加入到list集合中
            nlis.add(insternode);
            //重新對最新的list數組進行排序
            Collections.sort(nlis);
        }
        //查看最終根節點
        System.out.println(nlis);
    }


}


/**
 * ,構造哈夫曼數節點類,
 * 此類也可以看成一個二叉樹:根節點(Node對象)、左節子點(left)、右位元組點(right)
 * 實現Comparable介面:說明此類是可通過Collections工具類排序的,
 */
class Node implements Comparable<Node> {
    int value;  //每個節點的值(權值)
    Node left;   //每個二叉樹的左指向節點
    Node right;   //每個二叉樹的右指向節點

    //構造方法,這裡只需要初始化value實例變數,用於對象的賦值,而 left 與 right 本身就是此對象,可直接用於value賦值
    public Node(int value) {
        this.value = value;
    }

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

    //支援從小到大排序
    @Override
    public int compareTo(Node o) {
        return this.value - o.value;
    }
}

結果:

3.構建哈夫曼編碼

  • 這裡是對一段字元串進行哈夫曼編碼,所以需要先處理字元串

  • 在哈夫曼樹的基礎上,規定了路徑編碼。

  • 遍歷已經創建好了的哈夫曼樹,從它的根節點遍歷次樹到葉子節點,左子路徑編碼:0 、右子路徑編碼:1


import java.util.*;

/**
 * 構造哈夫曼數+生成哈夫曼編碼,編程實現。
 */
public class test1 {
    public static void main(String[] args) {
        //需要轉換為哈夫曼編碼的字元串
        String valus="asdsgddbhj ,sjsh";
        //將字元串存以node對象存入list集合中
        List<Node> list = ListAndNode(valus);
        //生成哈夫曼樹
        Node node = HFMTree(list);
        //得到哈夫曼編碼
        HFMTable(node,"",andindex);
        System.out.println(yezijied);   //{32=1010, 97=1011, 98=1100, 115=01, 100=111, 103=1101, 104=001, 106=100, 44=000}

    }

/*
第四步:
創建哈夫曼編碼表:將葉子節點以0、1表示。0===》向左子節點走。1===》向右子節點走
    yezijied:存放葉子節點對應的哈夫曼編碼。此集合作業與全局
    andindex:拼接編碼。(拼接對應的0或1,形參最終的哈夫曼編碼)
 */

    static Map<Byte,String> yezijied=new HashMap<>();
    static  StringBuilder andindex=new StringBuilder();

    /**
     *
      * @param node:節點
     * @param index:路徑表示:左路徑為0.右路勁為1
     * @param sub:拼接路徑,使其成為對應葉子節點的哈夫曼碼
     */
public static void HFMTable(Node node,String index,StringBuilder sub){
    //
    StringBuilder gitindex=new StringBuilder(sub);
    //拼接路徑
    gitindex.append(index);
    //如果節點為空就不需要處理
    if (node!=null) {
        //如果當前節點不是葉子節點
        if (node.value == null) {
            //如果節點不為空就遞歸其左邊節點。並設置向左為0
            HFMTable(node.left, "0", gitindex);
            //如果節點不為空就遞歸其右邊節點。並設置向右為1
            HFMTable(node.right, "1", gitindex);
        } else {
            //為葉子節點就將其存入map集合中
            yezijied.put(node.value, gitindex.toString());
        }

    }
}

    /*
    第三步:
    @param nodes:已經存入list集合中的Node節點
    創建字元串的哈夫曼樹結構
     */
    public static Node HFMTree(List<Node> nodes){
        //循環條件:節點數必須大於1.如果等於1就是一個節點(根節點),沒有分支
        while (nodes.size()>1){
            //排序list集合,根據權值(節點個數)從小到大排序
            Collections.sort(nodes);

            /*
            創建一個二叉樹:
            feiyezijied:根節點
            nodeleft:左子節點
            noderight:右子節點
             */
            //得到權值最小的兩個節點.這兩個節點分別可看作左右兩個子節點
            Node nodeleft = nodes.get(0);
            Node noderight = nodes.get(1);
            //創建新的Node對象:這可以想像為兩個葉子節點生成的根節點,
            // 由於哈夫曼數的原理,需要編碼的值是葉子節點,而葉子節點上的父節點只是通過葉子節點虛擬創建的節點,
            // 是為了形成一整顆完整的樹。所以它是沒有字元串原始值,,其可用兩個位元組的權值之和標記
            Node feiyezijied=new Node(null,nodeleft.quanzhi+noderight.quanzhi);
            //Node對象的左位元組點
            feiyezijied.left=nodeleft;
            //Node對象的右位元組點
            feiyezijied.right=noderight;

            //刪除原集合中的以使用的節點對象.即上面已經每次獲得的集合中兩個最小的節點
            nodes.remove(nodeleft);
            nodes.remove(noderight);

            //將新創建的Node節點加入list集合中
            nodes.add(feiyezijied);
            //重新對list集合排序
            Collections.sort(nodes);
        }
        //返回最終根節點
        return nodes.get(0);
    }



    /*
    第二步:
    @param valus:傳入需要編碼的字元串,將其變成節點
    將需要編碼的字元串,每個原始值(ASCIIM碼)以節點(Node)對象形式傳入list集合中。
    而節點對象Node初始化了value與quanzhi,所以節點對象是包括這兩個值,所以將每個節點對象當作一個map.
    設k=value、v=quanzhi
     */
    public static List<Node> ListAndNode(String valus){
        //將字元對象存入byte數組。
        byte[] bytes = valus.getBytes();
        //創建List集合
        List<Node> nodes=new ArrayList<>();
        //創建Map集合
        Map<Byte,Integer> node=new HashMap<>();
        //遍歷bytes數組,得到每個字元串的原始值
        for (byte by:bytes){
            //先試著通過傳入的第一個k獲取v
            Integer index = node.get(by);
            //如果map集合中此原始值對應的個數還沒有
            if (index==null){
                node.put(by,1);
            }else {
                node.put(by,index+1);
            }
        }
        //遍歷map集合,並將每次遍歷的元素,以Node對象的形式存入list集合
        for (Map.Entry<Byte,Integer> n:node.entrySet()){
            nodes.add(new Node(n.getKey(),n.getValue()));
        }
        //最後返回此list集合
        return nodes;
    }

}

  /*
  第一步:
    節點類:其本身可可看作一個概念性的二叉樹
    Node對象本身可看作是一個二叉樹的根節點
    實現Comparable介面:泛型規定此介面作用與此Node節點,說明此類是可排序的,通過' Collections.sort()'
     */

class Node implements Comparable<Node>{
    Byte value;     //原始值:字元本身的ASCIIM碼。因為一段字元串中有許多相同的字元,但相同字元卻對應這一個ASCIIM碼
    int quanzhi;    //此字元value在一段字元串中出現的次數
    Node left;      //Node對象看作是二叉樹的根節點,那麼這就是此二叉樹的左子節點
    Node right;     //Node對象看作是二叉樹的根節點,那麼這就是此二叉樹的右邊子節點

    //構造器初始化 value 、quanzhi。
    public Node(Byte value, int quanzhi) {
        this.value = value;
        this.quanzhi = quanzhi;
    }

    //重寫toString:因為我們需要拿到這兩個值
    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                ", quanzhi=" + quanzhi +
                '}';
    }

    //實現Comparable介面中的方法:手動設置排序規則
    @Override
    public int compareTo(Node o) {
        //設置為通過權值從小到大排序
        return    this.quanzhi-o.quanzhi;
    }

    //前序遍歷
    public void qxbl(){
        //先輸出當前節點,也就是根節點
        System.out.println(this);
        //如果左子節點不是null節點,就遞歸遍歷輸出左子節點.null表示不是葉子節點
        if (this.left!=null){
            this.left.qxbl();
        }
        //同樣遞歸右子節點
        if (this.right!=null){
            this.right.qxbl();
        }
    }
}