11.樹結構的應用

目錄

1.堆排序
2.哈夫曼樹
3.哈夫曼編碼
4.二叉排序樹
5.平衡二叉樹(AVL樹)

1.堆排序

堆的介紹:

堆是具有以下性質的完全二叉樹:
1.每個節點的值都大於或等於其左右孩子節點的值,稱爲大頂堆。注意 : 沒有要求節點的左孩子的值和右孩子的值的大小關係。
在這裏插入圖片描述
採用順序存儲後爲:
在這裏插入圖片描述
2.每個節點的值都小於或等於其左右孩子節點的值,稱爲小頂堆。
在這裏插入圖片描述

堆排序介紹:

堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均爲 O(nlogn),它也是不穩定排序。一般升序採用大頂堆,降序採用小頂堆。

堆排序基本思想:

以升序爲例:
1.將待排序序列構造成一個大頂堆;
2.此時,整個序列的最大值就是堆頂的根節點,順序存儲中數組下標爲0。
3.將其與末尾元素進行交換,此時末尾就爲最大值。
4.然後將剩餘 n-1 個元素重新構造成一個堆,這樣會得到 n 個元素的次小值。如此反覆執行,便能得到一個有序序列了。
 
這樣在不斷構建大頂堆的過程中,組成大頂堆的元素個數逐漸減少,最後就得到一個有序序列了。

堆排序步驟圖解說明:

以原始二叉樹順序存儲的數組 [4, 6, 8, 5, 9]爲例:
第一步:構造初始堆:將給定無序序列構造成一個大頂堆(一般升序採用大頂堆,降序採用小頂堆)。
在這裏插入圖片描述
從最後一個非葉子結點開始,即節點6,從右至左,從下至上進行調整:
在這裏插入圖片描述
節點9比6大,9和6兩個節點交換位置,構成以9爲根的大頂堆。以此類推,找到第二個非葉節點 4,4 和 9 交換:
在這裏插入圖片描述
這時,交換導致了子樹[4,5,6]結構混亂,繼續調整,[4,5,6]中 6 最大,交換 4 和 6。
在這裏插入圖片描述
此時,實現了將一個無序序列構造成了一個大頂堆。
 
實際上述的過程可以歸納爲從下往上找到最大值不斷往上移,在判斷非葉子節點是否需要交換的時候,其下面的子樹經過前面的調整一定是大頂堆。所以當非葉子節點和它的子節點交換後,導致子樹混亂時,只需要將這個較小數,不斷往下一層移動就可以了。因爲原子樹本身是大頂堆,只是變了一個數,只需要這樣簡單處理就行。
 
第二步:將堆頂元素與順序存儲數組中末尾的元素進行交換,使末尾元素最大,並暫時將該最大值看成剝離出二叉樹:
在這裏插入圖片描述
然後按照上述過程繼續調整爲大頂堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反覆進行交換、重建、交換:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
反覆循環上述過程,最終使得整個序列有序:
在這裏插入圖片描述

堆排序代碼實現:

package sort;

import java.util.Arrays;

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = {4, 6, 8, 5, 9};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int[] array){
        //初始i指向最底層的最右邊的非葉子節點(最後一個非葉子節點),將完全二叉樹構建成一個大頂堆
        for (int i = array.length/2-1; i >= 0; i--) {
            adjustToBigHeap(array,i,array.length);
        }
        //將堆頂節點交換到數組尾部,將去掉尾部的數組(二叉樹)再次調整爲大頂堆,循環進行該過程,直到只剩一個節點
        for (int j = array.length-1; j > 0; j--) {
            int temp = array[j];
            array[j] = array[0];
            array[0] = temp;
            //只變了堆頂一個元素,將該較小值不斷往下層移即可
            adjustToBigHeap(array, 0, j);
        }
    }

    /**
     * 將二叉樹調整爲大頂堆
     * @param array 待調整二叉樹的順序存儲數組
     * @param i 非葉子節點下標
     * @param length 截取的二叉樹的順序存儲數組的長度(堆頂元素交換到數組末尾後不再參與大頂堆的構建了)
     */
    public static void adjustToBigHeap(int[] array, int i, int length){
        //初始指向該非葉子節點的左子節點
        for (int k = i*2+1; k < length; k = k*2+1) {
            int nodeData = array[i];
            //以該非葉子節點爲樹根,將該樹調整爲大頂堆
            if (k+1<length && array[k]<array[k+1]){//右子節點存在,且左子節點的值小於右子節點,則k指向較大值
                k++;
            }
            if (array[k]>nodeData){//該非葉子節點的葉子節點值比它大,則交換
                array[i] = array[k];
                array[k] = nodeData;
                //該非葉子節點交換後,可能導致子樹混亂,不再爲大頂堆。所以需要將該較小值不斷往下移,直到滿足大頂堆條件。
                //所以循環進行,繼續訪問交換後的較小值的子節點,將子節點的值同這個較小值比較,比它大則較小值繼續下移,比它小則已經是大頂堆。
                i = k;
            }else {//已經滿足大頂堆條件則不需要交換,子樹也不會混亂,直接退出循環。
                break;
            }
        }
    }
}

2.哈夫曼樹

哈弗曼樹介紹:

1.給定n個權值作爲n個葉子節點,構造一棵二叉樹,若該樹的帶權路徑長度(WPL)達到最小,稱這樣的二叉樹爲最優二叉樹,也稱爲哈夫曼樹(Huffman Tree)或赫夫曼樹。
2.哈弗曼樹是帶權路徑長度最短的樹,權值較大的節點離根較近。

哈弗曼樹介紹中的概念說明:

1.路徑和路徑長度:在一棵樹中,從一個節點往下可以達到的孩子或孫子節點之間的通路,稱爲路徑。通路中分支的數目稱爲路徑長度。若規定樹根節點的層數爲1,則從根節點到第L層結點的路徑長度爲L-1。
2.節點的權及帶權路徑長度:若將二叉樹中節點賦給一個有着某種含義的數值,則這個數值稱爲該節點的權。節點的帶權路徑長度爲:從根結點到該節點之間的路徑長度與該節點的權的乘積。
3.樹的帶權路徑長度:樹的帶權路徑長度規定爲所有葉子節點的帶權路徑長度之和,記爲WPL(weighted path length) ,權值越大的節點離根節點越近時的二叉樹WPL才最小,纔是最優二叉樹。
4.WPL最小的就是哈夫曼樹。
在這裏插入圖片描述
如上圖中中間那顆二叉樹纔是哈弗曼樹。

構建哈弗曼樹步驟:

1.將權值序列進行從小到大排序,每個權值對應一個葉子節點
2.取出權值序列中最小的兩個權值作爲葉子節點組成一顆子樹,它們的父節點爲兩者的權值和;
3.將上述構成的子樹的根節點的權值放入權值序列,並重新排序;
4.重複上述3個步驟,直到權值序列中只有一個權值(整棵樹根節點),就得到了一顆哈弗曼樹。

圖解構建哈弗曼樹過程:

例如將權值序列:{13, 7, 8, 3, 29, 6, 1}構建成一顆哈弗曼樹。首先將該序列排序爲升序:
在這裏插入圖片描述
取出最小的兩個權值1和3,作爲葉子節點構成一顆子樹,它們的父節點權值爲二者權值的和4。並將該子樹根節點的權值4插入權值序列尾部,重新排序:
在這裏插入圖片描述
重複上述步驟,取出4和6,構成子樹,將根節點10插入權值序列尾部,重新排序:
在這裏插入圖片描述
重複上述步驟,取出7和8,構成子樹,將根節點15插入權值序列尾部,重新排序:
在這裏插入圖片描述
重複上述步驟,取出10和13,構成子樹,將根節點23插入權值序列尾部,重新排序:
在這裏插入圖片描述
重複上述步驟,取出15和23,構成子樹,將根節點38插入權值序列尾部,重新排序:
在這裏插入圖片描述
最後,權值序列只剩最後一個值,即爲整棵樹根節點,哈弗曼樹則構造完成。
在這裏插入圖片描述

代碼實現:

package tree;

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

/**
 * 節點類Node,實現Comparable接口,便於排序時直接使用Collections集合工具
 */
class Node implements Comparable<Node>{
    public int data;//節點權值
    public Node left, right;//左右孩子節點指針

    public Node(int data) {
        this.data = data;
    }

    @Override
    public int compareTo(Node o) {
        return this.data - o.data;
    }
}

/**
 * 哈夫曼樹
 */
public class HuffmanTree {

    public static void main(String[] args) {
        int array[] = { 13, 7, 8, 3, 29, 6, 1 };
        HuffmanTree huffmanTree = new HuffmanTree();
        huffmanTree.generateHuffmanTree(array);
        System.out.println("先序遍歷構建的哈夫曼樹:");
        huffmanTree.preOrderTraverse();
    }

    private Node root;

    /**
     * 構建生成哈夫曼樹
     * @param array 權值數組
     * @return
     */
    public void generateHuffmanTree(int[] array){
        if (array==null || array.length<2){
            throw new RuntimeException("不滿足構成哈夫曼樹條件");
        }
        //將權值放到集合中方便取出插入操作和排序
        List<Node> nodes = new ArrayList<>();
        for (int i = 0; i < array.length; i++) {
            nodes.add(new Node(array[i]));
        }
        //開始構建哈夫曼樹,直到結合中只有一個權值
        while (nodes.size()>1){
            //升序排序
            Collections.sort(nodes);
            //取出兩個最小的權值,構成子樹
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            Node parentNode = new Node(leftNode.data + rightNode.data);
            parentNode.left = leftNode;
            parentNode.right = rightNode;
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //將parentNode插入到權值序列尾部
            nodes.add(parentNode);
        }
        //保存根節點
        this.root = nodes.get(0);
    }

    /**
     * 重載preOrderTraverse
     */
    public void preOrderTraverse(){
        this.preOrderTraverse(this.root);
    }

    /**
     * 先序遍歷哈夫曼樹
     * @param node
     */
    public void preOrderTraverse(Node node){
        if (node==null){
            return;
        }
        System.out.print(node.data+" ");
        preOrderTraverse(node.left);
        preOrderTraverse(node.right);
    }

}

3.哈夫曼編碼

通信領域中信息的處理方式:

1.定長編碼:以下圖中字符串i like like like java do you like a java中40個字符(包括空格)爲例,在計算機中按照二進制來傳遞信息,總的長度是359(包括空格)。
在這裏插入圖片描述
2.變長編碼:實際上定長編碼數據太長太大了,在通信領域中通常會採用邊長編碼。變長編碼首先統計字符串中每個字符出現的次數,如上例字符串中:d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 ' ':9。然後則可以將這12個字符串從0開始編碼,一般原則是出現次數越多的字符編碼越小,比如空格出現了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
 
但是這裏我們設計的變長編碼方式存在一個問題:a的編碼1是其他字符含1編碼的前綴,這就會導致當我們解碼10010110100這串編碼時,遇到編碼1時不能確定是直接翻譯成a字符呢還是這個1是其他字符編碼的一部分。這就造成匹配的多義性。
 
所以字符的編碼都不能是其他字符編碼的前綴,符合此要求的編碼叫做前綴編碼, 即不能匹配到重複的編碼。哈夫曼編碼就很好的解決了上述問題。

赫夫曼編碼介紹:

1.哈夫曼編碼(Huffman Coding),是一種編碼方式, 屬於一種程序算法;
2.哈夫曼編碼是哈夫曼樹在電訊通信中的經典的應用之一。
3.哈夫曼編碼廣泛地用於數據文件壓縮。其壓縮率通常在20%~90%之間。
4.赫夫曼碼是可變字長編碼(VLC)的一種。由Huffman於1952年提出的一種編碼方法,被稱之爲最佳編碼。

哈夫曼編碼步驟:

1.統計字符串中每個字符出現的次數;
2.將各個字符出現的次數作爲權值,構建一顆哈弗曼樹;
3.根據赫夫曼樹,給各個字符,規定編碼 (前綴編碼), 向左的路徑爲 0 ,向右的路徑爲 1。如哈夫曼樹介紹中的示例哈夫曼樹編碼後如下圖所示:
在這裏插入圖片描述
這樣示例字符串i like like like java do you like a java的字符哈夫曼編碼結果爲:o=1000 u=10010 d= 100110 y=100111 i=101 a=110 k=1110 e=1111 j=0000 v=0001 l=001 ' '=01。進而整個字符串的哈夫曼編碼爲:10101001101111011110100110111101111010011011110111101000011000011100110011110000110 01111000100100100110111101111011100100001100001110,通過赫夫曼編碼處理後長度爲133,遠比定長編碼的359短,起到了壓縮的作用。並且可以校驗任意一個字符的編碼不是其他字符編碼的前綴,這樣在解碼時就可以不斷的匹配,將編碼翻譯成字符。所以赫夫曼編碼也是無損處理方案。

解壓和壓縮代碼實現:

package tree;

import java.util.*;

class HmBiNode implements Comparable<HmBiNode>{
    public Byte data;//存放數據(字符)的ASCII碼值
    public int weight;//權值,即字符出現的次數
    public HmBiNode left, right;

    public HmBiNode(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public int compareTo(HmBiNode o) {
        return this.weight-o.weight;
    }

    @Override
    public String toString() {
        if (this.data==null){
            return "HmBiNode{" +
                    "data=null" +
                    ", weight=" + weight +
                    '}';
        }
        return "HmBiNode{" +
                "data=" + (char)data.byteValue() +
                ", weight=" + weight +
                '}';
    }

    /**
     * 先序遍歷該節點作爲根節點的二叉樹
     */
    public void preOrderTraverse(){
        System.out.print(this+" ");
        if (this.left!=null){
            this.left.preOrderTraverse();
        }
        if (this.right!=null){
            this.right.preOrderTraverse();
        }
    }
}

public class HuffmanCode {

    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        System.out.println("待壓縮的字符串:"+content);
        byte[] bytes = content.getBytes();
        System.out.println("哈夫曼編碼壓縮前原始數據的數據長度及數據:");
        System.out.println("長度:"+bytes.length+",數據:"+Arrays.toString(bytes));
        HuffmanCode huffmanCode = new HuffmanCode();
        byte[] zipBytes = huffmanCode.zip(bytes);
        System.out.println("哈夫曼編碼壓縮後的數據長度及數據:");
        System.out.println("長度:"+zipBytes.length+",數據:"+Arrays.toString(zipBytes));
        System.out.println("壓縮率:"+(float)(bytes.length-zipBytes.length)/bytes.length *100 +"%" );
        System.out.println("開始解壓.....");
        byte[] unzipBytes = huffmanCode.unzip(zipBytes);
        System.out.println("解壓後的原始數據數據長度及數據:");
        System.out.println("長度:"+unzipBytes.length+",數據:"+Arrays.toString(unzipBytes));
        System.out.println("解壓後的字符串:"+ new String(unzipBytes));
    }

    //哈夫曼編碼表
    private Map<Byte, String> huffmanCodesForm = new HashMap<>();

    /**
     * 對數據進行壓縮
     * @param bytes
     */
    public byte[] zip(byte[] bytes){
        //解析需要編碼的數據,得到哈夫曼樹節點
        List<HmBiNode> nodes = this.getNodes(bytes);
        System.out.println("統計每個字符出現的次數,得到哈夫曼樹的葉子節點集合爲:");
        System.out.println(nodes);
        //構建生成哈夫曼樹,返回根節點
        HmBiNode root = this.generateHuffmanTree(nodes);
        System.out.println("先序遍歷構建的哈夫曼樹:");
        root.preOrderTraverse();
        //生成哈夫曼編碼表:初始根節點沒有父節點,編碼不能是0或1,給定空字符串。
        this.generateCodesForm(root);
        System.out.println("\n生成的哈夫曼編碼表爲:");
        System.out.println(this.huffmanCodesForm);
        //實現對原始整個數據的編碼
        byte[] code = this.code(bytes);
        return code;
    }

    /**
     * 對數據進行解壓
     * @param bytes
     * @return
     */
    public byte[] unzip(byte[] bytes){
        //解壓得到原始數據的哈夫曼編碼數據
        String huffmanCodeData = this.bytesToBitString(bytes);
        System.out.println("將壓縮數據解壓爲原數據的哈夫曼編碼數據:");
        System.out.println(huffmanCodeData);
        //對照哈夫曼編碼表,將哈夫曼編碼數據解碼爲原來的數據
        byte[] decode = this.decode(huffmanCodeData);
        return decode;
    }

    /**
     *  統計每個字符出現的次數,得到哈夫曼樹的葉子節點
     * @param bytes 數據字節數組
     * @return
     */
    private List<HmBiNode> getNodes(byte[] bytes){
        //統計每個字符出現的次數
        Map<Byte, Integer> counts = new HashMap<>();//存放字符出現的次數,鍵:字符ASCII 值:次數
        for (byte b : bytes){
            Integer count = counts.get(b);
            if (count==null){//沒有記錄過,次數初始爲1
                counts.put(b,1);
            }else {//再次出現,次數加1
                counts.put(b, count+1);
            }
        }
        //生成節點
        List<HmBiNode> nodes = new ArrayList<>();
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()){
            nodes.add(new HmBiNode(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    /**
     * 構建生成哈夫曼樹
     * @param nodes 節點權值集合
     * @return 返回哈夫曼樹根節點
     */
    private HmBiNode generateHuffmanTree(List<HmBiNode> nodes){
        if (nodes==null || nodes.size()<2){
            throw new RuntimeException("不滿足構成哈夫曼樹條件");
        }
        //構建哈夫曼樹,直到結合中只有一個權值
        while (nodes.size()>1){
            //升序排序
            Collections.sort(nodes);
            //取出兩個最小的權值,構成子樹
            HmBiNode leftNode = nodes.get(0);
            HmBiNode rightNode = nodes.get(1);
            HmBiNode parentNode = new HmBiNode(null,leftNode.weight + rightNode.weight);
            parentNode.left = leftNode;
            parentNode.right = rightNode;
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //將parentNode插入到權值序列尾部
            nodes.add(parentNode);
        }
        //保存根節點
        return nodes.get(0);
    }

    /**
     * 通過生成的哈夫曼樹,遞歸遍歷找到每個葉子節點;
     * 遍歷過程中實現對每個數據的編碼(左邊0,右邊1),得到編碼表;
     * @param node
     * @param code
     * @param codeStr
     */
    private void generateCodesForm(HmBiNode node, String code, StringBuilder codeStr){
        //拷貝之前的編碼(0、1),並添加上當前的編碼。拷貝的原因是避免在同一個引用上操作,不斷疊加;
        // 每個字符的編碼是不同的,應該是單獨的字符串對象
        StringBuilder stringBuilder = new StringBuilder(codeStr);
        stringBuilder.append(code);
        //如果當前節點爲葉子節點,則結束對該葉子節點的編碼,並將該字符的編碼結果存入編碼表
        if (node.data!=null){
            huffmanCodesForm.put(node.data, stringBuilder.toString());
            return;
        }
        //非葉子節點不斷遞歸遍歷、編碼
        generateCodesForm(node.left,"0",stringBuilder);
        generateCodesForm(node.right,"1",stringBuilder);
    }

    /**
     * 重載generateCodesForm
     * @param root
     */
    private void generateCodesForm(HmBiNode root){
        this.generateCodesForm(root,"",new StringBuilder());
    }

    /**
     * 依託於解析後生成的哈夫曼編碼表,實現對原始數據轉爲編碼數據
     * 並將編碼後的數據不斷取8位存儲到字節數組byte[]中,實現壓縮
     * @param bytes
     * @return
     */
    private byte[] code(byte[] bytes){
        StringBuilder stringBuilder = new StringBuilder();
        //將原字符轉換爲對應編碼
        for (byte b : bytes){
            stringBuilder.append(this.huffmanCodesForm.get(b));
        }
        System.out.println("對原數據哈夫曼編碼後:");
        System.out.println(stringBuilder.toString());
        //將編碼後的字符串轉爲byte(8位)數組
        int length = (stringBuilder.length()+7)/8;//每個字節byte 8位,計算可以分爲多少個字節來存儲
        int index = 0;
        byte[] huffmanCodeBytes = new byte[length];
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            //截取符合byte長度(8位)的二進制字符串
            String substring = null;
            if (i+8>stringBuilder.length()){//字符串不夠8位
                substring = stringBuilder.substring(i);//從i直接截取到末尾
            }else {
                substring = stringBuilder.substring(i, i + 8);
            }
            //將二進制字符串轉爲整型,再轉爲byte,byte存儲的是該整數的二進制原碼的補碼
            // 如:10101000(2)=168(10)。10101000(原碼)的補碼爲:11011000(2)=-88
            huffmanCodeBytes[index++] = (byte) Integer.parseInt(substring,2);
        }
        return huffmanCodeBytes;
    }

    /**
     * 將壓縮的字節數組解壓縮爲原數據的哈夫曼編碼數據
     * @param bytes
     * @return  返回哈夫曼編碼數據
     */
    private String bytesToBitString(byte[] bytes){
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            if (bytes[i]>=0){//byte爲正數時需要補爲8位(高位的0不顯示)補碼
                if (i==bytes.length-1){//如果是最後一個則編碼時可能就不夠8位,不夠8位沒有符號位,則一定爲正數,就不作補位
                    String str = Integer.toBinaryString(bytes[i]);
                    stringBuilder.append(str);
                    continue;
                }
                //將原碼與100000000(2)=256(10)作按位或運算,這樣高位有1,不會作爲正數省略,然後再取8位
                String str = Integer.toBinaryString(bytes[i]|256);
                str = str.substring(str.length()-8);
                stringBuilder.append(str);
            }else {//byte爲負數時,由於是轉爲的int(4個字節32位),所以只取前8位補碼
                String str = Integer.toBinaryString(bytes[i]);
                str = str.substring(str.length()-8);
                stringBuilder.append(str);
            }
        }
        return stringBuilder.toString();
    }

    /**
     * 通過解壓縮後得到的原數據的哈弗曼編碼數據,解碼得到原數據
     * @param huffmanCodeData
     * @return
     */
    private byte[] decode(String huffmanCodeData){
        //反轉原哈夫曼編碼表,之前是原數據對應編碼,現在反轉爲編碼對應原數據
        Map<String, Byte> form = new HashMap<>();
        for (Map.Entry<Byte, String> entry : this.huffmanCodesForm.entrySet()){
            form.put(entry.getValue(), entry.getKey());
        }
        List<Byte> data = new ArrayList<>();
        //掃描原數據的哈夫曼編碼數據,與反轉編碼表比對,進行解碼
        for (int i = 0; i < huffmanCodeData.length();) {
            String code = "";
            //沒有與反轉編碼表比對成功,則增加掃描長度
            while (form.get(code)==null){
                code += huffmanCodeData.substring(i,i+1);
                i++;
            }
            //比對成功後當前code對應反轉編碼表中的數據則爲原始數據
            data.add(form.get(code));
            //比對成功後i定位到比對完的位置,i++後則開始對下一個編碼的掃描
        }
        //將集合轉換爲byte數組
        byte[] bytes = new byte[data.size()];
        for (int i = 0; i < data.size(); i++) {
            bytes[i] = data.get(i);
        }
        return bytes;
    }
}

運行結果:
在這裏插入圖片描述
刪掉上面示例代碼中的控制檯輸出,實現真正對文件進行壓縮和解壓縮:

//在上述代碼中添加zipFile和unzipFile兩個方法,並修改main方法
public static void main(String[] args) {
        HuffmanCode huffmanCode = new HuffmanCode();
        //壓縮文件到源文件目錄
        boolean zipResult = huffmanCode.zipFile("F:/myReport.txt");
        System.out.println(zipResult?"壓縮文件成功!":"壓縮文件失敗!");
        //解壓縮文件到源文件目錄
        boolean unzipResult = huffmanCode.unzipFile("F:/myReport.myzip");
        System.out.println(unzipResult?"解壓縮文件成功!":"解壓縮文件失敗!");
    }

    //哈夫曼編碼表
    private Map<Byte, String> huffmanCodesForm = new HashMap<>();

    /**
     * 壓縮文件到源文件目錄
     * @param srcFilePath 待壓縮文件的全路徑
     */
    public boolean zipFile(String srcFilePath){
        FileInputStream in = null;
        ObjectOutputStream objOut = null;
        boolean result = true;
        try {
            in = new FileInputStream(srcFilePath);
            //創建和源文件一樣的字節數組
            byte[] bytes = new byte[in.available()];
            //讀取源文件
            in.read(bytes);
            //壓縮源文件字節數組
            byte[] zipBytes = this.zip(bytes);
            //將壓縮後的源文件字節數組寫入壓縮文件
            String suffix = srcFilePath.substring(srcFilePath.indexOf("."));//源文件後綴
            String aimFilePath = srcFilePath.replace(suffix,".myzip");
            objOut = new ObjectOutputStream(new FileOutputStream(aimFilePath));
            objOut.writeObject(zipBytes);
            //寫入哈夫曼編碼表
            objOut.writeObject(this.huffmanCodesForm);
            //最後寫入源文件後綴
            objOut.writeObject(suffix);
        }catch (Exception e){
            result = false;
            throw e;
        }finally {//關閉字節流對象
            try {
                in.close();
                objOut.flush();
                objOut.close();
                return result;
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
    }

    /**
     * 解壓縮文件到源文件目錄
     * @param srcFilePath 待解壓縮文件的全路徑
     */
    public boolean unzipFile(String srcFilePath){
        ObjectInputStream objIn = null;
        FileOutputStream out = null;
        boolean result = true;
        try {
            objIn = new ObjectInputStream(new FileInputStream(srcFilePath));
            //讀取源文件原數據byte數組
            byte[] bytes = (byte[]) objIn.readObject();
            //繼續讀取哈夫曼編碼表
            this.huffmanCodesForm  = (Map<Byte, String>) objIn.readObject();
            //讀取源文件後綴
            String suffix = (String)objIn.readObject();
            String aimFilePath = srcFilePath.replace(srcFilePath.substring(srcFilePath.indexOf(".")),"-unzip"+suffix);
            out = new FileOutputStream(aimFilePath);
            //解壓縮字節數組
            byte[] unzipBytes = this.unzip(bytes);
            //將解壓縮後字節數組寫出
            out.write(unzipBytes);
        }catch (Exception e){
            System.out.println("解壓縮出錯:"+e.getMessage());
            result = false;
        }finally {//關閉字節流對象
            try {
                objIn.close();
                out.flush();
                out.close();
                return result;
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
    }

壓縮結果:
在這裏插入圖片描述
解壓結果:
在這裏插入圖片描述

4.二叉排序樹

二叉排序樹介紹:

二叉排序樹(Binary Sort Tree),又稱二叉查找樹(Binary Search Tree),亦稱二叉搜索樹。二叉排序樹是一棵空樹,或者是具有下列性質的二叉樹:
(1)若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
(2)若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
(3)左、右子樹也分別爲二叉排序樹;
(4)沒有鍵值相等的結點。
在這裏插入圖片描述

查找節點:

從根節點開始,若關鍵字值等於待查找值,查找成功;否則,小於關鍵字,向左遞歸查找;大於關鍵字,向右遞歸查找

插入節點:

二叉排序樹是一種動態樹表。其特點是:樹的結構通常不是一次生成的,而是在查找過程中,當樹中不存在關鍵字等於給定值的結點時再進行插入。新插入的結點一定是一個新添加的葉子結點,並且是查找不成功時查找路徑上訪問的最後一個結點的左孩子或右孩子結點。

刪除節點:

在二叉排序樹刪去一個結點,分三種情況討論:
1.刪除節點爲葉子結點,也就是無左右子樹,可以直接刪除;
在這裏插入圖片描述
2.刪除節點只有左子樹或右子樹,只需要將其父節點直接指向其左子樹或者右子樹;
在這裏插入圖片描述
3.刪除節點左右子樹均不爲空,不能簡單的刪除,但是可以根據二叉排序樹的規律將待刪除節點與其後繼節點交換(與其左子樹最大值交換或右子樹最小值交換)後轉換爲前兩種情況。
在這裏插入圖片描述
 
總結:第一種情況其實也可以看成只有左或右子樹的情況,只是子樹爲空,所以第一、二種情況可以用相同邏輯處理。而第三種情況,只需要多一步交換操作,然後又可以按照第一、二種情況來處理。

代碼實現:

package tree;

public class BinarySortTree {

    public static void main(String[] args) {
        int[] array = {7, 3, 10, 14, 5, 1, 9, 12, 11, 13, 4, 1};
        BinarySortTree binarySortTree = new BinarySortTree();
        for (int i = 0; i < array.length; i++) {
            System.out.print(binarySortTree.insert(array[i])?array[i]+"插入成功!":array[i]+"插入失敗!已經存在!");
        }
        System.out.println("\n中序遍歷結果:");
        binarySortTree.inOrderTraverse();
        System.out.print("\n刪除關鍵字10:");
        System.out.print(binarySortTree.delete(10)!=null?"刪除成功!":"刪除失敗!關鍵字不存在!");
        System.out.println("刪除關鍵字10後的中序遍歷結果:");
        binarySortTree.inOrderTraverse();
        System.out.print("\n刪除關鍵字1:");
        System.out.print(binarySortTree.delete(1)!=null?"刪除成功!":"刪除失敗!關鍵字不存在!");
        System.out.println("刪除關鍵字1後的中序遍歷結果:");
        binarySortTree.inOrderTraverse();
        System.out.print("\n刪除關鍵字5:");
        System.out.print(binarySortTree.delete(5)!=null?"刪除成功!":"刪除失敗!關鍵字不存在!");
        System.out.println("刪除關鍵字5後的中序遍歷結果:");
        binarySortTree.inOrderTraverse();
        System.out.print("\n再次刪除關鍵字5:");
        System.out.print(binarySortTree.delete(5)!=null?"刪除成功!":"刪除失敗!關鍵字不存在!");
        System.out.println("刪除關鍵字5後的中序遍歷結果:");
        binarySortTree.inOrderTraverse();
    }

    private Node root;

    //指向查找過程中的前驅節點,方便插入和刪除操作
    private Node preNode;

    /**
     * 二叉排序樹查找關鍵字
     * @param node
     * @param key
     * @return
     */
    public Node search(Node node, int key){
        if (node==null || node.data == key){//節點爲null或查找成功,則直接返回節點
            return node;
        }else if (key < node.data){//向左遞歸查找
            this.preNode = node;//保存前驅節點
            return search(node.left, key);
        }else {//向右遞歸查找
            this.preNode = node;//保存前驅節點
            return search(node.right, key);
        }
    }

    /**
     * 插入關鍵字
     * @param key
     * @return
     */
    public boolean insert(int key){
        Node searchNode = search(this.root, key);
        if (this.preNode==null){//前驅節點爲null,則該二叉排序樹爲空數,插入節點爲根節點
            this.root = new Node(key);
            return true;
        }else if (searchNode!=null){//查找成功,不需要做插入操作,插入失敗
            return false;
        }else { //根據關鍵字和前驅節點值的大小比較,將關鍵字插入到合適的子節點
            Node node = new Node(key);
            if (key < this.preNode.data){
                this.preNode.left = node;
            }
            else {
                this.preNode.right = node;
            }
            return true;
        }
    }

    /**
     * 刪除關鍵字
      * @param key
     * @return
     */
    public Node delete(int key){
        Node delNode = search(this.root, key);
        if (delNode == null){//待刪除關鍵字不存在,刪除失敗
            return null;
        }else{
            //第三種情況:同時存在左右子樹
            if (delNode.left!=null && delNode.right!=null){
                //查找左子樹的關鍵字最大的節點,或右子樹的關鍵字最小的節點。這裏查找右子樹
                this.preNode = delNode;
                Node minNode = delNode.right;
                while (minNode.left!=null){
                    this.preNode = minNode;
                    minNode = minNode.left;
                }
                //將待刪除節點與其右子樹最小關鍵字節點交換
                int tempData = delNode.data;
                delNode.data = minNode.data;
                minNode.data = tempData;
                //交換後,將待刪除節點的指針指向minNode
                delNode = minNode;
            }
            //經過前一步處理,下面只有前兩種情況,只有一個子樹或者沒有任何子樹,而沒有子樹其實也可以看成是隻有一個空子樹
            Node childNode = null;
            //獲取其子樹根節點
            if (delNode.left!=null){
                childNode = delNode.left;
            }else {
                childNode = delNode.right;
            }
            //待刪除節點前驅節點爲null,說明刪除結點爲根節點
            if (this.preNode == null){
                this.root = childNode;
            }else {
                //待刪除節點是其前驅節點的左子節點
                if (this.preNode.left == delNode){
                    this.preNode.left = childNode;
                }else {
                    this.preNode.right = childNode;
                }
            }
        }
        //返回刪除的節點
        return delNode;
    }

    /**
     * 重載inOrderTraverse
     */
    public void inOrderTraverse(){
        this.inOrderTraverse(this.root);
    }

    /**
     * 中序遍歷二叉排序樹
     * @param node
     */
    private void inOrderTraverse(Node node){
        if (node == null){
            return;
        }
        inOrderTraverse(node.left);
        System.out.print(node.data+" ");
        inOrderTraverse(node.right);
    }


}

運行結果:
在這裏插入圖片描述

5.平衡二叉樹(AVL樹)

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