看懂堆排序——堆與堆排序(三)
看懂堆排序——堆與堆排序(三)
有了前面兩篇文章的鋪墊,
我們終於可以學習“堆排序”了。
假使給定一個陣列a[N]
,其包含元素a[0]
,a[1]
,a[2]
,…,a[N-1]
,現要將其中元素按升序排列。如果利用堆這種資料結構,你會怎麼做?
堆排序的基本思想
很自然地想到,首先把此陣列構造成一個小根堆(利用原來的陣列,原地構造),然後依次刪除最小元素,直至堆為空。為了儲存每次刪除的最小元素,我們需要一個額外的陣列,待堆為空的時候,把額外陣列的內容拷貝到原來的陣列。
這種方法可行嗎?當然可行。但是需要一個額外的陣列,當陣列很大時,這個空間開銷是非常可觀的。避免使用額外的陣列的聰明做法是意識到這樣一個事實:在每次刪除最小元素之後,堆的規模縮小了1. 因此,位於堆中最後的那個單元可以用來存放剛剛刪去的元素。具體來說,堆排序的步驟如下:
- 為給定的序列建立一個堆(本文以最大堆為例)
- 交換堆的第一個元素
a[0]
和最後一個元素a[n-1]
- 堆的大小減
1
(--n
)。如果n==1
,演算法停止;否則,對a[0]
進行下濾 - 重複2~3步
程式碼詳解
父親下標和孩子下標的關係
因為待排序的陣列一般都是從0開始,而不是從1開始,所以之前討論的父節點和孩子節點之間的關係需要修改。
之前的是:
對於陣列任一位置 i
上的元素,其左兒子在位置 2i
上,右兒子在2i+1
上,它的父親則在位置
現在的是:
對於陣列任一位置 i
上的元素,其左兒子在位置 2i+1
上,右兒子在2i+2
上,它的父親則在位置
以節點 D 為例,D 的下標是 3.
B
是它的父節點,B
的下標是 1(=\lfloor (3-1)/2 \rfloor),如圖中黑色的線;H
是它的左孩子,H
的下標是 7(=2*3+1),如圖中藍色的線;I
是它的右孩子,I
的下標是 8(=2*3+2),如圖中紅色的線;
所以,我們的巨集定義是:
#define LEFT(i) (2*i+1)
#define RIGHT(i) (2*i+2)
#define PARENT(i) ((i-1)/2)
列印陣列的函式
void print_array_debug(int a[], int len, int pos, char token)
{
for(int i=0; i<len; ++i)
{
if( i == pos )
{
printf("%c %d ", token, a[i]); //列印元素值和記號
}
else
{
printf("%3d ",a[i]); //正常列印
}
}
printf("\n\n");
}
為了展示出排序的過程,我設計了這個函式。其實這個函式和普通的列印函式差不多,無非就是多了一個在某個元素前面列印一個標記的功能。比如要在a[0]
的前面列印一個'*'
,那麼可以這樣呼叫(假設陣列長度是10):
print_array_debug(a, 10, 0, '*');
如果不想用它的列印標記功能,則可以給pos
傳一個負數,給token
隨便什麼值都行。比如
#define DUMMY_POS -1
#define DUMMY_TOKEN '\0'
然後呼叫
print_array_debug(a, 10, DUMMY_POS, DUMMY_TOKEN);
下濾函式
對於給定的數列,我們首先要對其進行“堆化”,堆化的方法如下:
在初始化一棵包含 n 個節點的完全二叉樹時,按照給定的順序來放置鍵;
從最後一個父母節點開始,到根為止,檢查這些父母節點的鍵是否滿足父母優勢。如果該節點不滿足,就把該節點的鍵 K 和它子女的最大鍵進行交換,然後再檢查在新的位置上,K 是否滿足父母優勢。這個過程一直繼續到 K 滿足父母優勢為止(最終它必須滿足,因為對每個葉子中的鍵來說,這條件是自動滿足的)。
如果該節點不滿足父母優勢,就把該節點的鍵 K 和它子女的最大鍵進行交換,然後再檢查在新的位置上,K 是否滿足父母優勢。這個過程一直繼續到 K 滿足父母優勢為止——這種策略叫做下濾(percolate down)。
// 下濾函式(遞迴解法)
// 假定以 LEFT(t) 和 RIGHT(t) 為根的子樹都已經是大根堆
// 調整以 t 為根的子樹,使之成為大根堆。
// 節點位置為 [0], [1], [2], ..., [n-1]
void percolate_down_recursive(int a[], int n, int t)
{
int left = LEFT(t);
int right = RIGHT(t);
int max = t; //假設當前節點的鍵值最大
if(left < n) // 說明t有左孩子
{
max = a[left] > a[max] ? left : max;
}
if(right < n) // 說明t有右孩子
{
max = a[right] > a[max] ? right : max;
}
if(max != t)
{
swap(a + max, a + t); // 交換t和它的某個孩子,即t被換到了max位置
percolate_down_recursive(a, n, max); // 遞迴,繼續考察t
}
}
構造堆的函式
// 自底向上建堆,下濾法
void build_max_heap(element_t a[], int n)
{
int i;
// 從最後一個父母節點開始下濾,一直到根節點
for(i = PARENT(n); i >= 0; --i)
{
percolate_down_recursive(a, n, i);
}
}
刪除最大元函式
//把最大元素和堆末尾的元素交換位置,堆的規模減1,再下濾根節點
void delete_max_to_end(int heap[], int heap_size)
{
if(heap_size == 2) // 當剩下2個節點的時候,交換後不用下濾
{
swap( heap + 0, heap + 1 );
}
else if(heap_size > 2)
{
swap( heap + 0, heap + heap_size - 1 );
percolate_down_recursive(heap, heap_size-1, 0);
}
return;
}
排序主函式
void heap_sort(int a[], int length)
{
build_max_heap(a,length); //堆的構造
#ifdef PRINT_PROCEDURE
printf("build the max heap:\n");
print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
#endif
for(int size=length; size>=2; --size) //當堆的大小為1時,停止
{
delete_max_to_end(a,size);
#ifdef PRINT_PROCEDURE
print_array_debug(a, ELMT_NUM, size-1, '|');
#endif
}
}
完整程式碼及執行截圖
#include <stdio.h>
#define LEFT(i) (2*i+1)
#define RIGHT(i) (2*i+2)
#define PARENT(i) ((i-1)/2)
#define ELMT_NUM 10
#define DUMMY_POS -1
#define DUMMY_TOKEN '\0'
typedef int element_t;
void print_array_debug(int a[], int len, int pos, char token)
{
for(int i=0; i<len; ++i)
{
if( i == pos )
{
printf("%c %d ", token, a[i]); //列印元素值和記號
}
else
{
printf("%3d ",a[i]); //正常列印
}
}
printf("\n\n");
}
//交換*a和*b
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// 下濾函式(遞迴解法)
// 假定以 LEFT(t) 和 RIGHT(t) 為根的子樹都已經是大根堆
// 調整以 t 為根的子樹,使之成為大根堆。
// 節點位置為 [0], [1], [2], ..., [n-1]
void percolate_down_recursive(int a[], int n, int t)
{
int left = LEFT(t);
int right = RIGHT(t);
int max = t; //假設當前節點的鍵值最大
if(left < n) // 說明t有左孩子
{
max = a[left] > a[max] ? left : max;
}
if(right < n) // 說明t有右孩子
{
max = a[right] > a[max] ? right : max;
}
if(max != t)
{
swap(a + max, a + t); // 交換t和它的某個孩子,即t被換到了max位置
percolate_down_recursive(a, n, max); // 遞迴,繼續考察t
}
}
// 自底向上建堆,下濾法
void build_max_heap(element_t a[], int n)
{
int i;
// 從最後一個父母節點開始下濾,一直到根節點
for(i = PARENT(n); i >= 0; --i)
{
percolate_down_recursive(a, n, i);
}
}
//把最大元素和堆末尾的元素交換位置,堆的規模減1,再下濾根節點
void delete_max_to_end(int heap[], int heap_size)
{
if(heap_size == 2) // 當剩下2個節點的時候,交換後不用下濾
{
swap( heap + 0, heap + 1 );
}
else if(heap_size > 2)
{
swap( heap + 0, heap + heap_size - 1 );
percolate_down_recursive(heap, heap_size-1, 0);
}
return;
}
void heap_sort(int a[], int length)
{
build_max_heap(a,length);
#ifdef PRINT_PROCEDURE
printf("build the max heap:\n");
print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
#endif
for(int size=length; size>=2; --size)
{
delete_max_to_end(a,size);
#ifdef PRINT_PROCEDURE
print_array_debug(a, ELMT_NUM, size-1, '|');
#endif
}
}
int main(void)
{
int a[ELMT_NUM]={4,1,3,2,16,9,10,14,8,7}; //10個
printf("the array to be sorted:\n ");
print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
heap_sort(a,ELMT_NUM);
printf("sort finished:\n ");
print_array_debug(a,ELMT_NUM, DUMMY_POS, DUMMY_TOKEN);
}
假設檔名為heap_sort.c
,編譯:
gcc heap_sort.c -DPRINT_PROCEDURE
執行結果如下圖:
圖中豎線右邊的是已經有序的元素,豎線左邊是堆。
【本系列完】
參考資料
【1】《資料結構與演算法分析(原書第2版)》(機械工業出版社,2004)
【2】《演算法設計與分析基礎(第3版)》(清華大學出版社,2015)
相關文章
- 堆與堆排序(一)排序
- 堆操作與堆排序排序
- 線性建堆法與堆排序排序
- PHP 實現堆, 堆排序以及索引堆PHP排序索引
- 大根堆和堆排序的原理與實現排序
- 與堆和堆排序相關的問題排序
- 堆的基本操作及堆排序排序
- 二叉堆及堆排序排序
- 堆和堆的應用:堆排序和優先佇列排序佇列
- 《排序演算法》——堆排序(大頂堆,小頂堆,Java)排序演算法Java
- 堆排序排序
- 第三章:查詢與排序(下)----------- 3.16堆的概念及堆排序思路排序
- 高階資料結構---堆樹和堆排序資料結構排序
- 堆、堆排序和優先佇列的那些事排序佇列
- PHP面試:說下什麼是堆和堆排序?PHP面試排序
- 白話經典演算法系列之七 堆與堆排序演算法排序
- js堆排序JS排序
- [JAVA]堆排序Java排序
- 堆排序與優先佇列排序佇列
- 資料結構之堆 → 不要侷限於堆排序資料結構排序
- 《演算法筆記》4. 堆與堆排序、比較器詳解演算法筆記排序
- 五分鐘看懂一個高難度的排序:堆排序排序
- 堆排序詳解排序
- python 堆排序Python排序
- 堆排序 Heap Sort排序
- 堆排序(C++)排序C++
- HeapSort 堆排序排序
- 【筆記】堆排序筆記排序
- 實現堆排序排序
- 簡單堆排序排序
- js 實現堆排序JS排序
- 堆排序(php實現)排序PHP
- 堆排序演算法排序演算法
- 資料結構與演算法——堆排序資料結構演算法排序
- 【資料結構與演算法】堆排序資料結構演算法排序
- 資料結構與演算法:堆排序資料結構演算法排序
- 堆排序(實現c++)排序C++
- 排序演算法__堆排序排序演算法