堆排序
圖片和描述轉載於https://www.cnblogs.com/lanhaicode/p/10546257.html,代碼是根據該描述以自己的理解自己實現的。
1、什麼是堆?
堆是一種非線性結構,(本篇隨筆主要分析堆的數組實現)可以把堆看作一個數組,也可以被看作一個完全二叉樹,通俗來講堆其實就是利用完全二叉樹的結構來維護的一維數組
按照堆的特點可以把堆分爲大頂堆和小頂堆
大頂堆:每個結點的值都大於或等於其左右孩子結點的值
小頂堆:每個結點的值都小於或等於其左右孩子結點的值
(堆的這種特性非常的有用,堆常常被當做優先隊列使用,因爲可以快速的訪問到“最重要”的元素)
2、堆的特點(數組實現)
(圖片來源:https://www.cnblogs.com/chengxiao/p/6129630.html)
我們對堆中的結點按層進行編號,將這種邏輯結構映射到數組中就是下面這個樣子
(圖片來源:https://www.cnblogs.com/chengxiao/p/6129630.html)
我們用簡單的公式來描述一下堆的定義就是:(讀者可以對照上圖的數組來理解下面兩個公式)
大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
3、堆和普通樹的區別
內存佔用:
普通樹佔用的內存空間比它們存儲的數據要多。你必須爲節點對象以及左/右子節點指針分配額外的內存。堆僅僅使用數組,且不使用指針
(可以使用普通樹來模擬堆,但空間浪費比較大,不太建議這麼做)
平衡:
二叉搜索樹必須是“平衡”的情況下,其大部分操作的複雜度才能達到O(nlog2n)。你可以按任意順序位置插入/刪除數據,或者使用 AVL 樹或者紅黑樹,但是在堆中實際上不需要整棵樹都是有序的。我們只需要滿足對屬性即可,所以在堆中平衡不是問題。因爲堆中數據的組織方式可以保證O(nlog2n) 的性能
搜索:
在二叉樹中搜索會很快,但是在堆中搜索會很慢。在堆中搜索不是第一優先級,因爲使用堆的目的是將最大(或者最小)的節點放在最前面,從而快速的進行相關插入、刪除操作
4、堆排序的過程
先了解下堆排序的基本思想:
將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就爲最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值,
如此反覆執行,便能得到一個有序序列了,建立最大堆時是從最後一個非葉子節點開始從下往上調整的(這句話可能不好太理解),下面會舉一個例子來理解堆排序的基本思想
給一個無序序列如下
int a[6] = {7, 3, 8, 5, 1, 2};
現在可以根據數組將完全二叉樹還原出來
好了,現在我們要做的事情就是要把7,3,8,5,1,2變成一個有序的序列,如果想要升序就是1,2,3,5,7,8 如果想要降序就是8,7,5,3,2,1 ,這兩種就是我們要的最終結果,然後我們就可以根據我們想要的結果來選擇
適合類型的堆來進行排序
升序----使用大頂堆
降序----使用小頂堆
5、爲什麼升序要用大頂堆呢
上面提到過大頂堆的特點:每個結點的值都大於或等於其左右孩子結點的值,我們把大頂堆構建完畢後根節點的值一定是最大的,然後把根節點的和最後一個元素(也可以說最後一個節點)交換位置,那麼末尾元素此時就是最大元素了(理解這點很重要)
知道了堆排序的原理下面就可以來操作了,在進行操作前先理清一下步驟
(假設我們想要升序的排列)
第一步:先n個元素的無序序列,構建成大頂堆
第二步:將根節點與最後一個元素交換位置,(將最大元素"沉"到數組末端)
第三步:交換過後可能不再滿足大頂堆的條件,所以需要將剩下的n-1個元素重新構建成大頂堆
第四步:重複第二步、第三步直到整個數組排序完成
6、圖解交換過程(得到升序序列,使用大頂堆來調整)
這裏以int a[6] = {7, 3, 8, 5, 1, 2}爲例子
先要找到最後一個非葉子節點,數組的長度爲6,那麼最後一個非葉子節點就是:長度/2-1,也就是6/2-1=2,然後下一步就是比較該節點值和它的子樹值,如果該節點小於其左\右子樹的值就交換(意思就是將最大的值放到該節點)
8只有一個左子樹,左子樹的值爲2,8>2不需要調整
下一步,繼續找到下一個非葉子節點(其實就是當前座標-1就行了),該節點的值爲3小於其左子樹的值,交換值,交換後該節點值爲5,大於其右子樹的值,不需要交換
下一步,繼續找到下一個非葉子節點,該節點的值爲7,大於其左子樹的值,不需要交換,再看右子樹,該節點的值小於右子樹的值,需要交換值
下一步,檢查調整後的子樹,是否滿足大頂堆性質,如果不滿足則繼續調整(這裏因爲只將右子樹的值與根節點互換,只需要檢查右子樹是否滿足,而8>2剛好滿足大頂堆的性質,就不需要調整了,
如果運氣不好整個數的根節點的值是1,那麼就還需要調整右子樹)
到這裏大頂堆的構建就算完成了,然後下一步交換根節點(8)與最後一個元素(2)交換位置(將最大元素"沉"到數組末端),此時最大的元素就歸位了,然後對剩下的5個元素重複上面的操作
(這裏用粉紅色來表示已經歸位的元素)
剩下只有5個元素,最後一個非葉子節點是5/2-1=1,該節點的值(5)大於左子樹的值(3)也大於右子樹的值(1),滿足大頂堆性質不需要交換
找到下一個非葉子節點,該節點的值(2)小於左子樹的值(5),交換值,交換後左子樹不再滿足大頂堆的性質再調整左子樹,左子樹滿足要求後再返回去看根節點,根節點的值(5)小於右子樹的值(7),再次交換值
得到新的大頂堆,如下圖,再把根節點的值(7)與當前數組最後一個元素值(1)交換,再重構大頂堆->交換值->重構大頂堆->交換值····,直到整個數組都變成有序序列
最後得到的升序序列如下圖
7、大頂堆排序的代碼實現
#include <stdio.h>
void Swap(int *heap, int i,int j); /* 交換元素的值 */
void BuildMaxHeap(int *heap, int len);/* 構建大頂堆 */
int main()
{
int a[] = {7, 3, 8, 5, 1, 2};
int len = sizeof(a)/sizeof(int); /* 數組長度 */
int i,j;
for (i = len; i > 0; i--)// 需要構建len次。
{
BuildMaxHeap(a, i);
Swap(a, 0,i-1);//構建完成一次後,將,堆頂和最後一個元素交換位置,此時,最後一個元素爲最大值,下一次不需要處理最後一個元素了。
}
for (i = 0; i < len; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
int HeapAdjust(int *heap,int i,int len)
{
int l,r,max;
max =i;
l=2*i+1;
r=2*i+2;
if (l < len && heap[max] < heap[l]) /* 根節點大於左子樹,構建小頂堆只需修改比較條件 */
{
min =l;
}
if (r < len && heap[max] < heap[r]) /* 根節點大於右子樹,構建小頂堆只需修改比較條件 */
{
max =r;
}
if(max !=i)
{
Swap(heap,max,i);
HeapAdjust(heap,max,len);//交換完以後,二叉樹下面的節點可能又變成無序的了,因此遞歸向下調整一下。
}
}
/* Function: 構建大頂堆 */
void BuildMaxHeap(int *heap,int len)
{
int i;
for (i = len/2-1; i >= 0; i--)//從最後一個非葉子節點開始。進行堆調整。
{
HeapAdjust(heap,i,len);
}
}
/* Function: 交換交換根節點和數組末尾元素的值*/
void Swap(int *heap, int i,int j)
{
int temp;
temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}