選擇排序時,採用遍歷的方式在n個元素中找最大 (小) 的元素,時間複雜度是o(n),這是乙個非常慢的操作。如果把這個操作的時間複雜度降低到o(logn),排序速度就會明顯變快。這就是選擇排序的高階形式:堆排序。下面我們從堆的前世今生開始講。
在前面我們已經學過完全二叉樹了,我們在完全二叉樹的基礎上提出「大頂堆」和「小頂堆」的概念。這裡我們主要討論「大頂堆」:「大頂堆」是一棵完全二叉樹,而且堆頂元素一定比兩個兒子 (如果存在的話) 都大 (或相等),但是兩個兒子之間沒有大小的要求。例如下圖中a圖是乙個大頂堆,b圖不是完全二叉樹、c圖中有乙個子樹不符合大頂堆的定義,所以b圖和c圖不是大頂堆。
有了大頂堆的定義之後,我們想知道它有什麼用。
可以看到:大頂堆中堆頂元素是整個堆中最大的元素,如果把堆頂元素去掉,並且把最後乙個元素放到堆頂,我們最多隻需要進行logn次 (n個元素的堆一共有 logn + 1層,簡化後記作logn) 調整,就可以讓所有元素重新符合大頂堆的定義,新堆的元素個數比原來少1。
用「堆」的方式尋找最大 (小) 值的時間複雜度為o(logn),把這種利用「堆」尋找最小 (大) 值的方式用在選擇排序中,那麼選擇排序的效率就可以從o(n2) 提高到o(n * logn)。這種改進方式就是堆排序。要實現堆排序,我們需要解決下面幾個問題:
1、堆的儲存方式。
由於堆是一棵完全二叉樹,可以用一維陣列儲存,所有元素都是挨在一起的,中間沒有空位,這種儲存方式非常緊湊,不浪費空間。遍歷這個一維陣列就是用層序方式遍歷堆。
2、怎麼建大頂堆?
建大頂堆有兩種方式:① 從乙個空的堆開始,乙個乙個增加資料,直到所有資料都新增完成。這種方式每增加乙個資料都要進行o(logn)次調整,所以建堆的時間複雜度為o(n * logn);② 直接對所有的資料進行調整得到堆,這種建堆的時間複雜度為o(n)。顯然第二種方式建堆更快。在本章中,我們只討論第二種方式。第一種方式在優先順序佇列中很有用,優先順序佇列的內容我們將在擴充套件篇中討論。
大頂堆的定義要求:堆頂元素一定比兩個兒子 (如果存在的話) 都大 (或相等)。如果堆頂元素的編號是i,那麼它的左兒子的編號是2 * i + 1,右兒子的編號是2 * i + 2 (如果存在的話)。接下來,我們就只需要在這三個元素中找到最大值即可,可以用乙個getmaxindex()函式實現這個功能:
三個引數分別是是father和兩個兒子的編號。father一定存在,但兩個兒子不一定存在。
int
getmaxindex
(int father,
int leftson,
int rightson)
if(rightson < size && arr[maxindex]
< arr[rightson]
)//rightson < size表示右兒子存在
return maxindex;
//返回最大值的編號
}
找到這個最大值之後,需要跟父節點比較一下,如果父節點本來就是最大值,那就說明這三個數符合大頂堆的要求,就不需要調整了。否則就需要調整。怎麼調整?很簡單,就是把最大的那個值跟父節點交換一下即可:
swap
(father, maxindex);
但是以maxindex這個結點作為子堆的堆頂元素之後,子堆可能又不符合要求了,那就繼續用相同的方式調整吧。看到「相同的方式」這幾個字,我們自然而然的想到了用遞迴:
void
shiftdown
(int i)
//向下調整。i是父節點的編號
//如果father是最大的值,符合堆的定義,退出遞迴
else
//遞迴的出口
}
也可以用迭代的方式進行維護,這樣就可以避免遞迴造成的一些問題:
void
shiftdown
(int i)
//向下調整。i是父節點的編號
else
//符合堆的定義時,退出迴圈
}}
我們要想一想從**開始維護堆?對於葉子結點而言,它沒有子節點,所以沒有必要對葉子結點進行維護。最後乙個有子節點的元素的編號是 (n - 1) / 2。我們可以從最後乙個有子節點的元素開始維護,反著進行,第乙個元素最後維護。部分資料中,是從陣列編號為1的位置開始存放資料的,這時最後乙個有子節點的元素的編號是n / 2。為了相容這種方式,我們也從編號為n / 2的元素開始維護。這樣做可能會多維護一次,但不影響結果。
for
(int i = size /
2; i >=
0; i--
)
3、我們已經能建立大頂堆了,也知道怎麼維護堆了。也知道首元素 (堆頂元素) 是最大值了,接下來怎麼進行堆排序呢?
在完成上面的準備工作之後,我們只需要把堆頂元素與最後的元素進行交換,然後從堆頂開始重新維護這個大頂堆,同時size–,避免下一次把最大的元素放進堆中進行調整。這樣,隨著size逐漸變小,堆也逐漸變小,每次找到的最大值也變小。最後得到乙個從小到大排序的陣列。這就是堆排序。
下面是堆排序的完整**:
#include
#include
int arr=
;int size =0;
intswap
(int a,
int b)
//三個引數分別是是father和兩個兒子的編號。father一定存在,但兩個兒子不一定存在。
intgetmaxindex
(int father,
int leftson,
int rightson)
if(rightson < size && arr[maxindex]
< arr[rightson]
)//rightson < size表示右兒子存在
return maxindex;
//返回最大值的編號
}void
shiftdown
(int i)
//向下調整。i是父節點的編號
else
//符合堆的定義時,退出迴圈 }}
void
createmaxheap()
//建立大頂堆
}void
heapsort()
//堆排序
size = size2;
//恢復size
}int
main()
下面分析一下堆排序的時間複雜度。堆排序消耗的時間分為兩部分:1、建堆過程,時間複雜度為o(n);2、排序過程:找到乙個最大值後需要進行logn次調整才能使整個堆重新符合大頂堆的定義,一共n個元素,所以排序過程的時間複雜度為:o(n * logn)。整個堆排序的時間複雜度為:o(n + n * logn) = o(n * logn)。正是因為堆排序中存在乙個額外的建堆過程,所以相對於其它高階排序演算法,堆排序是最慢的。
值得注意的是,堆排序在任何情況下的時間複雜度都是o(n * logn),且它只需要1個單位的輔助空間即可完成排序。可以說是非常節約記憶體的一種排序方式。這個排序演算法在記憶體非常緊張的裝置 (例如52微控制器只有256個位元組的記憶體空間) 中非常有用,而且堆排序還可以進行部分排序,這也是非常特殊的能力。另外,堆排序是跳躍式交換資料的,所以它是一種不穩定的排序演算法。
堆除了用於排序之外,還有很多其它用處,比如實現優先順序佇列、用於優化尋找最大 (小) 值、用於尋找最大 (小) 的第k個值,或前k個值、求中值等,這些內容我們將在擴充套件篇中繼續討論。
下一小節,我們繼續討論堆排序的一種改進方式。
c語言 實現堆排序演算法
今天在 演算法導論 第二版看完了 堆排序 演算法,就順便用c語言實現了一下。堆排序演算法的核心思想,使用一種二叉堆的資料結構來儲存資料,其中二叉堆 最小二叉堆 的主要性質為 1 父節點小於所有的子節點的數值 注 最小堆 2 二叉堆為滿二叉樹 其中堆排序演算法,主要包括一下幾個主要的部分 1 保持堆特...
排序演算法的C語言實現 堆排序
堆 優先佇列 可以用於花費nlogn 時間的排序,基於該想法的演算法叫做堆排序。因為堆的根總是最大的或者最小的,所以我們可以先將輸入陣列轉換為最大或者最小堆,然後刪除最大 最小值 也就是刪除根。這在二叉堆的介紹中已經實現了。一種方法是將刪除的元素放入另乙個陣列,但是這樣會浪費一倍的記憶體空間。由於每...
經典排序演算法 堆排序(C語言實現)
堆排序的基本原理為將待排序序列構造成乙個大根堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然後將剩餘n 1個元素重新構造成乙個大根堆,重複上述操作,最終序列為有序。備註 大根堆是每個結點的值都大於或等於其左右孩子結點的值 小根堆是每個結點的值都小於或等於其左...