Java數據結構與算法 day08 樹結構實際應用(一)

第10章 樹結構實際應用

本章源碼:https://github.com/name365/Java-Data-structure

堆排序

大頂堆和小頂堆圖解說明

堆排序基本介紹

1.堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均爲O(nlogn),它也是不穩定排序。
2.堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆, 注意 : 沒有要求結點的左孩子的值和右孩子的值的大小關係。
3.每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆
4.大頂堆舉例說明

在這裏插入圖片描述

對堆中的結點按層進行編號,映射到數組中就是下面這個樣子:

在這裏插入圖片描述

大頂堆特點:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]  // i 對應第幾個節點,i從0開始編號7
  • 小頂堆舉例說明

在這裏插入圖片描述

小頂堆:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2] // i 對應第幾個節點,i從0開始編號

綜上:一般升序採用大頂堆降序採用小頂堆


堆排序的思路圖解與實現

堆排序基本思想

1.將待排序序列構造成一個大頂堆
2.此時,整個序列的最大值就是堆頂的根節點。
3.將其與末尾元素進行交換,此時末尾就爲最大值。
4.然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了。

可以看到在構建大頂堆的過程中,元素的個數逐漸減少,最後就得到一個有序序列了.

要求:給你一個數組 {4,6,8,5,9} , 要求使用堆排序法,將數組升序排序。

堆排序步驟圖解說明:

步驟一:構造初始堆。將給定無序序列構造成一個大頂堆(一般升序採用大頂堆,降序採用小頂堆)。原始的數組[4,6,8,5,9]

  • 假設給定無序序列結構如下 :

在這裏插入圖片描述

  • 此時我們從最後一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。

在這裏插入圖片描述

  • 找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。

在這裏插入圖片描述

  • 這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。

在這裏插入圖片描述

  • 此時,就將一個無序序列構造成了一個大頂堆。

步驟二:將堆頂元素與末尾元素進行交換,使末尾元素最大。然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反覆進行交換、重建、交換。

  • 將堆頂元素9和末尾元素4進行交換 。

在這裏插入圖片描述

  • 重新調整結構,使其繼續滿足堆定義。

在這裏插入圖片描述

  • 再將堆頂元素 8 與末尾元素 5 進行交換,得到第二大元素 8。

在這裏插入圖片描述

  • 後續過程,繼續進行調整,交換,如此反覆進行,最終使得整個序列有序。

在這裏插入圖片描述

對上述思路進行總結:
1.將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆; 
2.將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端; 
3.重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟, 直到整個序列有序。

核心代碼說明:

public static void adHeap(int arr[],int i,int length){
		int temp = arr[i];	//先取出當前元素的值,保存在臨時變量
		//說明:
		//1. j = i * 2 + 1 j 是 i結點的左子結點
		for(int j = i * 2 + 1;j < length;j = j * 2 + 1){
			if(j+1 < length && arr[j] < arr[j+1]){	//說明左子節點的值小於右子節點的值
				j++;	//j指向右子節點
			}
			if(arr[j] > temp){ //如果子節點大於父結點
				arr[i] = arr[j];	//將較大的值賦給當前節點
				i = j;	//讓i指向j,繼續循環
			}else{
				break;
			}
			//當for 循環結束後,已經將以 i 爲父結點的樹的最大值,放在了最頂(局部)
			arr[i] = temp;	//將temp值放到調整後的位置
		}
	}

初始情況:

arr = {4,6,8,5,9}

當 i = 1 時:

在這裏插入圖片描述

首先,當temp=arr[i] ==> temp=6;進入for循環,j=i*2+1 ==> j=3,

進入if判斷,執行j++,即j指向右子節點,j=4;

arr[j] ==> arr[4] 即: arr[j]爲9,

此時,arr[j] > temp ==> 9 > 6,進入判斷

在這裏插入圖片描述

此時,子節點大於父節點,

而此時 i=1,j=4,即arr[i]=arr[j]進行賦值,arr[i] = arr[j] ==> arr[1]=9,將6替換成9。

i=j ==> i=4 ,繼續讓i指向j,因爲節點可能下面還有節點,繼續執行循環.

在這裏插入圖片描述

但此時 j=4 ,經過循環賦值語句,j=9,temp=6,不符合判斷條件,直接執行 arr[i] = temp ==> arr[4]=6;

在這裏插入圖片描述

上述過程代碼化實現:

import java.util.Arrays;

public class HeapSort {

	public static void main(String[] args) {
		//要求:將數組進行升序排序
		int arr[] = {4,6,8,5,9};
		headSort(arr);
	}
	
	//編寫一個堆排序的方法
	public static void headSort(int arr[]){
		System.out.println("堆排序:");
		
		//分佈完成
		adHeap(arr,1,arr.length);
		System.out.println("第一次:" + Arrays.toString(arr)); //4,9,8,5,6
		
		adHeap(arr,0,arr.length);
		System.out.println("第二次:" + Arrays.toString(arr)); //9,6,8,5,4

	}
	
	//將一個數組(二叉樹),調整成一個大頂堆
	/**
	  * 功能: 完成 將 以 i 對應的非葉子結點的樹調整成大頂堆
	  * 舉例: int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adHeap => 得到 {4, 9, 8, 5, 6}
	  * 如果我們再次調用  adHeap 傳入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5,4}
	  * @Description 
	  * @author subei
	  * @date 2020年6月8日上午10:29:31
	  * @param arr 待調整的數組
	  * @param i 非葉子結點的索引
	  * @param length 表示對多少個元素進行調整
	 */
	public static void adHeap(int arr[],int i,int length){
		int temp = arr[i];	//先取出當前元素的值,保存在臨時變量
		//說明:
		//1. j = i * 2 + 1 j 是 i結點的左子結點
		for(int j = i * 2 + 1;j < length;j = j * 2 + 1){
			if(j+1 < length && arr[j] < arr[j+1]){	//說明左子節點的值小於右子節點的值
				j++;	//j指向右子節點
			}
			if(arr[j] > temp){ //如果子節點大於父結點
				arr[i] = arr[j];	//將較大的值賦給當前節點
				i = j;	//讓i指向j,繼續循環
			}else{
				break;
			}
			//當for 循環結束後,已經將以 i 爲父結點的樹的最大值,放在了最頂(局部)
			arr[i] = temp;	//將temp值放到調整後的位置
		}
	}
}

最終完整代碼實現如下:

import java.util.Arrays;

public class HeapSort2 {

	public static void main(String[] args) {
		//要求:將數組進行升序排序
		int arr[] = {4,6,8,5,9};
		headSort(arr);
	}
	
	//編寫一個堆排序的方法
	public static void headSort(int arr[]){
		System.out.println("堆排序:");
		int temp = 0;
		//完成我們最終代碼
		//1.將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆
		for(int i=arr.length / 2 - 1;i >= 0;i--){
			adHeap(arr,i,arr.length);
		}
		System.out.println("步驟一的數組排序結果=" + Arrays.toString(arr));
		
		//2.將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
		//3.重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
		for(int j=arr.length - 1;j >0 ;j--){
			//交換
			temp=arr[j];
			arr[j]=arr[0];
			arr[0]=temp;
			adHeap(arr, 0, j); 
		}
		System.out.println("最後的數組排序結果=" + Arrays.toString(arr));
	}
	
	//將一個數組(二叉樹),調整成一個大頂堆
	/**
	  * 功能: 完成 將 以 i 對應的非葉子結點的樹調整成大頂堆
	  * 舉例: int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adHeap => 得到 {4, 9, 8, 5, 6}
	  * 如果我們再次調用  adHeap 傳入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5,4}
	  * @Description 
	  * @author subei
	  * @date 2020年6月8日上午10:29:31
	  * @param arr 待調整的數組
	  * @param i 非葉子結點的索引
	  * @param length 表示對多少個元素進行調整
	 */
	public static void adHeap(int arr[],int i,int length){
		int temp = arr[i];	//先取出當前元素的值,保存在臨時變量
		//說明:
		//1. j = i * 2 + 1 j 是 i結點的左子結點
		for(int j = i * 2 + 1;j < length;j = j * 2 + 1){
			if(j+1 < length && arr[j] < arr[j+1]){	//說明左子節點的值小於右子節點的值
				j++;	//j指向右子節點
			}
			if(arr[j] > temp){ //如果子節點大於父結點
				arr[i] = arr[j];	//將較大的值賦給當前節點
				i = j;	//讓i指向j,繼續循環
			}else{
				break;
			}
			//當for 循環結束後,已經將以 i 爲父結點的樹的最大值,放在了最頂(局部)
			arr[i] = temp;	//將temp值放到調整後的位置
		}
	}
}

對上述堆排序進行速度測試:

import java.text.SimpleDateFormat;
import java.util.Date;

public class HeapSort2 {

	public static void main(String[] args) {
		//要求:將數組進行升序排序
		
		//創建要給80000個的隨機的數組
		int[] arr = new int[8000000];
		for (int i = 0; i < 8000000; i++) {
			arr[i] = (int) (Math.random() * 8000000); // 生成一個[0, 8000000) 數
		}

		System.out.println("排序前");
		Date data1 = new Date();
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String date1Str = simpleDateFormat.format(data1);
		System.out.println("排序前的時間是=" + date1Str);
		
		headSort(arr);
		
		Date data2 = new Date();
		String date2Str = simpleDateFormat.format(data2);
		System.out.println("排序後的時間是=" + date2Str);
	}
	
	//編寫一個堆排序的方法
	public static void headSort(int arr[]){
		System.out.println("堆排序:");
		int temp = 0;
		//完成我們最終代碼
		//1.將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆
		for(int i=arr.length / 2 - 1;i >= 0;i--){
			adHeap(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;
			adHeap(arr, 0, j); 
		}
	}
	
	//將一個數組(二叉樹),調整成一個大頂堆
	/**
	  * 功能: 完成 將 以 i 對應的非葉子結點的樹調整成大頂堆
	  * 舉例: int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adHeap => 得到 {4, 9, 8, 5, 6}
	  * 如果我們再次調用  adHeap 傳入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5,4}
	  * @Description 
	  * @author subei
	  * @date 2020年6月8日上午10:29:31
	  * @param arr 待調整的數組
	  * @param i 非葉子結點的索引
	  * @param length 表示對多少個元素進行調整
	 */
	public static void adHeap(int arr[],int i,int length){
		int temp = arr[i];	//先取出當前元素的值,保存在臨時變量
		//說明:
		//1. j = i * 2 + 1 j 是 i結點的左子結點
		for(int j = i * 2 + 1;j < length;j = j * 2 + 1){
			if(j+1 < length && arr[j] < arr[j+1]){	//說明左子節點的值小於右子節點的值
				j++;	//j指向右子節點
			}
			if(arr[j] > temp){ //如果子節點大於父結點
				arr[i] = arr[j];	//將較大的值賦給當前節點
				i = j;	//讓i指向j,繼續循環
			}else{
				break;
			}
			//當for 循環結束後,已經將以 i 爲父結點的樹的最大值,放在了最頂(局部)
			arr[i] = temp;	//將temp值放到調整後的位置
		}
	}
}

赫夫曼樹

赫夫曼樹的基本介紹

給定n個權值作爲n個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度(wpl)達到最小,稱這樣的二叉樹爲最優二叉樹,也稱爲哈夫曼樹(Huffman Tree), 還有的書翻譯爲霍夫曼樹。

赫夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。

赫夫曼樹幾個重要概念和舉例:

1.路徑和路徑長度:在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱爲路徑。通路中分支的數目稱爲路徑長度。若規定根結點的層數爲1,則從根結點到第L層結點的路徑長度爲L-1
2.結點的權及帶權路徑長度:若將樹中結點賦給一個有着某種含義的數值,則這個數值稱爲該結點的權。結點的帶權路徑長度爲:從根結點到該結點之間的路徑長度與該結點的權的乘積

在這裏插入圖片描述

3.樹的帶權路徑長度:樹的帶權路徑長度規定爲所有葉子結點的帶權路徑長度之和,記爲WPL(weighted path length) ,權值越大的結點離根結點越近的二叉樹纔是最優二叉樹。
4.WPL最小的就是赫夫曼樹

在這裏插入圖片描述

赫夫曼樹創建步驟圖解與實現

要求:給你一個數列 {13, 7, 8, 3, 29, 6, 1},要求轉成一顆赫夫曼樹.

構成赫夫曼樹的步驟:

  1. 從小到大進行排序, 將每一個數據,每個數據都是一個節點 , 每個節點可以看成是一顆最簡單的二叉樹

  2. 取出根節點權值最小的兩顆二叉樹

  3. 組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和

在這裏插入圖片描述

  1. 再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複 1-2-3-4 的步驟,直到數列中,所有的數據都被處理,就得到一顆赫夫曼樹

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

赫夫曼樹創建代碼實現

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

public class HuffTreeTest {

	public static void main(String[] args) {
		int arr[] = { 13, 7, 8, 3, 29, 6, 1 };
		Node root = creatHFTree(arr);

		preOrder(root);
	}

	// 前序遍歷方法
	public static void preOrder(Node root) {
		if (root != null) {
			root.preOrder();
		} else {
			System.out.println("這是一個空樹。無法遍歷");
		}
	}

	// 創建赫夫曼樹的方法
	public static Node creatHFTree(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));
		}

		// int count = 0; //統計處理次數

		// 處理的過程是循環的過程
		while (nodes.size() > 1) {

			// 排序:從小到大排序
			Collections.sort(nodes);

			// System.out.println("第" + count + "次排序後的結果:nodes = " + nodes);

			// 取出根節點權值最小的兩顆二叉樹
			// (1)取出權值最小的結點(二叉樹)
			Node leftNode = nodes.get(0);
			// (1)取出權值另一個最小的結點(二叉樹)
			Node rightNode = nodes.get(1);

			// (3)構建一個新的二叉樹
			Node parents = new Node(leftNode.value + rightNode.value);
			parents.left = leftNode;
			parents.right = rightNode;

			// (4)從ArrayList刪除處理過的二叉樹
			nodes.remove(leftNode);
			nodes.remove(rightNode);

			// (5)將parent加入到nodes
			nodes.add(parents);

			// count++;
			// System.out.println("第" + count + "次處理後的結果:" + nodes);
		}

		// 返回赫夫曼樹的root結點
		return nodes.get(0);
	}

}

// 創建結點類
// 爲了讓Node 對象持續排序Collections集合排序
// 讓Node 實現Comparable接口
class Node implements Comparable<Node> {
	int value; // 結點權值
	Node left; // 指向左子結點
	Node right; // 指向右子結點

	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;
	}

	// 前序遍歷
	public void preOrder() {
		// 當前結點
		System.out.println(this);
		// 左子結點
		if (this.left != null) {
			this.left.preOrder();
		}
		// 右子結點
		if (this.right != null) {
			this.right.preOrder();
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章