通俗易懂的讲解堆排序(含Gif图)
堆的定義
堆排序是一種樹形結(jié)構(gòu)選擇排序方法,其特點(diǎn)是:在排序過程中,將序列視為一顆完全二叉樹的順序存儲結(jié)構(gòu),利用完全二叉樹中雙親結(jié)點(diǎn)和孩子結(jié)點(diǎn)之間的關(guān)系,在當(dāng)前無序區(qū)間中選擇關(guān)鍵字最大(或最小)的元素。堆的定義如下:
n 個關(guān)鍵字序列 L[1,2,3...n] 稱為堆,當(dāng)且僅當(dāng)該序列滿足:
① L(i) <= L(2i) 且 L(i) <= L(2i+1)? ? ? ? ?② L(i) >= L(2i) 且 L(i) >= L(2i+1)? ? ? ? ? ? ? ? ? ?(1 <= i <= ?n/2?)
滿足第一種情況的堆稱為小根堆(或小頂堆),滿足第二種情況的堆稱為大根堆(大頂堆)。在大根堆中,最大的元素存放在根結(jié)點(diǎn)中。對其任一非根結(jié)點(diǎn),它的值小于等于其父親結(jié)點(diǎn)的值。小根堆的定義剛好相反,根結(jié)點(diǎn)是最小元素。建立一個堆有兩種方法,很多書籍把這兩種方法稱為向上調(diào)整和向下調(diào)整:
向下調(diào)整是讓調(diào)整的結(jié)點(diǎn)與其孩子結(jié)點(diǎn)進(jìn)行比較。
向上調(diào)整是讓調(diào)整的結(jié)點(diǎn)與其父親結(jié)點(diǎn)進(jìn)行比較。
?
向上調(diào)整
向上調(diào)整需要從根結(jié)點(diǎn)開始建堆(以小根堆為例),基本思路:每添加一個元素,將該元素與其父結(jié)點(diǎn)進(jìn)行比較,若新增結(jié)點(diǎn)小于父結(jié)點(diǎn),則交換兩個結(jié)點(diǎn)的值,然后繼續(xù)與上一級的父結(jié)點(diǎn)進(jìn)行比較。停止向上調(diào)整的條件是該結(jié)點(diǎn)大于父結(jié)點(diǎn)的值。現(xiàn)有序列 list [ ] = {87, 45, 78, 32, 17, 65, 53, 9} 分別用向上調(diào)整和向下調(diào)整進(jìn)行建堆,看看這兩種方法有什么區(qū)別。
向上調(diào)整建堆的過程:
添加元素 9 的向上調(diào)整 gif:
向上調(diào)整代碼:
void AdjustUp(int a[],int size) { //size是數(shù)組目前的大小int temp,flag=0;if(size==1)return; //此時是堆頂,不需要上調(diào)while(size!=1 && flag==0) {if(a[size] < a[size/2]) //與父結(jié)點(diǎn)進(jìn)行對比{temp = a[size];a[size] = a[size/2];a[size/2] = temp;}else{flag = 1;}size = size/2; //更新編號i為它父結(jié)點(diǎn)的編號。} }void AdjustUp1(int a[], int k) {//參數(shù)k為向上調(diào)整的結(jié)點(diǎn),也是堆的元素個數(shù)if(k == 1)return;a[0] = a[k];int i = k/2;while(i > 0 && a[i] > a[0]) //若結(jié)點(diǎn)值小于父結(jié)點(diǎn),則將父結(jié)點(diǎn)向下調(diào){a[k] = a[i]; //父節(jié)點(diǎn)向下調(diào)k = i;i = k/2; //繼續(xù)向上比較}a[k] = a[0]; }?
向下調(diào)整
向下調(diào)整建堆是一個反復(fù)篩查的過程。n 個結(jié)點(diǎn)的完全二叉樹,最后一個結(jié)點(diǎn)是第?n/2?個結(jié)點(diǎn)的孩子(完全二叉樹的性質(zhì))。對第?n/2?個結(jié)點(diǎn)為根的子樹篩查(對于小根堆,若結(jié)點(diǎn)的關(guān)鍵字大于左右孩子中關(guān)鍵字較小者,則交換),使該子樹成為堆。之后向前依次對各結(jié)點(diǎn)(?n/2?-1 ~ 1)為根的子樹進(jìn)行篩選,看該結(jié)點(diǎn)值是否小于其左右子結(jié)點(diǎn)的值,若不小于,則將左右子結(jié)點(diǎn)中的較小值與之交換,交換后可能會破壞下一級的堆,于是繼續(xù)采取上述方法構(gòu)造下一級的堆,直到以該結(jié)點(diǎn)為根的子樹構(gòu)成堆為止。反復(fù)利用上述調(diào)整方法建堆,直到根結(jié)點(diǎn)。根據(jù)原始數(shù)據(jù)建立的樹形結(jié)構(gòu)如下:
根據(jù)向下調(diào)整的思路,從第 4 個結(jié)點(diǎn)開始進(jìn)行篩查建立小根堆:
向下調(diào)調(diào)整思路:將該結(jié)點(diǎn)與其孩子結(jié)點(diǎn)進(jìn)行比較,選取其中最小進(jìn)行交換,若該節(jié)點(diǎn)本身就是最小的結(jié)點(diǎn),則不需要進(jìn)行交換操作。如上圖,需要將 9 和 32 進(jìn)行交換。然后對第 3 個結(jié)點(diǎn)進(jìn)行調(diào)整,該過程較為簡單,所以省略。接下來展示第 2?個結(jié)點(diǎn)的調(diào)整情況。
對第 2 個結(jié)點(diǎn)進(jìn)行調(diào)整時,需要交換 9 和 45。但是交換后發(fā)現(xiàn) 32 比 45 要小,所以還需要進(jìn)行一次交換操作。該過程告訴我們,在交換父子結(jié)點(diǎn)后,子結(jié)點(diǎn)的堆屬性可能被破壞,所以要對子結(jié)點(diǎn)進(jìn)行調(diào)整(直至子結(jié)點(diǎn)為葉子結(jié)點(diǎn)為止)。以下是第 2 個結(jié)點(diǎn)最終的調(diào)整情況:
向下調(diào)整的最終形態(tài):
向下調(diào)整 gif:
向下調(diào)整代碼:
void heap_ajust_min(int *b, int i, int size) //a為數(shù)組,size為堆的大小 {int lchild = 2*i; //i的左孩子節(jié)點(diǎn)序號 int rchild = 2*i +1; //i的右孩子節(jié)點(diǎn)序號 int min = i; //記錄根和左右子樹中最小的數(shù)的下標(biāo)int temp;if(i <= size/2) //調(diào)整不需要從葉結(jié)點(diǎn)開始{if(lchild<=size && b[lchild]<b[min]){min = lchild;} if(rchild<=size && b[rchild]<b[min]){min = rchild;} //兩個if語句尋找三個結(jié)點(diǎn)中最小的數(shù)if(min != i) //如果min不等于i,說明最小的值在左右子樹中{temp = b[i]; //交換a[i]和a[min]的值b[i] = b[min];b[min] = temp;heap_ajust_min(b, min, size); //被交換的子樹可能不滿足堆的定義,需要對被交換的子樹重新調(diào)整}} }void build_heap_min(int *b, int size) //建立小根堆 {int i;for(i = size/2; i >= 1; i--) //非葉子節(jié)點(diǎn)最大序號值為size/2,從這個結(jié)點(diǎn)開始調(diào)整{ //注意for中的循環(huán)條件(i = size/2; i >= 1; i--)heap_ajust_min(b, i, size); } }向下調(diào)整的時間與樹的高度有關(guān),為 O(h)。建堆過程中每次向下調(diào)整時,大部分結(jié)點(diǎn)的高度都比較小。我們很容易會發(fā)現(xiàn)向上調(diào)整和向下調(diào)整的最終結(jié)果有所不同,但都滿足了小根堆的性質(zhì)。向上調(diào)整和向下調(diào)整都能夠完成建立堆的工作。區(qū)別在于,使用向上調(diào)整建堆需要從第一個元素開始,過程相當(dāng)于每次插入一個元素,然后再進(jìn)行向上調(diào)整的工作。向下調(diào)整充分的利用了完全二叉樹的性質(zhì),從?n/2?結(jié)點(diǎn)到根結(jié)點(diǎn)逐一篩查。它們的具體操作如下:
for(k = 1; k < size ; ++k) //向上調(diào)整建堆 {AdjustUp(b, k); }for(i = size/2; i >= 1; i--) //向下調(diào)整建堆 {heap_ajust_min(b, i, size); }?
堆排序
應(yīng)用堆進(jìn)行排序的思路很簡單:首先將存放在 L[1,2,3...n] 中的 n 個元素建成初始堆,由于堆本身的特點(diǎn),堆頂元素就是最值。輸出堆頂元素后,通常將堆底元素送入堆頂,此時根結(jié)點(diǎn)不滿足堆的性質(zhì),經(jīng)過向下調(diào)整后使其恢復(fù)堆的性質(zhì),再輸出堆頂元素。重復(fù)操作,直至最后一個元素為止。
堆排序代碼:
void heap_sort_min(int *a, int size) {int i;int temp;for(i = size; i >= 1; i--){temp = a[1];a[1] = a[i];a[i] = temp; //交換堆頂和最后一個元素heap_ajust_min(a, 1, i-1); //再一次調(diào)整堆頂節(jié)點(diǎn)成為小頂堆} }利用上述小根堆代碼完成排序,會發(fā)現(xiàn)序列是倒序的,這是因為排序過程是從后往前不斷調(diào)整小根堆的結(jié)果。如果想要獲得升序序列,需要使用大根堆進(jìn)行排序。
堆支持刪除和插入操作,刪除元素時,需要將最后一個元素放到堆頂,然后使用向下調(diào)整,使堆保持堆的特性。對堆進(jìn)行插入操作時,先將新結(jié)點(diǎn)放在堆的末端,再對這個新結(jié)點(diǎn)進(jìn)行向上調(diào)整操作。注意:插入元素時(小根堆為例),只需要調(diào)用一次向上調(diào)整函數(shù)便能恢復(fù)堆的屬性。因為在插入之前,堆本身是完整的。此時若插入元素小于父結(jié)點(diǎn)的值,那么只需要交換兩個結(jié)點(diǎn)而不需要考慮別的情況。這是因為之前滿足堆性質(zhì),而插入了一個更小的值,交換后堆屬性不變。具體可以看添加 9 的時候的 gif。可以根據(jù)需求,將向上調(diào)整和向下調(diào)整搭配著使用。例如:小根堆刪除元素后使用向下調(diào)整恢復(fù)堆的性質(zhì)。
完整代碼:
#include<stdio.h> #include<math.h>void heap_ajust_min(int *b, int i, int size) /*a為堆存儲數(shù)組,size為堆的大小*/ {int lchild = 2*i; //i的左孩子節(jié)點(diǎn)序號 int rchild = 2*i +1; //i的右孩子節(jié)點(diǎn)序號 int min = i; /*存放三個頂點(diǎn)中最大的數(shù)的下標(biāo)*/int temp;if(i <= size/2) //假設(shè)i是葉節(jié)點(diǎn)就不用進(jìn)行調(diào)整 {if(lchild<=size && b[lchild]<b[min]){min = lchild;} if(rchild<=size && b[rchild]<b[min]){min = rchild;}if(min != i){temp = b[i]; /*交換a[i]和a[max]的值*/b[i] = b[min];b[min] = temp;heap_ajust_min(b, min, size); /*被交換的位置曾經(jīng)是大根堆,如今可能不是大根堆所以須要又一次調(diào)整使其成為大根堆結(jié)構(gòu)*/ }} }void build_bheap_min(int *b, int size) //建立小堆 {int i;for(i=size/2; i >= 1; i--) //非葉節(jié)點(diǎn)最大序號值為size/2{heap_ajust_min(b, i, size); } }void heap_sort_min(int *a, int size) {int i;int temp;for(i = size; i >= 1; i--){temp = a[1];a[1] = a[i];a[i] = temp; //交換堆頂和最后一個元素heap_ajust_min(a, 1, i-1); //再一次調(diào)整堆頂節(jié)點(diǎn)成為小頂堆} } void AdjustUp(int a[],int size) {int temp,flag=0;int k;if(size==1)return;//此時是堆頂,不需要上調(diào)while(size!=1 && flag==0){if(a[size] < a[size/2]){temp = a[size];a[size] = a[size/2];a[size/2] = temp;}else{flag = 1;}size = size/2;//更新編號i為它父結(jié)點(diǎn)的編號。} }void AdjustUp1(int a[], int k) {//參數(shù)k為向上調(diào)整的結(jié)點(diǎn),也是堆的元素個數(shù)if(k == 1)return;a[0] = a[k];int i = k/2;while(i > 0 && a[i] > a[0]) //若結(jié)點(diǎn)值小于父結(jié)點(diǎn),則將父結(jié)點(diǎn)向下調(diào){a[k] = a[i]; //父節(jié)點(diǎn)向下調(diào)k = i;i = k/2; //繼續(xù)向上比較}a[k] = a[0]; }int main(int argc, char *argv[]) { int i,j,k;int count=1;int a[]={0, 87, 45, 78, 32, 17, 65, 53, 9};int b[]={0, 87, 45, 78, 32, 17, 65, 53, 9};int size = sizeof(a)/sizeof(int) -1;build_bheap_min(a, size);printf("向下調(diào)整建立小頂堆:\n"); count=1;for(i=0;i<=4;i++){for(j=0;j<pow(2,i);j++){if(count<=size){printf("%d ",a[count++]);}else{break;}}printf("\n");}for(k = 1; k < 9 ; ++k){AdjustUp(b, k);}printf("向上調(diào)整小頂堆:\n"); count=1;for(i=0;i<=4;i++){for(j=0;j<pow(2,i);j++){if(count<=size){printf("%d ",b[count++]);}else{break;}}printf("\n");}return 0; }?
總結(jié)
以上是生活随笔為你收集整理的通俗易懂的讲解堆排序(含Gif图)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自信和优越感的区别
- 下一篇: Topk 问题详解及代码和数据分析