哈夫曼編碼——java實現

最近多媒體佈置了一個哈夫曼樹的壓縮算反的題目。然後我覺得我裏面用了不少算法。努力把它的時間複雜度變成了線性。
首先怎麼生成一棵哈夫曼樹(人工運算)我們是知道的吧。
計算機上。
我的做法分成了3步,準確說是4步。去生成一棵哈夫曼樹,其複雜度控制在O(n)
然後我用哈夫曼樹去進行編碼其複雜度我也把它降成了O(n)這兩部都是線性就能解決問題。
最後一個解碼。沒辦法只能變成O(KN),K是哈夫曼編碼時的字典大小。字典是變得。我沒辦法降低算法複雜度了。

給一個String串。我們第一步肯定是吧字符數量從小到大排序。爲了求出存在的比例嘛~

先介紹下節點

class Node
{
    int number;//存放多少個數。就是指裏面value有多少次出現在string裏
    char value;//存放string裏每個出現的char。唯一的
    Node(){}
    Node(char temp,int number)
    {
        value=temp;
        this.number=number;
    }
    //這裏是後面用於生產樹時在用的。
    String code="";//存放哈西曼編碼
    Node left=null;
    Node right=null;
    Node(Node a,Node b)//吧兩個出現頻率小的節點結合。
    {
        number=a.number+b.number;
        left=a;
        right=b;
        value='#';//中間節點
    }
}

此處我用map去數數。是爲了降低時間複雜度。
如果直接開一個結點然後把結點放到列表裏去,下次查找時就得從列隊的第一個結點查到最後一個。
這樣時間複雜度就是n^2了,而用map可以減低時間複雜度。因爲它是HashMap時間複雜度是1。總複雜度就變成了n
這一步時間複雜度是O(N)

private static List<Node> count(String src)
    {
        /*
         *此處我用map去數數。是爲了降低時間複雜度。
         *如果直接開一個結點然後把結點放到列表裏去,下次查找時就得從列隊的第一個結點查到最後一個。
         *這樣時間複雜度就是n^2了,而用map可以減低時間複雜度。因爲它是HashMap時間複雜度是1
         */
        Map<Character,Integer> map=new HashMap<Character,Integer>();
        char[] charArray=src.toCharArray();
        for(Character c:charArray)
        {
            if(!map.containsKey(c))
            {
                map.put(c,0);
            }
            map.put(c, map.get(c)+1);
        }

        //這是我們再把map裏的字符和數量導入到結點裏存儲。這樣就可以把這個函數的算法複雜度降低了
        List<Node> list=new ArrayList<Node>();
        for(Map.Entry<Character, Integer> entry:map.entrySet())
        {
            Node temp=new Node(entry.getKey(),entry.getValue());
            list.add(temp);
        }
        Collections.sort(list,(Node a,Node b)->(a.number<b.number?b.number:a.number));
        Collections.reverse(list);
        return list;
    }

然後是生成一棵哈夫曼樹(並編碼。這裏其實可以生成兩步的,我合併了下)
規則就是我們熟知的。左邊填1,右邊填0
先選出數量少的去生成一棵樹,然後最後把多的給嫁接上
這一步時間複雜度是O(N)

private static Node buildTree(List<Node> list)
    {
        while(!(list.size()==1))
        {
            Node left=list.get(0);//先選出裏面數量最少的當作左邊節點
            left.code=left.code+"1";
            list.remove(0);//拿出來後記得刪除它。便於下次再拿出一個頭

            Node right=list.get(0);//由於前面的第一少的已經拿掉了。那麼這個就變成第一少的了
            right.code=right.code+"0";
            list.remove(0);

            Node root=new Node(left,right);//然後生成一個新節點。把它放入數組中

            int index=0;//把索引定位。找到它在第幾個數前面,爲了插入回原來的列表。使這個列表的順序還是有序的。這裏其實是一遍插入排序
            while(list.size()-1>index&&root.number>(list.get(index)).number)
            {
                index++;
            }

            list.add(index,root);//插入列表中
        }

/* 樹已經建好了。但是還沒編碼。
         * 我之前對樹做了點手腳
         * 讓它每個節點記下自己當前處在左邊還是右邊。
         * 就像我們人工做時,左右寫1和0一樣。
         * 然後我們只要通過一次廣度搜索並且把節點從上往下的加就好了
         * 廣度收索爲了減少回溯等時間。我們採取隊列去做
         */
        Queue<Node> queue=new LinkedList<Node>();
        queue.add(list.get(0));
        while(!queue.isEmpty())
        {
            Node temp=queue.poll();
            if(temp.left!=null)
            {
                temp.left.code=temp.code+temp.left.code;
                queue.add(temp.left);
            }
            if(temp.right!=null)
            {
                temp.right.code=temp.code+temp.right.code;
                queue.add(temp.right);
            }
        }
        return list.get(0);//傳出來應該就一個根節點了。
    }

到上面爲止一棵哈夫曼樹就建好了。然後我們要把它變成字典。爲了方便查找嘛
採用廣度收索的方式去查找
這一步時間複雜度是O(N)

//之前已經生成了一棵哈夫曼樹,現在把這課哈夫曼樹變成個字典。便於我們查找
    private static Map<Character,String> dictionary(Node root)
    {
        //把節點存放到map裏方便查找(預處理)
        //這裏通過廣度搜索的方式存放節點
        Map<Character,String> map=new HashMap<Character,String>();
        Queue<Node> queue=new LinkedList<Node>();
        queue.add(root);
        while(!queue.isEmpty())
        {
            Node temp=queue.poll();
            if(temp.left!=null)
                queue.add(temp.left);
            if(temp.right!=null)
                queue.add(temp.right);
            if(temp.value=='#')
                continue;
            map.put(temp.value, temp.code);
        }
        return map;
    }

之後還得有轉碼字符串吧。吧字符串通過哈希錶轉碼

private static String createString(Map<Character,String> dictionary,String src)
    {

        StringBuilder answer=new StringBuilder();
        char[] array=src.toCharArray();

        //利用字典去查找檢索
        for(int i=0;i<array.length;i++)
        {
            answer.append(dictionary.get(array[i]));
        }
        return answer.toString();
    }

最後是解碼
/* 這裏沒辦法減小算法複雜度了好像。就行查字典幾個必須步驟好久就是這樣
* 需要全部遍歷一下數組,同時每次找都得遍歷下字典。
* 爲了儘可能的減少不必要的查找,我們用字典去找字符串
* 看看傳出來第一個找到的是不是第0位來加快速度
*/
這一步時間複雜度是O(KN) K是字典大小。是個不定的東西

private static String readString(Map<Character,String> dictionary,String src)
    {
        StringBuilder temp=new StringBuilder(src);//臨時存放數據的
        StringBuilder result=new StringBuilder();
        while(temp.length()>0)
        {
            for(Map.Entry<Character, String> entry: dictionary.entrySet())
            {
                if(temp.indexOf(entry.getValue())==0)//是不是第一位
                {
                    result.append(entry.getKey());
                    temp.delete(0, entry.getValue().length());
                    break;
                }
            }
        }
        return result.toString();
    }

結束。
下面是全部代碼

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Scanner;

//用於哈夫曼樹
class Node
{
    int number;//存放多少個數。就是指裏面value有多少次出現在string裏
    char value;
    Node(){}
    Node(char temp,int number)
    {
        value=temp;
        this.number=number;
    }

    String code="";//存放哈西曼數
    Node left=null;
    Node right=null;
    Node(Node a,Node b)
    {
        number=a.number+b.number;
        left=a;
        right=b;
        value='#';//中間節點
    }
}

public class Huffman 
{
    public static void main(String[] argc)
    {
        Scanner cin=new Scanner(System.in);
        String code=cin.next().trim();
        huffmancoding(code);
    }


    //哈夫曼樹部分
    //第一步計算所有的字符數量並按小到大排序
    private static List<Node> count(String src)
    {
        /*
         *此處我用map去數數。是爲了降低時間複雜度。
         *如果直接開一個結點然後把結點放到列表裏去,下次查找時就得從列隊的第一個結點查到最後一個。
         *這樣時間複雜度就是n^2了,而用map可以減低時間複雜度。因爲它是HashMap時間複雜度是1
         */
        Map<Character,Integer> map=new HashMap<Character,Integer>();
        char[] charArray=src.toCharArray();
        for(Character c:charArray)
        {
            if(!map.containsKey(c))
            {
                map.put(c,0);
            }
            map.put(c, map.get(c)+1);
        }

        //這是我們再把map裏的字符和數量導入到結點裏存儲。這樣就可以把這個函數的算法複雜度降低了
        List<Node> list=new ArrayList<Node>();
        for(Map.Entry<Character, Integer> entry:map.entrySet())
        {
            Node temp=new Node(entry.getKey(),entry.getValue());
            list.add(temp);
        }
        Collections.sort(list,(Node a,Node b)->(a.number<b.number?b.number:a.number));
        Collections.reverse(list);
        return list;
    }

    /*生成一棵哈夫曼樹,並編碼
     * 規則就是我們熟知的。左邊填1,右邊填0
     * 先選出數量少的去生成一棵樹,然後最後把多的給嫁接上
     */
    private static Node buildTree(List<Node> list)
    {
        while(!(list.size()==1))
        {
            Node left=list.get(0);//先選出裏面數量最少的當作左邊節點
            left.code=left.code+"1";
            list.remove(0);//拿出來後記得刪除它。便於下次再拿出一個頭

            Node right=list.get(0);//由於前面的第一少的已經拿掉了。那麼這個就變成第一少的了
            right.code=right.code+"0";
            list.remove(0);

            Node root=new Node(left,right);//然後生成一個新節點。把它放入數組中

            int index=0;//把索引定位。找到它在第幾個數前面,爲了插入回原來的列表。使這個列表的順序還是有序的
            while(list.size()-1>index&&root.number>(list.get(index)).number)
            {
                index++;
            }

            list.add(index,root);//插入列表中
        }

        /* 樹已經建好了。但是還沒編碼。
         * 我之前對樹做了點手腳
         * 讓它每個節點記下自己當前處在左邊還是右邊。
         * 就像我們人工做時,左右寫1和0一樣。
         * 然後我們只要通過一次廣度搜索並且把節點從上往下的加就好了
         * 廣度收索爲了減少回溯等時間。我們採取隊列去做
         */
        Queue<Node> queue=new LinkedList<Node>();
        queue.add(list.get(0));
        while(!queue.isEmpty())
        {
            Node temp=queue.poll();
            if(temp.left!=null)
            {
                temp.left.code=temp.code+temp.left.code;
                queue.add(temp.left);
            }
            if(temp.right!=null)
            {
                temp.right.code=temp.code+temp.right.code;
                queue.add(temp.right);
            }
        }
        return list.get(0);//傳出來應該就一個根節點了。
    }

    //之前已經生成了一棵哈夫曼樹,現在把這課哈夫曼樹變成個字典。便於我們查找
    private static Map<Character,String> dictionary(Node root)
    {
        //把節點存放到map裏方便查找(預處理)
        //這裏通過廣度搜索的方式存放節點
        Map<Character,String> map=new HashMap<Character,String>();
        Queue<Node> queue=new LinkedList<Node>();
        queue.add(root);
        while(!queue.isEmpty())
        {
            Node temp=queue.poll();
            if(temp.left!=null)
                queue.add(temp.left);
            if(temp.right!=null)
                queue.add(temp.right);
            if(temp.value=='#')
                continue;
            map.put(temp.value, temp.code);
        }
        return map;
    }

    //Test函數
    private static void huffmancoding(String src)
    {
        //哈夫曼樹的字典
        Map<Character,String> temp=dictionary(buildTree(count(src)));
        System.out.println("哈夫曼編碼");
        System.out.println("原字符串:"+src);
        System.out.print("字典: ");
        int i=1;
        for(Map.Entry<Character, String> entry: temp.entrySet())
        {
            if(i++%4==0)
                System.out.println();
            System.out.print(entry.getKey()+"值爲"+entry.getValue()+" ");
        }
        System.out.println();
        String result=createString(temp, src);
        System.out.println("編碼後爲:"+result);
        System.out.println("解碼後爲:"+readString(temp,result));
    }

    //這一步就是利用這課哈夫曼樹對字符串進行編碼
    private static String createString(Map<Character,String> dictionary,String src)
    {

        StringBuilder answer=new StringBuilder();
        char[] array=src.toCharArray();

        //利用字典去查找檢索
        for(int i=0;i<array.length;i++)
        {
            answer.append(dictionary.get(array[i]));
        }
        return answer.toString();
    }

    //解碼哈西曼編碼
    /* 這裏沒辦法減小算法複雜度了好像。就行查字典幾個必須步驟好久就是這樣
     * 需要全部遍歷一下數組,同時每次找都得遍歷下字典。
     * 爲了儘可能的減少不必要的查找,我們用字典去找字符串
     * 看看傳出來第一個找到的是不是第0位來加快速度
     */
    private static String readString(Map<Character,String> dictionary,String src)
    {
        StringBuilder temp=new StringBuilder(src);//臨時存放數據的
        StringBuilder result=new StringBuilder();
        while(temp.length()>0)
        {
            for(Map.Entry<Character, String> entry: dictionary.entrySet())
            {
                if(temp.indexOf(entry.getValue())==0)//是不是第一位
                {
                    result.append(entry.getKey());
                    temp.delete(0, entry.getValue().length());
                    break;
                }
            }
        }
        return result.toString();
    }
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章