最近明顯文章更新頻率下降了,那是由於我在惡補數據結構和算法的相關知識,至關因而從零開始學習。算法
找了不少視頻和資料,最後發現 b 站尚硅谷的視頻教程仍是相對不錯的,總共 195 集。每一個小節都是按先概念、原理,而後代碼實現的步驟講解。若是你也準備入門數據結構和算法,我推薦能夠看下這個系列教程。數組
昨天一天一會兒肝了 40 多集,從樹的後半部分到圖的所有部分。能夠看到,每一集其實時間也不算長,短的幾分鐘,長的也就半個小時。開 2 倍速看,倍兒爽。數據結構
話很少說,下面進入正題。數據結構和算法
咱們知道,樹有不少種,最經常使用的就是二叉樹了。二叉樹又有滿二叉樹和徹底二叉樹。而二叉堆,就是基於徹底二叉樹的一種數據結構。它有如下兩個特性。學習
所以,根據第二個特性,就把二叉堆分爲大頂堆(或叫最大堆),和小頂堆(或叫最小堆)。code
顧名思義,大頂堆,就是父節點大於等於左右孩子節點的堆,小頂堆就是父節點小於左右孩子節點的堆。視頻
看一下大頂堆的示例圖,小頂堆相似,只不過是小值在上而已。blog
注意:大頂堆只保證父節點大於左右孩子節點的值,不須要保證左右孩子節點之間的大小順序。如圖中,7 的左子節點 6 比右子節點 1 大,而 8 的左子節點 4 卻比右子節點 5 小。(小頂堆同理)排序
二叉堆的定義咱們知道了,那麼給你一個無序的徹底二叉樹,怎麼把它構建成二叉堆呢?教程
咱們以大頂堆爲例。給定如下一個數組,(徹底二叉樹通常用數組來存儲)
{4, 1, 9, 3, 7, 8, 5, 6, 2}
咱們畫出它的初始狀態,而後分析怎麼一步一步構建成大頂堆。
因爲大頂堆,父節點的值都大於左右孩子節點,因此樹的根節點確定是全部節點中值最大的。所以,咱們須要從樹的最後一層開始,逐漸的把大值向上調整(左右孩子節點中較大的節點和父節點交換),直到第一層。
其實,更具體的說,應該是從下面的非葉子節點開始調整。想想,爲何。
反向思考一下,若是從第一層開始調整的話,例如圖中就是 4 和 9 交換位置以後,你不能保證 9 就是全部節點的最大值(額,圖中的例子可能不是太好,正好是 9 最大)。若是下邊還有比 9 大的數字,你最終仍是須要從下面向上遍歷調整。那麼,我還不如一開始就直接從下向上調整呢。
另外,爲何從從最下面的非葉子節點(圖中節點 3 )開始。由於葉子節點的下面已經沒有子節點了,它只能和父節點比較,從葉子節點開始沒有意義。
第一步,以 3 爲父節點開始,比較他們的子節點 6和 2 ,6最大,而後和 3 交換位置。
第二步,6 和 7 比較,7 最大,7 和 1 交換位置。
第三步,7 和 9 比較,9 最大,9 和 4 交換位置。
第四步,咱們發現交換位置以後,4 下邊還有比它大的,所以還須要以 4 爲父節點和它的左右子節點進行比較。發現 8 最大,而後 8 和 4 交換位置。
最終,實現了一個大頂堆的構建。下面以代碼實現交換過程。
/** * 調整爲大頂堆 * @param arr 待調整的數組 * @param parent 當前父節點的下標 * @param length 須要對多少個元素進行調整 */ private static void adjustHeap(int[] arr, int parent, int length){ //臨時保存父節點 int temp = arr[parent]; //左子節點的下標 int child = 2 * parent + 1; //若是子節點的下標大於等於當前須要比較的元素個數,則結束循環 while(child < length){ //判斷左子節點和右子節點的大小,若右邊大,則把child定位到右邊 if(child + 1 < length && arr[child] < arr[child + 1]){ child ++; } //若child大於父節點,則交換位置,不然退出循環 if(arr[child] > temp){ //父子節點交換位置 arr[parent] = arr[child]; //由於交換位置以後,不能保證當前的子節點是它子樹的最大值,因此須要繼續向下比較, //把當前子節點設置爲下次循環的父節點,同時,找到它的左子節點,繼續下次循環 parent = child; child = 2 * parent + 1; }else{ //若是當前子節點小於等於父節點,則說明此時的父節點已是最大值了, //所以無需繼續循環 break; } } //把當前節點值替換爲最開始暫存的父節點值 arr[parent] = temp; } public static void main(String[] args) { int[] arr = {4,1,9,3,7,8,5,6,2}; //構建一個大頂堆,從最下面的非葉子節點開始向上遍歷 for (int i = arr.length/2 - 1 ; i >= 0; i--) { adjustHeap(arr,i,arr.length); } System.out.println(Arrays.toString(arr)); } //打印結果: [9, 7, 8, 6, 1, 4, 5, 3, 2]。 和咱們分析的結果如出一轍
在 while 循環中,if(arr[child] > temp) else的邏輯, 對應的就是圖中的第三步和第四步。即須要確保,交換後的子節點要比它下邊的孩子節點都大,否則須要繼續循環,調整位置。
堆排序就是利用大頂堆或者小頂堆的特性來進行排序的。
它的基本思想就是:
步驟:
仍是以上邊的數組爲例,看一下堆排序的過程。
一共有九個元素,把它調整爲大頂堆,而後把堆頂元素 9 和末尾元素 2 交換位置。
此時,9已經有序了,不須要調整。而後把剩餘八個元素調整爲大頂堆,再把這八個元素的堆頂元素和末尾元素交換位置,以下,8 和 3 交換位置。
此時,8和 9 已經有序了,不須要調整。而後把剩餘七個元素調整爲大頂堆,再把這七個元素的堆頂元素和末尾元素交換位置。以下, 7 和 2 交換位置。
以此類推,通過 n - 1 次循環調整,到了最後只剩下一個元素的時候,就不須要再比較了,由於它已是最小值了。
看起來好像過程很複雜,但實際上是很是高效的。沒有增刪,直接在原來的數組上修改就能夠。由於咱們知道數組的增刪是比較慢的,每次刪除,插入元素,都要移動數組後邊的 n 個元素。此外,也不佔用額外的空間。
代碼實現:
//堆排序,大頂堆,升序 private static void heapSort(int[] arr){ //構建一個大頂堆,從最下面的非葉子節點開始向上遍歷 for (int i = arr.length/2 - 1 ; i >= 0; i--) { adjustHeap(arr,i,arr.length); } System.out.println(Arrays.toString(arr)); //循環執行如下操做:1.交換堆頂元素和末尾元素 2.從新調整爲大頂堆 for (int i = arr.length - 1; i > 0; i--) { //將堆頂最大的元素與末尾元素互換,則數組中最後的元素變爲最大值 int temp = arr[i]; arr[i] = arr[0]; arr[0] = temp; //從堆頂開始從新調整結構,使之成爲大頂堆 // i表明當前數組須要調整的元素個數,是逐漸遞減的 adjustHeap(arr,0,i); } }
時間複雜度和空間複雜度:
堆排序,每次調整爲大頂堆的時間複雜度爲 O(logn),而 n 個元素,總共須要循環調整 n-1 次 ,因此堆排序的時間複雜度就是 O(nlogn)。它的數學推導比較複雜,感興趣的同窗能夠本身查看相關資料。
因爲沒有佔用額外的內存空間,所以,堆排序的空間複雜度爲 O(1)。