28 | 堆和堆排序:为什么说堆排序没有快速排序快?
如何理解“堆”
堆排序是一種原地的、時間復雜度為 O(nlogn) 的排序算法
堆的兩個特點:
對于每個節點的值都大于等于子樹中每個節點值的堆,叫做“大頂堆”。對于每個節點的值都小于等于子樹中每個節點值的堆,叫做“小頂堆”。
如何實現一個“堆”
如何存儲一個堆
完全二叉樹比較適合用數組來存儲。用數組來存儲完全二叉樹是非常節省存儲空間的。因為不需要存儲左右子節點的指針,單純地通過數組的下標,就可以找到一個節點的左右子節點和父節點。
堆都支持哪些操作
堆中插入一個元素
插入元素放到最后,需要進行堆化操作:堆化實際上有兩種,從下往上和從上往下。這里我先講從下往上的堆化方法——順著節點所在的路徑,向上或者向下,對比,然后交換
public class Heap {private int[] a; // 數組,從下標1開始存儲數據private int n; // 堆可以存儲的最大數據個數private int count; // 堆中已經存儲的數據個數public Heap(int capacity) {a = new int[capacity + 1];n = capacity;count = 0;}public void insert(int data) {if (count >= n) return; // 堆滿了++count;a[count] = data;int i = count;while (i/2 > 0 && a[i] > a[i/2]) { // 自下往上堆化swap(a, i, i/2); // swap()函數作用:交換下標為i和i/2的兩個元素i = i/2;}}}刪除堆頂元素
思路一:刪除堆頂元素之后,就需要把第二大的元素放到堆頂,那第二大元素肯定會出現在左右子節點中。然后我們再迭代地刪除第二大節點,以此類推,直到葉子節點被刪除。此方法會出現數組空洞
思路二:把最后一個節點放到堆頂,然后利用同樣的父子節點對比方法。對于不滿足父子節點大小關系的,互換兩個節點,并且重復進行這個過程,直到父子節點之間滿足大小關系為止。這就是從上往下的堆化方法。
實現代碼:
public void removeMax() {if (count == 0) return -1; // 堆中沒有數據a[1] = a[count];--count;heapify(a, count, 1); }private void heapify(int[] a, int n, int i) { // 自上往下堆化while (true) {int maxPos = i;if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;if (maxPos == i) break;swap(a, i, maxPos);i = maxPos;} }如何基于堆實現排序?
這種排序方法的時間復雜度非常穩定,是 O(nlogn),并且它還是原地排序算法。堆排序的過程分為2步驟:
1、建堆
思路一:數組中包含 n 個數據,但是我們可以假設,起初堆中只包含一個數據,就是下標為 1 的數據。然后,我們調用前面講的插入操作,將下標從 2 到 n 的數據依次插入到堆中。這樣我們就將包含 n 個數據的數組,組織成了堆。此方法從前往后處理數組數據,并且每個數據插入堆中時,都是從下往上堆化。不推薦
思路二:從后往前處理數組,并且每個數據都是從上往下堆化。以下圖示:
實現代碼
private static void buildHeap(int[] a, int n) {for (int i = n/2; i >= 1; --i) {heapify(a, n, i);} }private static void heapify(int[] a, int n, int i) {while (true) {int maxPos = i;if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;if (maxPos == i) break;swap(a, i, maxPos);i = maxPos;} }對下標從 2n? 開始到 1 的數據進行堆化,下標是 2n?+1 到 n 的節點是葉子節點,我們不需要堆化。實際上,對于完全二叉樹來說,下標從 2n?+1 到 n 的節點都是葉子節點。
建堆的時間復雜度就是 O(n)。
2、排序
- 建堆結束之后,數組中的數據已經是按照大頂堆的特性來組織的。數組中的第一個元素就是堆頂,也就是最大的元素。我們把它跟最后一個元素交換,那最大元素就放到了下標為 n 的位置。
- 這個過程有點類似上面講的“刪除堆頂元素”的操作,當堆頂元素移除之后,我們把下標為 n 的元素放到堆頂,然后再通過堆化的方法,將剩下的 n?1 個元素重新構建成堆。堆化完成之后,我們再取堆頂的元素,放到下標是 n?1 的位置,一直重復這個過程,直到最后堆中只剩下標為 1 的一個元素,排序工作就完成了。
堆排序過程代碼:
// n表示數據的個數,數組a中的數據從下標1到n的位置。 public static void sort(int[] a, int n) {buildHeap(a, n);int k = n;while (k > 1) {swap(a, 1, k);--k;heapify(a, k, 1);} }分析一下堆排序的時間復雜度、空間復雜度以及穩定性
1、整個堆排序的過程,都只需要極個別臨時存儲空間,所以堆排序是原地排序算法。
2、堆排序包括建堆和排序兩個操作,建堆過程的時間復雜度是 O(n),排序過程的時間復雜度是 O(nlogn),所以,堆排序整體的時間復雜度是 O(nlogn)。
3、堆排序不是穩定的排序算法,因為在排序的過程,存在將堆的最后一個節點跟堆頂節點互換的操作,所以就有可能改變值相同數據的原始相對順序。
解釋:在前面的講解以及代碼中,我都假設,堆中的數據是從數組下標為 1 的位置開始存儲。那如果從 0 開始存儲,實際上處理思路是沒有任何變化的,唯一變化的,可能就是,代碼實現的時候,計算子節點和父節點的下標的公式改變了。如果節點的下標是 i,那左子節點的下標就是 2?i+1,右子節點的下標就是 2?i+2,父節點的下標就是 2i?1?。
解答標題
第一點,堆排序數據訪問的方式沒有快速排序友好。對于快速排序來說,數據是順序訪問的。而對于堆排序來說,數據是跳著訪問的。堆排序中,最重要的一個操作就是數據的堆化。比如下面這個例子,對堆頂節點進行堆化,會依次訪問數組下標是 1,2,4,8 的元素,而不是像快速排序那樣,局部順序訪問,所以,這樣對 CPU 緩存是不友好的。
第二點,對于同樣的數據,在排序過程中,堆排序算法的數據交換次數要多于快速排序、
?
?
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的28 | 堆和堆排序:为什么说堆排序没有快速排序快?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 18 | 散列表(上):Word文档中的
- 下一篇: OPT和LRU页面置换算法C语言代码,页