資料 data 結構(structure)是一門 研究組織資料方式的學科,有了程式語言也就有了資料結構.學好資料結構才可以編寫出更加漂亮,更加有效率的程式碼。
我會用資料結構與演演算法【Java】這一系列的部落格記錄自己的學習過程,如有遺留和錯誤歡迎大家提出,我會第一時間改正!!!
注:資料結構與演演算法【Java】這一系列的部落格參考於B站尚矽谷的視訊,視訊原地址為【尚矽谷】資料結構與演演算法(Java資料結構與演演算法)
上一篇文章資料結構與演演算法【Java】07---樹結構基礎部分
1.堆排序是利用堆這種資料結構而設計的一種排序演演算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均為 O(nlogn)
,它是不穩定排序。
堆是具有以下性質的完全二元樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大根堆(或大頂堆), 注意 : 沒有
要求結點的左孩子的值和右孩子的值的大小關係。
每個結點的值都小於或等於其左右孩子結點的值,稱為小根堆(或小頂堆)
一般升序採用大根堆,降序採用小根堆
堆排序的基本思想是:
步驟圖解
要求:給你一個陣列 {4,6,8,5,9} , 要求使用堆排序法,將陣列升序排序。
假設給定無序序列結構如下
此時我們從最後一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點arr.length/2-1=5/2-1=1
,也就是下面的 6 結點),從左至右,從下至上進行調整。
3.找到第二個非葉節點 4,由於[4,9,8]中 9 元素最大,4 和 9 交換。
4.這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中 6 最大,交換 4 和 6。
此時,我們就將一個無序序列構造成了一個大頂堆.
1.將堆頂元素 9 和末尾元素 4 進行交換
2.重新調整結構,使其繼續滿足堆定義
3.再將堆頂元素 8 與末尾元素 5 進行交換,得到第二大元素 8
4.後續過程,繼續進行調整,交換,如此反覆進行,最終使得整個序列有序
再簡單總結下堆排序的基本思路:
1).將無序序列構建成一個堆,根據升序降序需求選擇大根堆或小根堆;
2).將堆頂元素與末尾元素交換,將最大元素"沉"到陣列末端;
3).重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序
動態演示
堆排序的理解還是比較困難的,尤其是程式碼實現過程,下面提供兩種程式碼實現,大家可以選擇適合自己的實現方法來理解堆排序
程式碼實現(一)
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
//升序--->大頂堆
long startTime=System.currentTimeMillis();
int arr[] = {5,3,7,1,4,6,2};
heapSort(arr);
long endTime=System.currentTimeMillis();
System.out.println("程式執行時間: "+(endTime-startTime)+"ms");
}
//編寫一個堆排序的方法
public static void heapSort(int arr[]) {
int temp = 0;
//完成我們最終程式碼
//將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆
for(int i = arr.length / 2 -1; i >=0; i--) {
adjustHeap(arr, i, arr.length);
}
/*
* 2).將堆頂元素與末尾元素交換,將最大元素"沉"到陣列末端;
3).重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
*/
for(int j = arr.length-1;j >0; j--) {
//交換
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
System.out.println("陣列=" + Arrays.toString(arr));
}
//將一個陣列(二元樹), 調整成一個大頂堆
/**
* 功能: 完成 將 以 i 對應的非葉子結點的樹調整成大頂堆
* 舉例 int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
* 如果我們再次呼叫 adjustHeap 傳入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5, 4}
* @param arr 待調整的陣列
* @param i 表示非葉子結點在陣列中索引
* @param length 表示對多少個元素繼續調整, length 是在逐漸的減少
*/
public static void adjustHeap(int arr[], int i, int length) {
int temp = arr[i];//先取出當前元素的值,儲存在臨時變數
//開始調整
//說明
//1. k = i * 2 + 1 k 是 i結點的左子結點
for(int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if(k+1 < length && arr[k] < arr[k+1]) { //說明左子結點的值小於右子結點的值
k++; // k 指向右子結點
}
if(arr[k] > temp) { //如果子結點大於父結點
arr[i] = arr[k]; //把較大的值賦給當前結點
i = k; //!!! i 指向 k,繼續迴圈比較
} else {
break;//!
}
}
//當for 迴圈結束後,我們已經將以i 為父結點的樹的最大值,放在了 最頂(區域性)
arr[i] = temp;//將temp值放到調整後的位置
}
}
結果:
程式碼實現(二)
//交換陣列中的元素
public static void swap(int[]num ,int i,int j) {
int temp=num[i];
num[i]=num[j];
num[j]=temp;
}
//將待排序的陣列構建成大根堆
public static void buildbigheap(int []num,int end) {
//從最後一個非葉子節點開始構建,依照從下往上,從右往左的順序
for(int i=end/2;i>=0;i--) {
adjustnode(i, end, num);
}
}
//調整該節點及其以下的所有節點
public static void adjustnode(int i,int end,int []num) {
int left=2*i+1;
int right=2*i+2;
int big=i;
//判斷小分支那個是大元素
if(left<end&&num[i]<num[left])
i=left;
if(right<end&&num[i]<num[right])
i=right;
if(i!=big) {
//交換順序之後需要繼續校驗
swap(num, i, big);
//重新校驗,防止出現交換之後根節點小於孩子節點的情況
adjustnode(i, end, num);
}
}
public static void main(String[] args) {
int []num ={5,3,7,1,4,6,2};
long startTime=System.currentTimeMillis();
//第一次構建大根堆
buildbigheap(num, num.length);
for(int j=num.length-1;j>0;j--) {
System.out.print("第"+(num.length-j)+"次排序前: ");
for(int k=0;k<num.length;k++) {
System.out.print(num[k]+" ");
}
//交換隊頭已經排序得到的最大元素與隊尾元素
swap(num, 0, j);
System.out.print("第"+(num.length-j)+"次排序後: ");
for(int k=0;k<num.length;k++) {
System.out.print(num[k]+" ");
}
System.out.println();
//交換結束之後,大根堆已經被破壞,需要開始重新構建大根堆
buildbigheap(num,j);
}
long endTime=System.currentTimeMillis();
System.out.println("程式執行時間: "+(endTime-startTime)+"ms");
}
結果:
1、給定 n 個權值作為 n 個葉子結點,構造一棵二元樹, 若該樹的帶權路徑長度(wpl) 達到最小,稱這樣的二元樹為
最優二元樹,也稱為哈夫曼樹(Huffman Tree), 還有的書翻譯為霍夫曼樹。
2、赫夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近
重要概念和舉例說明
WPL(weighted path length)
,權值越大的結點離根結點越近的二元樹才是最優二元樹。給出一個數列 {13, 7, 8, 3, 29, 6, 1}
,要求轉成一顆赫夫曼樹
構成赫夫曼樹的步驟:
圖解:
(1)選出最小的兩個陣列成二元樹
(2)接下來在4,6,7,8...
中選擇最小的兩個4,6
(注意這裡要加入第一步組成的節點4
,大的在右邊,小的在左邊)
(3)重複上述步驟
public class HuffmanTree {
public static void main(String[] args) {
int arr[] = { 13, 7, 8, 3, 29, 6, 1 };
Node root = createHuffmanTree(arr);
preOrder(root); //67,29,38,15,7,8,23,10,4,1,3,6,13
}
//編寫一個前序遍歷的方法
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、傳輸的 字串i like like like java do you like a 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、按照上面字元出現的次數構建一顆赫夫曼樹, 次數作為權值
構成赫夫曼樹的步驟:
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 like java do you like a java"
字串對應的編碼為 (注意這裡我們使用的無失真壓縮)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
通過赫夫曼編碼處理 長度為 133,且不會有多義性
6、長度為 : 133
說明:原來長度是359
, 壓縮了 (359-133) / 359 = 62.9%
此編碼滿足字首編碼, 即字元的編碼都不能是其他字元編碼的字首。不會造成匹配的多義性
赫夫曼編碼是無失真處理方案(可以完全恢復)
注:這個赫夫曼樹根據 排序方法不同,也可能不太一樣,這樣對應的 赫夫曼編碼也不完全一樣,但是 wpl 是
一樣的,都是最小的, 最後生成的赫夫曼編碼的長度是一樣,比如: 如果我們讓每次生成的新的二元樹總是排在權
值相同的二元樹的最後一個,則生成的二元樹如下圖,但是編碼長度是不會變的,還是133
將給出的一段文字,比如"i like like like java do you like a java"
, 根據前面的講的赫夫曼編碼原理,對其進行數
據 壓 縮 處 理 , 形 式 如
"1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100 110111101111011100100001100001110"
功能: 根據赫夫曼編碼壓縮資料的原理,需要建立 "i like like like java do you like a java"
對應的赫夫曼樹
思路:
(1) Node { data (存放資料), weight (權值), left 和 right }
(2) 得到 "i like like like java do you like a java"
對應的 byte[] 陣列
(3) 編寫一個方法,將準備構建赫夫曼樹的Node 節點放到 List , 形式 [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......], 體現 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
(4) 可以通過List 建立對應的赫夫曼樹
程式碼實現
import java.util.*;
public class HuffmanCode {
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);
System.out.println("前序遍歷:");
huffmanTreeRoot.preOrder();
}
//前序遍歷的方法
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else {
System.out.println("赫夫曼樹為空");
}
}
/**
*
* @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<>();
//遍歷bytes,儲存每一個byte出現的次數=》map[key,value]
HashMap<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 ascii碼
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();
}
}
}
結果:(建立出赫夫曼樹前序遍歷)
我們已經生成了 赫夫曼樹, 下面我們繼續完成任務
=01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
10101000101111111100100010111111110010001011111111001001010011011100011100000110111010001111001010 00101111111100110001001010011011100
1、生成赫夫曼樹對應的赫夫曼編碼
程式碼實現
//測試是否生成了對應的赫夫曼編碼
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
System.out.println("生成的對應的赫夫曼編碼="+ HuffmanCode.huffmanCodes);
//...
//生成赫夫曼樹對應的赫夫曼編碼
//思路:
//1. 將赫夫曼編碼表存放在 Map<Byte,String> 形式
// 生成的赫夫曼編碼表{32(空格)=01, 97(a)=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();
//為了呼叫方便,我們過載 getCodes
private static Map<Byte, String> getCodes(Node root) {
if(root == null) {
return null;
}
//處理root的左子樹
getCodes(root.left, "0", stringBuilder);
//處理root的右子樹
getCodes(root.right, "1", stringBuilder);
return 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等於空,不處理
//判斷當前node是葉子節點還是非葉子結點
if (node.data == null){//非葉子節點
//遞迴處理
//向左遞迴
getCodes(node.left, "0", stringBuilder2);
//向右遞迴
getCodes(node.right, "1", stringBuilder2);
}else {//進入到這裡說明是葉子節點,找到了最後
huffmanCodes.put(node.data,stringBuilder2.toString());
}
}
}
結果:
2、使用赫夫曼編碼來生成赫夫曼編碼資料
程式碼實現
//測試返回byte陣列
byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
System.out.println("huffmanCodeBytes="+Arrays.toString(huffmanCodeBytes));//17
//...
//編寫一個方法,將字串對應的byte[] 陣列,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼 壓縮後的byte[]
/**
*
* @param bytes 這是原始的字串對應的 byte[]
* @param huffmanCodes 生成的赫夫曼編碼map
* @return 返回赫夫曼編碼處理後的 byte[]
* 舉例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
* 返回的是 字串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
* => 對應的 byte[] huffmanCodeBytes ,即 8位元對應一個 byte,放入到 huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(二補數) => byte [推導 10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000(原碼)= -88 ]
* huffmanCodeBytes[1] = -88
*/
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));
}
//System.out.println("測試 stringBuilder~~~=" + stringBuilder.toString());
//將 "1010100010111111110..." 轉成 byte[]
//統計返回 byte[] huffmanCodeBytes 長度
//一句話 int len = (stringBuilder.length() + 7) / 8;
int len;
if(stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//建立 儲存壓縮後的 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;
}
將3.3與3.4中編寫的所有方法封裝成一個方法
//使用一個方法,將前面的方法封裝起來,便於我們的呼叫
/**
* @param bytes 原始的字串對應的位元組陣列
* @return 是經過 赫夫曼編碼處理後的位元組陣列(壓縮後的陣列)
*/
private static byte[] huffmanZip(byte[] bytes){
List<Node> nodes = getNodes(bytes);
//根據nodes建立的赫夫曼樹
Node huffmanTreeRoot = createHuffmanTree(nodes);
//生成對應的赫夫曼編碼(根據赫夫曼樹)
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
//根據生成的赫夫曼編碼來對原始的位元組陣列進行壓縮
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
return huffmanCodeBytes;
}
資料壓縮的所有程式碼
import java.util.*;
public class HuffmanCode {
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("原始的content字串長度為:"+contentBytes.length);//40
byte[] huffmanCodesBytes = huffmanZip(contentBytes);
System.out.println("對content字串壓縮後的結果是:"+Arrays.toString(huffmanCodesBytes));
System.out.println("長度為:"+huffmanCodesBytes.length);//17
}
//使用一個方法,將前面的方法封裝起來,便於我們的呼叫
/**
* @param bytes 原始的字串對應的位元組陣列
* @return 是經過 赫夫曼編碼處理後的位元組陣列(壓縮後的陣列)
*/
private static byte[] huffmanZip(byte[] bytes){
List<Node> nodes = getNodes(bytes);
//根據nodes建立的赫夫曼樹
Node huffmanTreeRoot = createHuffmanTree(nodes);
//生成對應的赫夫曼編碼(根據赫夫曼樹)
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
//根據生成的赫夫曼編碼來對原始的位元組陣列進行壓縮
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
return huffmanCodeBytes;
}
//編寫一個方法,將字串對應的byte[] 陣列,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼 壓縮後的byte[]
/**
*
* @param bytes 這是原始的字串對應的 byte[]
* @param huffmanCodes 生成的赫夫曼編碼map
* @return 返回赫夫曼編碼處理後的 byte[]
* 舉例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
* 返回的是 字串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
* => 對應的 byte[] huffmanCodeBytes ,即 8位元對應一個 byte,放入到 huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(二補數) => byte [推導 10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000(原碼)= -88 ]
* huffmanCodeBytes[1] = -88
*/
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));
}
//System.out.println("測試 stringBuilder~~~=" + stringBuilder.toString());
//將 "1010100010111111110..." 轉成 byte[]
//統計返回 byte[] huffmanCodeBytes 長度
//一句話 int len = (stringBuilder.length() + 7) / 8;
int len;
if(stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//建立 儲存壓縮後的 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;
}
//生成赫夫曼樹對應的赫夫曼編碼
//思路:
//1. 將赫夫曼編碼表存放在 Map<Byte,String> 形式
// 生成的赫夫曼編碼表{32(空格)=01, 97(a)=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();
//為了呼叫方便,我們過載 getCodes
private static Map<Byte, String> getCodes(Node root) {
if(root == null) {
return null;
}
//處理root的左子樹
getCodes(root.left, "0", stringBuilder);
//處理root的右子樹
getCodes(root.right, "1", stringBuilder);
return 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等於空,不處理
//判斷當前node是葉子節點還是非葉子結點
if (node.data == null){//非葉子節點
//遞迴處理
//向左遞迴
getCodes(node.left, "0", stringBuilder2);
//向右遞迴
getCodes(node.right, "1", stringBuilder2);
}else {//進入到這裡說明是葉子節點,找到了最後
huffmanCodes.put(node.data,stringBuilder2.toString());
}
}
}
//前序遍歷的方法
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else {
System.out.println("赫夫曼樹為空");
}
}
/**
*
* @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<>();
//遍歷bytes,儲存每一個byte出現的次數=》map[key,value]
HashMap<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 ascii碼
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();
}
}
}
資料壓縮的結果:
壓縮率:(40-17)/40=57.5%
使用赫夫曼編碼來解碼資料,具體要求是
[-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"
在資料解壓的過程中我們需要兩個方法,一個是將壓縮後的結果轉為二進位制的字串,一個是對壓縮資料進行解碼
/**
* 將一個byte 轉成一個二進位制的字串, 這裡需要利用二進位制的原碼,反碼,二補數
* @param b 傳入的 byte
* @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個位元組,無需補高位
* @return 是該b 對應的二進位制的字串,(注意是按二補數返回)
*/
private static String byteToBitString(boolean flag, byte b) {
//使用變數儲存 b
int temp = b; //將 b 轉成 int
//如果是正數我們還存在補高位
if(flag) {
temp |= 256; //按位元與 256 1 0000 0000 | 0000 0001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp); //返回的是temp對應的二進位制的二補數
if(flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
//編寫一個方法,完成對壓縮資料的解碼
/**
*
* @param huffmanCodes 赫夫曼編碼表 map(key = value)
* @param huffmanBytes 赫夫曼編碼得到的位元組陣列
* @return 就是原來的字串對應的陣列
*/
private static byte[] decode(Map<Byte,String> huffmanCodes, byte[] huffmanBytes) {
//1. 先得到 huffmanBytes 對應的 二進位制的字串 , 形式 1010100010111...
StringBuilder stringBuilder = new StringBuilder();
//將byte陣列轉成二進位制的字串
for(int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
//判斷是不是最後一個位元組
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
//把字串按照指定的赫夫曼編碼進行解碼
//把赫夫曼編碼表進行調換,因為反向查詢 a->100 100->a
Map<String, Byte> map = new HashMap<String,Byte>();
for(Map.Entry<Byte, String> entry: huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
//key = value 變成 value = key
}
//建立要給集合,存放byte
List<Byte> list = new ArrayList<>();
//i 可以理解成就是索引,掃描 stringBuilder
for(int i = 0; i < stringBuilder.length(); ) {
int count = 1; // 小的計數器
boolean flag = true;
Byte b = null;
while(flag) {
//1010100010111...
//遞增的取出 key 1 (1,10,101...匹配)
String key = stringBuilder.substring(i, i+count);//i 不動,讓count移動,指定匹配到一個字元
b = map.get(key);
if(b == null) {//說明沒有匹配到
count++;
}else {
//匹配到
flag = false;
}
}
list.add(b);
i += count;//i 直接移動到 count
}
//當for迴圈結束後,我們list中就存放了所有的字元 "i like like like java do you like a java"
//把list 中的資料放入到byte[] 並返回
byte b[] = new byte[list.size()];
for(int i = 0;i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
測試
//解壓
byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
System.out.println("(解壓後)原來的字串="+new String(sourceBytes));
我們學習了通過赫夫曼編碼對一個字串進行編碼和解碼, 下面我們來完成對檔案的壓縮和解壓, 具體要求:
給你一個圖片檔案,要求對其進行無失真壓縮, 看看壓縮效果如何
思路:讀取檔案-> 得到赫夫曼編碼表 -> 完成壓縮
首先我們建立一個圖片檔案
壓縮程式碼
//編寫方法,將一個檔案進行壓縮
/**
*
* @param srcFile 你傳入的希望壓縮的檔案的全路徑
* @param dstFile 我們壓縮後將壓縮檔案放到哪個目錄
*/
public static void zipFile(String srcFile, String dstFile) {
//建立輸出流
OutputStream os = null;
ObjectOutputStream oos = null;
//建立檔案的輸入流
FileInputStream is = null;
try {
//建立檔案的輸入流
is = new FileInputStream(srcFile);
//建立一個和原始檔大小一樣的byte[]
byte[] b = new byte[is.available()];
//讀取檔案
is.read(b);
//直接對原始檔壓縮
byte[] huffmanBytes = huffmanZip(b);
//建立檔案的輸出流, 存放壓縮檔案
os = new FileOutputStream(dstFile);
//建立一個和檔案輸出流關聯的ObjectOutputStream
oos = new ObjectOutputStream(os);
//把 赫夫曼編碼後的位元組陣列寫入壓縮檔案
oos.writeObject(huffmanBytes);
//這裡我們以物件流的方式寫入 赫夫曼編碼,是為了以後我們恢復原始檔時使用
//注意一定要把赫夫曼編碼 寫入壓縮檔案
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());
}
}
}
測試程式碼
//測試壓縮檔案
String srcFile = "E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\3.jpg";
String dstFile ="E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\3.zip";
zipFile(srcFile,dstFile);
System.out.println("壓縮檔案成功");
結果
具體要求:將前面壓縮的檔案,重新恢復成原來的檔案。
思路:讀取壓縮檔案(資料和赫夫曼編碼表)-> 完成解壓(檔案恢復)
檔案解壓程式碼
//編寫一個方法,完成對壓縮檔案的解壓
/**
*
* @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);
//建立一個和 is關聯的物件輸入流
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);
//將bytes 陣列寫入到目標檔案
os = new FileOutputStream(dstFile);
//寫資料到 dstFile 檔案
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
}
測試程式碼
//測試解壓檔案
String zipFile = "E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\3.zip";
String dstFile = "E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\32.jpg";
unZipFile(zipFile,dstFile);
System.out.println("解壓成功");
結果
我們取資料夾中檢視發現原始的圖片和解壓後的檔案大小一樣---->無失真壓縮
我們將赫夫曼編碼所有的程式碼進行彙總
package com.qjd.huffmancode;
import java.io.*;
import java.util.*;
public class HuffmanCode {
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("原始的content字串長度為:"+contentBytes.length);//40
//壓縮
byte[] huffmanCodesBytes = huffmanZip(contentBytes);
System.out.println("對content字串壓縮後的結果是:"+Arrays.toString(huffmanCodesBytes)+"長度為:"+huffmanCodesBytes.length);
//解壓
byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
System.out.println("(解壓後)原來的字串="+new String(sourceBytes)+"長度為:"+new String(sourceBytes).length());
*/
//測試壓縮檔案
// String srcFile = "E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\3.jpg";
// String dstFile ="E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\3.zip";
// zipFile(srcFile,dstFile);
// System.out.println("壓縮檔案成功");
//測試解壓檔案
String zipFile = "E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\3.zip";
String dstFile = "E:\\資料結構與演演算法學習\\myself\\owncode\\resources\\32.jpg";
unZipFile(zipFile,dstFile);
System.out.println("解壓成功");
}
//編寫一個方法,完成對壓縮檔案的解壓
/**
*
* @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);
//建立一個和 is關聯的物件輸入流
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);
//將bytes 陣列寫入到目標檔案
os = new FileOutputStream(dstFile);
//寫資料到 dstFile 檔案
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
}
//編寫方法,將一個檔案進行壓縮
/**
*
* @param srcFile 你傳入的希望壓縮的檔案的全路徑
* @param dstFile 我們壓縮後將壓縮檔案放到哪個目錄
*/
public static void zipFile(String srcFile, String dstFile) {
//建立輸出流
OutputStream os = null;
ObjectOutputStream oos = null;
//建立檔案的輸入流
FileInputStream is = null;
try {
//建立檔案的輸入流
is = new FileInputStream(srcFile);
//建立一個和原始檔大小一樣的byte[]
byte[] b = new byte[is.available()];
//讀取檔案
is.read(b);
//直接對原始檔壓縮
byte[] huffmanBytes = huffmanZip(b);
//建立檔案的輸出流, 存放壓縮檔案
os = new FileOutputStream(dstFile);
//建立一個和檔案輸出流關聯的ObjectOutputStream
oos = new ObjectOutputStream(os);
//把 赫夫曼編碼後的位元組陣列寫入壓縮檔案
oos.writeObject(huffmanBytes);
//這裡我們以物件流的方式寫入 赫夫曼編碼,是為了以後我們恢復原始檔時使用
//注意一定要把赫夫曼編碼 寫入壓縮檔案
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());
}
}
}
//完成資料的解壓
//思路
//1. 將huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
// 重新先轉成 赫夫曼編碼對應的二進位制的字串 "1010100010111..."
//2. 赫夫曼編碼對應的二進位制的字串 "1010100010111..." =》 對照 赫夫曼編碼 =》 "i like like like java do you like a java"
/**
* 將一個byte 轉成一個二進位制的字串, 這裡需要利用二進位制的原碼,反碼,二補數
* @param b 傳入的 byte
* @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個位元組,無需補高位
* @return 是該b 對應的二進位制的字串,(注意是按二補數返回)
*/
private static String byteToBitString(boolean flag, byte b) {
//使用變數儲存 b
int temp = b; //將 b 轉成 int
//如果是正數我們還存在補高位
if(flag) {
temp |= 256; //按位元與 256 1 0000 0000 | 0000 0001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp); //返回的是temp對應的二進位制的二補數
if(flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
//編寫一個方法,完成對壓縮資料的解碼
/**
*
* @param huffmanCodes 赫夫曼編碼表 map(key = value)
* @param huffmanBytes 赫夫曼編碼得到的位元組陣列
* @return 就是原來的字串對應的陣列
*/
private static byte[] decode(Map<Byte,String> huffmanCodes, byte[] huffmanBytes) {
//1. 先得到 huffmanBytes 對應的 二進位制的字串 , 形式 1010100010111...
StringBuilder stringBuilder = new StringBuilder();
//將byte陣列轉成二進位制的字串
for(int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
//判斷是不是最後一個位元組
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
//把字串按照指定的赫夫曼編碼進行解碼
//把赫夫曼編碼表進行調換,因為反向查詢 a->100 100->a
Map<String, Byte> map = new HashMap<String,Byte>();
for(Map.Entry<Byte, String> entry: huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
//key = value 變成 value = key
}
//建立要給集合,存放byte
List<Byte> list = new ArrayList<>();
//i 可以理解成就是索引,掃描 stringBuilder
for(int i = 0; i < stringBuilder.length(); ) {
int count = 1; // 小的計數器
boolean flag = true;
Byte b = null;
while(flag) {
//1010100010111...
//遞增的取出 key 1 (1,10,101...匹配)
String key = stringBuilder.substring(i, i+count);//i 不動,讓count移動,指定匹配到一個字元
b = map.get(key);
if(b == null) {//說明沒有匹配到
count++;
}else {
//匹配到
flag = false;
}
}
list.add(b);
i += count;//i 直接移動到 count
}
//當for迴圈結束後,我們list中就存放了所有的字元 "i like like like java do you like a java"
//把list 中的資料放入到byte[] 並返回
byte b[] = new byte[list.size()];
for(int i = 0;i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
//使用一個方法,將前面的方法封裝起來,便於我們的呼叫
/**
* @param bytes 原始的字串對應的位元組陣列
* @return 是經過 赫夫曼編碼處理後的位元組陣列(壓縮後的陣列)
*/
private static byte[] huffmanZip(byte[] bytes){
List<Node> nodes = getNodes(bytes);
//根據nodes建立的赫夫曼樹
Node huffmanTreeRoot = createHuffmanTree(nodes);
//生成對應的赫夫曼編碼(根據赫夫曼樹)
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
//根據生成的赫夫曼編碼來對原始的位元組陣列進行壓縮
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
return huffmanCodeBytes;
}
//編寫一個方法,將字串對應的byte[] 陣列,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼 壓縮後的byte[]
/**
*
* @param bytes 這是原始的字串對應的 byte[]
* @param huffmanCodes 生成的赫夫曼編碼map
* @return 返回赫夫曼編碼處理後的 byte[]
* 舉例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
* 返回的是 字串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
* => 對應的 byte[] huffmanCodeBytes ,即 8位元對應一個 byte,放入到 huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(二補數) => byte [推導 10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000(原碼)= -88 ]
* huffmanCodeBytes[1] = -88
*/
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));
}
//System.out.println("測試 stringBuilder~~~=" + stringBuilder.toString());
//將 "1010100010111111110..." 轉成 byte[]
//統計返回 byte[] huffmanCodeBytes 長度
//一句話 int len = (stringBuilder.length() + 7) / 8;
int len;
if(stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//建立 儲存壓縮後的 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;
}
//生成赫夫曼樹對應的赫夫曼編碼
//思路:
//1. 將赫夫曼編碼表存放在 Map<Byte,String> 形式
// 生成的赫夫曼編碼表{32(空格)=01, 97(a)=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();
//為了呼叫方便,我們過載 getCodes
private static Map<Byte, String> getCodes(Node root) {
if(root == null) {
return null;
}
//處理root的左子樹
getCodes(root.left, "0", stringBuilder);
//處理root的右子樹
getCodes(root.right, "1", stringBuilder);
return 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等於空,不處理
//判斷當前node是葉子節點還是非葉子結點
if (node.data == null){//非葉子節點
//遞迴處理
//向左遞迴
getCodes(node.left, "0", stringBuilder2);
//向右遞迴
getCodes(node.right, "1", stringBuilder2);
}else {//進入到這裡說明是葉子節點,找到了最後
huffmanCodes.put(node.data,stringBuilder2.toString());
}
}
}
//前序遍歷的方法
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else {
System.out.println("赫夫曼樹為空");
}
}
/**
*
* @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<>();
//遍歷bytes,儲存每一個byte出現的次數=》map[key,value]
HashMap<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 ascii碼
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();
}
}
}
給出一個數列 (7, 3, 10, 12, 5, 1, 9),要求能夠高效的完成對資料的查詢和新增
解決方案:
1、使用陣列
陣列未排序, 優點:直接在陣列尾新增,速度快。 缺點:查詢速度慢.
陣列排序,優點:可以使用二分查詢,查詢速度快,缺點:為了保證陣列有序,在新增新資料時,找到插入位置後,後面的資料需整體移動,速度慢。
2、使用鏈式儲存-連結串列
不管連結串列是否有序,查詢速度都慢,新增資料速度比陣列快,不需要資料整體移動。
3、使用二叉排序樹
二叉排序樹:
BST: (Binary Sort(Search) Tree), 對於二叉排序樹的 任何一個非葉子節點,要求 左子節點的值比當前節點的值小, 右子節點的值比當前節點的值大。
特別說明:如果有相同的值,可以將該節點放在左子節點或右子節點
比如針對前面的資料 (7, 3, 10, 12, 5, 1, 9) ,對應的二叉排序樹為:
一個陣列建立成對應的二叉排序樹,並使用中序遍歷二叉排序樹,比如: 陣列為 Array(7, 3, 10, 12, 5, 1, 9) , 創
建成對應的二叉排序樹為 :
二叉排序樹的建立與遍歷程式碼
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7,3,10,12,5,1,9};
BinarySortTree binarySortTree = new BinarySortTree();
//迴圈的新增節點到二叉排序樹
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍歷二叉排序樹
System.out.println("中序遍歷二叉排序樹");
binarySortTree.infixOrder();
}
}
//建立二叉排序樹
class BinarySortTree{
private Node root;
//新增節點的方法
public void add(Node node){
if (root == null){
root = node;//如果root為空則直接讓root指向node
}else {
root.add(node);
}
}
//中序遍歷
public void infixOrder(){
if (root != null){
root.infixOrder();
}else {
System.out.println("二叉排序樹為空,不能遍歷");
}
}
}
//建立Node節點
class Node{
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//新增節點的方法
//遞迴的形式新增節點,需要滿足二叉排序樹的要求
public void add(Node node){
if (node == null){
return;
}
//判斷傳入的節點的值,和當前子樹根節點的值的關係
if (node.value<this.value){
//如果當前節點左子節點為空,直接將node給左節點
if (this.left == null){
this.left = node;
}else {//如果當前節點左子節點不為空,就遞迴的向左子樹進行新增
this.left.add(node);
}
}else {//新增的節點的值大於當前節點的值
if (this.right == null){
this.right = node;
}else {
this.right.add(node);
}
}
}
//中序遍歷的方法
public void infixOrder() {
if(this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if(this.right != null) {
this.right.infixOrder();
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
結果:
二叉排序樹的刪除情況比較複雜,有下面三種情況需要考慮
點 刪除葉子節點 (比如:2, 5, 9, 12)
刪除點 只有一顆子樹的節點 (比如:1)
刪除 有兩顆子樹的節點. (比如:7, 3,10 )
思路分析
第一種情況:刪除葉子節點 (比如:2, 5, 9, 12)
思路
(1) 需求先去找到要刪除的結點 targetNode
(2) 找到targetNode
的 父結點parent
(3) 確定 targetNode
是 parent
的左子結點 還是右子結點
(4) 根據前面的情況來對應刪除
左子結點 parent.left = null
右子結點 parent.right = null;
第二種情況: 刪除只有一顆子樹的節點 比如1
思路
(1) 需求先去找到要刪除的結點 targetNode
(2) 找到targetNode
的 父結點parent
(3) 確定targetNode
的子結點是左子結點還是右子結點
(4) targetNode
是parent
的左子結點還是右子結點
(5) 如果targetNode
有左子結點
targetNode
是 parent
的左子結點parent.left = targetNode.left;
5.2 如果targetNode
是 parent
的右子結點
parent.right = targetNode.left;
(6) 如果targetNode
有右子結點
6.1 如果 targetNode
是 parent
的左子結點
parent.left = targetNode.right;
6.2 如果 targetNode
是parent
的右子結點
parent.right = targetNode.right
第三種情況 : 刪除有兩顆子樹的節點. (比如:7, 3,10 )
思路
(1) 需求先去找到要刪除的結點 targetNode
(2) 找到targetNode
的 父結點parent
(3) 從targetNode
的右子樹找到最小的結點
(4) 用一個臨時變數,將 最小結點的值儲存 temp = 12
(5) 刪除該最小結點
(6) targetNode.value = temp
程式碼實現
package com.qjd.binarysorttree;
public class BinarySortTreeDemo {
public static void main(String[] args) {
int[] arr = {7,3,10,12,5,1,9,2};
BinarySortTree binarySortTree = new BinarySortTree();
//迴圈的新增節點到二叉排序樹
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍歷二叉排序樹
System.out.println("中序遍歷二叉排序樹");
binarySortTree.infixOrder();
//測試刪除節點
// binarySortTree.delNode(2);
// binarySortTree.delNode(5);
// binarySortTree.delNode(9);
// binarySortTree.delNode(12);
// binarySortTree.delNode(1);
binarySortTree.delNode(10);
System.out.println("刪除節點後");
binarySortTree.infixOrder();
}
}
//建立二叉排序樹
class BinarySortTree{
private Node root;
//新增節點的方法
public void add(Node node){
if (root == null){
root = node;//如果root為空則直接讓root指向node
}else {
root.add(node);
}
}
//查詢要刪除的結點
public Node search(int value) {
if(root == null) {
return null;
} else {
return root.search(value);
}
}
//查詢父結點
public Node searchParent(int value) {
if(root == null) {
return null;
} else {
return root.searchParent(value);
}
}
//編寫方法:
//1. 返回的 以node 為根結點的二叉排序樹的最小結點的值
//2. 刪除node 為根結點的二叉排序樹的最小結點
/**
*
* @param node 傳入的結點(當做二叉排序樹的根結點)
* @return 返回的 以node 為根結點的二叉排序樹的最小結點的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
//!!!這裡是向右子樹查詢,但是因為是二叉排序樹所以最小值一定在左子樹上
while(target.left != null) {
target = target.left;
}
//這時 target就指向了最小結點
//刪除最小結點
delNode(target.value);
return target.value;
}
//刪除結點
public void delNode(int value) {
if(root == null) {
return;
}else {
//1.需要先去找到要刪除的結點 targetNode
Node targetNode = search(value);
//如果沒有找到要刪除的結點
if(targetNode == null) {
return;
}
//如果我們發現當前這顆二叉排序樹只有一個結點
if(root.left == null && root.right == null) {
root = null;
return;
}
//去找到targetNode的父結點
Node parent = searchParent(value);
//如果要刪除的結點是葉子結點
if(targetNode.left == null && targetNode.right == null) {
//判斷targetNode 是父結點的左子結點,還是右子結點
if(parent.left != null && parent.left.value == value) { //是左子結點
parent.left = null;
} else if (parent.right != null && parent.right.value == value) {//是右子結點
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) { //刪除有兩顆子樹的節點
int minVal = delRightTreeMin(targetNode.right);//在右子樹中查詢最小值
targetNode.value = minVal;
} else { // 刪除只有一顆子樹的結點
//如果要刪除的結點有左子結點
if(targetNode.left != null) {
if(parent != null) {
//如果 targetNode 是 parent 的左子結點
if(parent.left.value == value) {
parent.left = targetNode.left;
} else { // targetNode 是 parent 的右子結點
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else { //如果要刪除的結點有右子結點
if(parent != null) {
//如果 targetNode 是 parent 的左子結點
if(parent.left.value == value) {
parent.left = targetNode.right;
} else { //如果 targetNode 是 parent 的右子結點
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
//中序遍歷
public void infixOrder(){
if (root != null){
root.infixOrder();
}else {
System.out.println("二叉排序樹為空,不能遍歷");
}
}
}
//建立Node節點
class Node{
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//查詢要刪除的節點
/**
*
* @param value 希望刪除的結點的值
* @return 如果找到返回該結點,否則返回null
*/
public Node search(int value) {
if(value == this.value) { //找到就是該結點
return this;
} else if(value < this.value) {//如果查詢的值小於當前結點,向左子樹遞迴查詢
//如果左子結點為空
if(this.left == null) {
return null;
}
return this.left.search(value);
} else { //如果查詢的值不小於當前結點,向右子樹遞迴查詢
if(this.right == null) {
return null;
}
return this.right.search(value);
}
}
//查詢要刪除結點的父結點
/**
*
* @param value 要找到的結點的值
* @return 返回的是要刪除的結點的父結點,如果沒有就返回null
*/
public Node searchParent(int value) {
//如果當前結點就是要刪除的結點的父結點,就返回
if((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else {
//如果查詢的值小於當前結點的值, 並且當前結點的左子結點不為空
if(value < this.value && this.left != null) {
return this.left.searchParent(value); //向左子樹遞迴查詢
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); //向右子樹遞迴查詢
} else {
return null; // 沒有找到父結點
}
}
}
//新增節點的方法
//遞迴的形式新增節點,需要滿足二叉排序樹的要求
public void add(Node node){
if (node == null){
return;
}
//判斷傳入的節點的值,和當前子樹根節點的值的關係
if (node.value<this.value){
//如果當前節點左子節點為空,直接將node給左節點
if (this.left == null){
this.left = node;
}else {//如果當前節點左子節點不為空,就遞迴的向左子樹進行新增
this.left.add(node);
}
}else {//新增的節點的值大於當前節點的值
if (this.right == null){
this.right = node;
}else {
this.right.add(node);
}
}
}
//中序遍歷的方法
public void infixOrder() {
if(this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if(this.right != null) {
this.right.infixOrder();
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
結果:
1、刪除葉子節點2、5、9
2、刪除只有一顆子樹的節點 1
3、刪除有兩顆子樹的節點10
給出一個數列{1,2,3,4,5,6}
,要求建立一顆二叉排序樹(BST), 並分析問題所在.
左邊 BST 存在的問題分析:
1、要求: 給出一個數列,建立出對應的平衡二元樹.數列 {4,3,6,5,7,8}
2、思路分析
1、要求: 給出一個數列,建立出對應的平衡二元樹.數列 {10,12, 8, 9, 7, 6}
2、思路分析
前面的兩個數列,進行單旋轉(即一次旋轉)就可以將非平衡二元樹轉成平衡二元樹,但是在某些情況下,單旋轉
不能完成平衡二元樹的轉換。比如數列
int[] arr = { 10, 11, 7, 6, 8, 9 };
執行原來的程式碼可以看到,並沒有轉成 AVL 樹.
int[] arr = {2,1,6,5,7,3};
執行原來的程式碼可以看到,並沒有轉成 AVL 樹
1、問題分析
2、思路分析
public class AvlTreeDemo {
public static void main(String[] args) {
//int[] arr = {4,3,6,5,7,8};
//int[] arr = { 10, 12, 8, 9, 7, 6 };
int[] arr = { 10, 11, 7, 6, 8, 9 };
//建立一個 AVLTree物件
AVLTree avlTree = new AVLTree();
//新增結點
for(int i=0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
//遍歷
System.out.println("中序遍歷");
avlTree.infixOrder();
System.out.println("在平衡處理後···");
System.out.println("樹的高度=" + avlTree.getRoot().height()); //3
System.out.println("樹的左子樹高度=" + avlTree.getRoot().leftHeight()); // 2
System.out.println("樹的右子樹高度=" + avlTree.getRoot().rightHeight()); // 2
System.out.println("當前的根結點=" + avlTree.getRoot());//8
}
}
// 建立AVLTree
class AVLTree {
private Node root;
public Node getRoot() {
return root;
}
// 查詢要刪除的結點
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
// 查詢父結點
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
// 編寫方法:
// 1. 返回的 以node 為根結點的二叉排序樹的最小結點的值
// 2. 刪除node 為根結點的二叉排序樹的最小結點
/**
*
* @param node
* 傳入的結點(當做二叉排序樹的根結點)
* @return 返回的 以node 為根結點的二叉排序樹的最小結點的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
// 迴圈的查詢左子節點,就會找到最小值
while (target.left != null) {
target = target.left;
}
// 這時 target就指向了最小結點
// 刪除最小結點
delNode(target.value);
return target.value;
}
// 刪除結點
public void delNode(int value) {
if (root == null) {
return;
} else {
// 1.需求先去找到要刪除的結點 targetNode
Node targetNode = search(value);
// 如果沒有找到要刪除的結點
if (targetNode == null) {
return;
}
// 如果我們發現當前這顆二叉排序樹只有一個結點
if (root.left == null && root.right == null) {
root = null;
return;
}
// 去找到targetNode的父結點
Node parent = searchParent(value);
// 如果要刪除的結點是葉子結點
if (targetNode.left == null && targetNode.right == null) {
// 判斷targetNode 是父結點的左子結點,還是右子結點
if (parent.left != null && parent.left.value == value) { // 是左子結點
parent.left = null;
} else if (parent.right != null && parent.right.value == value) {// 是由子結點
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) { // 刪除有兩顆子樹的節點
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
} else { // 刪除只有一顆子樹的結點
// 如果要刪除的結點有左子結點
if (targetNode.left != null) {
if (parent != null) {
// 如果 targetNode 是 parent 的左子結點
if (parent.left.value == value) {
parent.left = targetNode.left;
} else { // targetNode 是 parent 的右子結點
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else { // 如果要刪除的結點有右子結點
if (parent != null) {
// 如果 targetNode 是 parent 的左子結點
if (parent.left.value == value) {
parent.left = targetNode.right;
} else { // 如果 targetNode 是 parent 的右子結點
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
// 新增結點的方法
public void add(Node node) {
if (root == null) {
root = node;// 如果root為空則直接讓root指向node
} else {
root.add(node);
}
}
// 中序遍歷
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("二叉排序樹為空,不能遍歷");
}
}
}
// 建立Node結點
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
// 返回左子樹的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
// 返回右子樹的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
// 返回 以該結點為根結點的樹的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
//左旋轉方法
private void leftRotate() {
//建立新的結點,以當前根結點的值
Node newNode = new Node(value);
//把新的結點的左子樹設定成當前結點的左子樹
newNode.left = left;
//把新的結點的右子樹設定成帶你過去結點的右子樹的左子樹
newNode.right = right.left;
//把當前結點的值替換成右子結點的值
value = right.value;
//把當前結點的右子樹設定成當前結點右子樹的右子樹
right = right.right;
//把當前結點的左子樹(左子結點)設定成新的結點
left = newNode;
}
//右旋轉
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
// 查詢要刪除的結點
/**
*
* @param value
* 希望刪除的結點的值
* @return 如果找到返回該結點,否則返回null
*/
public Node search(int value) {
if (value == this.value) { // 找到就是該結點
return this;
} else if (value < this.value) {// 如果查詢的值小於當前結點,向左子樹遞迴查詢
// 如果左子結點為空
if (this.left == null) {
return null;
}
return this.left.search(value);
} else { // 如果查詢的值不小於當前結點,向右子樹遞迴查詢
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
// 查詢要刪除結點的父結點
/**
*
* @param value
* 要找到的結點的值
* @return 返回的是要刪除的結點的父結點,如果沒有就返回null
*/
public Node searchParent(int value) {
// 如果當前結點就是要刪除的結點的父結點,就返回
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
return this;
} else {
// 如果查詢的值小於當前結點的值, 並且當前結點的左子結點不為空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); // 向左子樹遞迴查詢
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); // 向右子樹遞迴查詢
} else {
return null; // 沒有找到父結點
}
}
}
@Override
public String toString() {
return "Node [value=" + value + "]";
}
// 新增結點的方法
// 遞迴的形式新增結點,注意需要滿足二叉排序樹的要求
public void add(Node node) {
if (node == null) {
return;
}
// 判斷傳入的結點的值,和當前子樹的根結點的值關係
if (node.value < this.value) {
// 如果當前結點左子結點為null
if (this.left == null) {
this.left = node;
} else {
// 遞迴的向左子樹新增
this.left.add(node);
}
} else { // 新增的結點的值大於 當前結點的值
if (this.right == null) {
this.right = node;
} else {
// 遞迴的向右子樹新增
this.right.add(node);
}
}
//當新增完一個結點後,如果: (右子樹的高度-左子樹的高度) > 1 , 左旋轉
if(rightHeight() - leftHeight() > 1) {
//如果它的右子樹的左子樹的高度大於它的右子樹的右子樹的高度
if(right != null && right.leftHeight() > right.rightHeight()) {
//先對右子結點進行右旋轉
right.rightRotate();
//然後在對當前結點進行左旋轉
leftRotate(); //左旋轉..
} else {
//直接進行左旋轉即可
leftRotate();
}
return ; //必須要!!!
}
//當新增完一個結點後,如果 (左子樹的高度 - 右子樹的高度) > 1, 右旋轉
if(leftHeight() - rightHeight() > 1) {
//如果它的左子樹的右子樹高度大於它的左子樹的高度
if(left != null && left.rightHeight() > left.leftHeight()) {
//先對當前結點的左結點(左子樹)->左旋轉
left.leftRotate();
//再對當前結點進行右旋轉
rightRotate();
} else {
//直接進行右旋轉即可
rightRotate();
}
}
}
// 中序遍歷
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
測試結果:
到這裡關於樹結構的實際應用的內容就結束了,關於樹結構的具體應用像赫夫曼編碼,二叉排序樹等程式碼比較複雜,
大家重點要根據思路圖解來分析解題過程,程式碼的具體實現要儘量理解,
最後希望這篇文章對大家有所幫助(◍•͈⌔•͈◍)