Java 數據結構 - 堆和堆排序:爲何快排比堆排序性能好
[toc]html
數據結構與算法目錄(http://www.javashuo.com/article/p-qvigrlkr-da.html)java
關於二叉樹這種數據結構在實現軟件工程中的應用,前面咱們已經介紹了紅黑樹,下面咱們再介紹另外一種常見的二叉樹 - 堆。算法
- 紅黑樹:基於平衡二叉查找樹的動態數據結構,用於快速插入和查找數據,其時間複雜度都是 O(logn)。
- 堆:按照結點大於等於(或小於等於)子結點,又分爲大頂堆和小頂堆。和紅黑樹不一樣,椎只是部分有序,即 "左結點 < 父結點 && 右結點 < 父結點",而有序二叉樹要求 "左結點 < 父結點 < 右結點"。椎插入和刪除元素的時間複雜度都是 O(logn)。堆常見的應用有優先級隊列和堆排序。
<center><img width=600 src='https://img2020.cnblogs.com/blog/1322310/202003/1322310-20200309114535069-1627370722.png'/></center>api
1. 什麼是堆
1.1 堆的定義
堆的嚴格定義以下,只要知足這兩點,它就是一個堆:數組
- 堆是一個徹底二叉樹;
- 堆中每個節點的值都必須大於等於(或小於等於)其子樹中每一個節點的值。
<b>說明:</b> 和紅黑樹不一樣,椎並是一個部分有序隊列,尤爲要注意如下兩點。數據結構
- 堆是徹底二叉樹,所以堆這種數據都是用<b>數組</b>進行存儲。
- 對於每一個節點的值都大於等於子樹中每一個節點值的堆,叫做<b>大頂堆</b>。反之則是<b>小頂堆</b>。
1.2 堆的常見操做
堆的常見操做兩個,分別是插入元素和刪除堆頂元素:性能
- 堆化(heapify):往堆中插入元素叫作堆化。堆化分爲從下往上和從上往下兩種堆化方法。
- 刪除堆頂元素:咱們知道,堆頂元素是最小或最大元素。刪除堆頂元素後,須要經過從上往下的堆化,使其從新知足堆的定義。
(1)堆化spa
咱們往一個小頂堆中添加新的結點,分析從下往上是如何進行堆化。固然,你也可使用從下往上的堆化。code
<center><img width=200 src='https://img2020.cnblogs.com/blog/1322310/202003/1322310-20200309122549671-1097358818.png'/></center>htm
如上圖所示,插入結點時從下往上是堆化爲兩步:
- 將數組最後位置添加一個新的結點,也就是 arr[size] = value。
- 從這個新結點和父結點依次向上比較並交換,直接從新符合堆的定義。其時間複雜度爲樹的高度,也就是 O(logn)。
private int[] arr; private int size; // arr[0] 不存儲任何元素,固然你也能夠將堆總體向前移動一位 public void add(int value) { if (size >= capcity) return; ++size; arr[size] = value; int i = size; while (i > 0 && arr[i / 2] > arr[i]) { swap(arr, i, i / 2); i = i / 2; } }
(2)刪除堆頂元素
與插入結點時相反,刪除元素時須要從上至下堆化
<center><img width=700 src='https://img2020.cnblogs.com/blog/1322310/202003/1322310-20200309122942037-853130731.png'/></center>
須要注意的是直接和子結點比較並交換位置,可能會出現數組空洞,不符合徹底二叉樹的定義,如右圖所示出現的數據空洞。解決方案如左所示,先將數組最後一位的結點交換到堆頂,而後再從上至下比較交換。
- 將數組最後結點賦值給椎頂結點,也就是 arr[1] = arr[size]。
- 從這個新結點和子結點依次向下比較並交換,直接從新符合堆的定義。其時間複雜度爲樹的高度,也就是 O(logn)。
public int poll() { if (size <= 0) return -1; int value = arr[1]; int i = 1; arr[i] = arr[size]; arr[size] = 0; size--; while (true) { int minPos = i; if (size >= 2 * i && arr[minPos] > arr[2 * i]) minPos = 2 * i; if (size >= 2 * i + 1 && arr[minPos] > arr[2 * i + 1]) minPos = 2 * i + 1; if (minPos == i) break; swap(arr, i, minPos); i = minPos; } return value; }
2. 堆排序
若是咱們要堆數據結構實現從小到大的排序,該怎麼實現呢?咱們知道將數組堆化成大頂堆後,堆頂是最大值,而後咱們依次取出堆頂元素,這樣取出的元素就是按從大到小的順序,咱們每次取出元素時依次放到數組最後。這樣當所有取出後,就實現了從小到大的排序。堆排序分爲兩步:
- 堆化:將數組原地建成一個堆。
- 排序:依次取出堆頂元素與數組最後一個元素交換位置。
2.1 堆化
原地堆化也有兩種思路:
- 從下往上進行堆化。和插入排序同樣,將數組分爲兩部爲:已經堆化和未堆化。依次遍歷未堆化部爲,將其插入到已經堆化部分。
- 從上往下進行堆化。遍歷全部的葉子結點,將其與堆頂結點交換後從上往下進行堆化。
// 從下往上堆化 private static void heavify(Integer[] arr) { for (int i = 1; i < arr.length; i++) { shiftUp(i, arr); } } // 大頂堆:從下往上堆化 private static void shiftUp(int i, Integer[] arr) { while (i > 0 && arr[i] > arr[(i - 1) / 2]) { swap(arr, i, (i - 1) / 2); i = (i - 1) / 2; } } // 從上往下堆化,對於徹底二叉樹而言,號子結點起始位置: "arr.length / 2 + 1" public void heavify(int[] arr) { for (int i = arr.length / 2; i >= 1; --i) { // 隨機數據的插入和刪除,堆的長度會發生變化,須要第二個參數來控制向下堆化的最大位置 shiftDown(i, arr.length, arr[i]); } } // 大頂堆:從上往下堆化 private static void shiftDown(int i, int size, Integer[] arr) { while (true) { int maxPos = i; if (size > 2 * i + 1 && arr[maxPos] < arr[2 * i + 1]) { maxPos = 2 * i + 1; } if (size > 2 * i + 2 && arr[maxPos] < arr[2 * i + 2]) { maxPos = 2 * i + 2; } if (maxPos == i) { break; } swap(arr, i, maxPos); i = maxPos; } }
<b>說明:</b> 最核心的方法是向上堆化 shiftUp 和 向下堆化 shiftDown 這兩個方法,能夠參考 PriorityQueue 小頂堆的實現。
2.2 排序
排序一樣是將數據分爲已經排序和未排序部分。其中未排序部分是一個大頂堆,依次從大頂堆中取出最大元素,插入已經排序部分。
public void sort(Integer[] arr) { heavify(arr); // 從大頂堆中取出最大元素,依次插入已經排序部分 doSort(arr); } private void doSort(Integer[] arr) { int n = arr.length; for (int i = 1; i < n; i++) { swap(arr, 0, n - i); shiftDown(0, n - i, arr); } }
2.3 三大指標
(1)時間複雜度
-
堆化時間複雜度:O(n)
層數 元素個數 時間複雜度 總時間複雜度 ○ 1 1 h-1 h-1 ○ ○ 2 2 h-2 2(h-2) ○ ○ ○ ○ 3 n/8 2 n/2^3 * 2 ○ ○ ○ ○ ○ ○ ○ ○ 4 n/4 1 n/2^2 * 1 ... h n/2 0 0 S = n/2 + 2n/4 + 3n/16 + 4n/32 + 2(h-2) + (h-1) = O(n)
-
排序時間複雜度:O(nlogn)
若是數據本來就是有序的,堆排序在堆化過程會打亂原先的順序,再進行排序,所以,即使是徹底有序數組的時間複雜度是 O(nlogn)。但對徹底逆序的數組,其時間複雜度也是 O(nlogn)。總的來講,堆排序的時間複雜度很是穩定。
(2)空間複雜度
堆排序是原地排序。
(3)穩定性
在堆化的過程當中,會出現非相鄰元素交換,所以堆排序是非穩定排序。
2.4 堆排序 vs 快速排序
在實際開發中,爲何快速排序要比堆排序性能好?
- 堆排序對 CPU 不友好。快速排序來講局部順序訪問,而堆排序則是逐層訪問。 好比,堆排序會依次訪問數組下標是 1,2,4,8 的元素,而不是像快速排序那樣,局部順序訪問。
- 堆排序算法的數據交換次數要多於快速排序。好比,有序數組建堆後,數據反而變得更無序。
3. 優先級隊列
如 Java 中的優先級隊列 PriorityQueue 和 PriorityBlockingQueue 就是使用<b>小頂堆</b>實現的。
- 合併有序小文件
- 高性能定時器
- Top N
- 利用堆求中位數
天天用心記錄一點點。內容也許不重要,但習慣很重要!