數(shù)據(jù)結(jié)構(gòu)與算法--優(yōu)先隊(duì)列和堆排序
在某些數(shù)據(jù)處理的例子中,總數(shù)據(jù)量太大,無法排序(甚至無法全部裝進(jìn)內(nèi)存)。例如,需要從十億個(gè)元素中選出最大的十個(gè),你真的想把一個(gè)10億規(guī)模的數(shù)組排序嗎?但有了優(yōu)先隊(duì)列,你只用一個(gè)能存儲十個(gè)元素的隊(duì)列即可。具體做法是讓元素一個(gè)個(gè)輸入,只要優(yōu)先隊(duì)列的個(gè)數(shù)大于10,就不斷刪除最小元素,最后優(yōu)先隊(duì)列長度不大于10時(shí)停止刪除,只剩10個(gè)自然就是所有元素中最大的10個(gè)了。
很多情況我們會收集一些元素,處理當(dāng)前鍵值最大(或最小)的元素,然后再收集更多的元素,再處理當(dāng)前最大的(或最小的)元素,這可以看成我們按照事件的優(yōu)先級順序來處理,生活中很多任務(wù)都是有優(yōu)先級高低之分的,所以優(yōu)先隊(duì)列可以高效地處理這些情況。
優(yōu)先隊(duì)列支持兩種操作:刪除最大元素(或最小元素)和插入元素。我們將看到,刪除最大元素的方法可以很簡單地轉(zhuǎn)換成刪除最小元素。
優(yōu)先隊(duì)列的初級實(shí)現(xiàn)
有幾種比較容易想到的思路。
1、基于無序數(shù)組(鏈表)
要刪除最大元素,我們可以在代碼中添加類似選擇排序的內(nèi)循環(huán),將最大元素和邊界元素交換,然后像pop方法那樣刪除它。
2、基于有序數(shù)組
另一種方法是在插入時(shí)就保證數(shù)組總是有序,像我們在符號表中基于有序數(shù)組的二分查找那樣,先確定新元素在數(shù)組中的排名,然后所有比它大的元素都要向右移動一格以騰出空間,新元素插入后仍然保持著數(shù)組有序。最大元素和最小元素都在數(shù)組邊界,刪除操作也就變得相當(dāng)簡單了。
采用以上兩種數(shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)很簡單,但是在插入元素和刪除最大元素這兩個(gè)操作之一在最壞情況下需要線性時(shí)間來完成。接下來要學(xué)習(xí)的二叉堆結(jié)構(gòu)能保證這兩種操作都能更快執(zhí)行。
堆的定義
在二叉堆的數(shù)組中,每個(gè)元素都要保證大于等于另外兩個(gè)特定位置的元素。相應(yīng)地,這些位置的元素又要大于等于數(shù)組中另外兩個(gè)元素,以此類推。這種結(jié)構(gòu)可以畫成二叉樹的樣子。當(dāng)一棵二叉樹的每個(gè)結(jié)點(diǎn)都大于等于它的兩個(gè)子結(jié)點(diǎn)時(shí),它被稱為堆有序。根結(jié)點(diǎn)是堆有序的二叉樹中的最大結(jié)點(diǎn)。這種堆稱為大頂堆。相反地,我們可以定義小頂堆。
用完全二叉樹表示二叉堆特別方便,如上圖所示,每個(gè)結(jié)點(diǎn)都大于等于它的兩個(gè)子結(jié)點(diǎn)。
用數(shù)組實(shí)現(xiàn)完全二叉樹也很簡單。因此我們可以直接用數(shù)組來實(shí)現(xiàn)二叉堆。具體方法是,數(shù)組的第一個(gè)位置a[0]不使用(為了計(jì)算簡單),根結(jié)點(diǎn)存在a[1],然后按照層級順序將二叉樹結(jié)點(diǎn)依次放入數(shù)組中。如下所示a[2]、a[3]是根結(jié)點(diǎn)的兩個(gè)子結(jié)點(diǎn),且都小于根結(jié)點(diǎn),而a[2]和a[3]的子結(jié)點(diǎn)分別在在數(shù)組中的位置4、5和6、7,以此類推。
可以觀察到規(guī)律:位置k的結(jié)點(diǎn),其左子結(jié)點(diǎn)的位置是2k,右子結(jié)點(diǎn)的位置是2k+1;其父結(jié)點(diǎn)的位置是?k/2?
。一棵大小為N的完全二叉樹的高度是?lg N?
。
我們希望在對堆的操作過程中(如插入元素和刪除最大元素)都保持著堆的狀態(tài)不被打破——即保持二叉堆中每個(gè)結(jié)點(diǎn)大于等于它兩個(gè)子結(jié)點(diǎn)的狀態(tài),為此每次插入元素或者刪除最大元素后都需要一些操作來恢復(fù)被打破的狀態(tài),這個(gè)過程稱為堆的有序化。堆的有序化是通過上浮和下沉兩個(gè)操作完成的。
由下至上的堆有序化——上浮
如果堆有序狀態(tài)因?yàn)槟硞€(gè)子結(jié)點(diǎn)比它的父結(jié)點(diǎn)更大而被打破,那么我們需要交換它和父結(jié)點(diǎn)的位置來恢復(fù)有序狀態(tài)。交換后這個(gè)結(jié)點(diǎn)比它的兩個(gè)子結(jié)點(diǎn)都大(一個(gè)是曾經(jīng)的父結(jié)點(diǎn),另外一個(gè)是原來父結(jié)點(diǎn)的子結(jié)點(diǎn),更小);如果交換后仍然比它的父結(jié)點(diǎn)大,就繼續(xù)交換,直到該結(jié)點(diǎn)不再大于它的父結(jié)點(diǎn)為止。位置k的父結(jié)點(diǎn)的位置是?k/2?
,記住這點(diǎn)就能輕松實(shí)現(xiàn),我們?yōu)檫@個(gè)方法起一個(gè)形象的方法名swim
(上浮)。
package Chap9;
public class MaxPQ<Key extends Comparable<Key>> {
private Key[] pq;
// 優(yōu)先隊(duì)列元素個(gè)數(shù)
private int N;
public MaxPQ() {
// pq[0]沒有使用
pq = (Key[]) new Comparable[2];
}
// 動態(tài)調(diào)整優(yōu)先隊(duì)列大小
private void resize(int max) {
Key[] temp = (Key[]) new Comparable[max];
// 有效值區(qū)間為[1, N]
for (int i = 1; i <= N; i++) {
temp[i] = pq[i];
}
pq = temp;
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
private void swim(int k) {
// k = 1說明當(dāng)前元素浮到了根結(jié)點(diǎn),它沒有父結(jié)點(diǎn)可以比較,也不能上浮了,所以k <= 1時(shí)候推出循環(huán)
while (k > 1 && less(k / 2, k)) {
swap(k/2, k);
// 上浮后,成為父結(jié)點(diǎn),所以下標(biāo)變成原父結(jié)點(diǎn)
k = k / 2;
}
}
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
private void swap(int i, int j) {
Key temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
}
我們順便定義了優(yōu)先隊(duì)列(基于大頂堆)的數(shù)據(jù)結(jié)構(gòu),用數(shù)組pq
表示二叉堆。重點(diǎn)看swim方法的實(shí)現(xiàn),和我們上面描述的操作完全一致,有一點(diǎn)注意while循環(huán)終止的條件中應(yīng)該是k <= 1
,因?yàn)閗 = 1時(shí)就說明當(dāng)前結(jié)點(diǎn)浮到了根結(jié)點(diǎn),它已經(jīng)不能再上浮了,而且根結(jié)點(diǎn)也沒有父結(jié)點(diǎn)和它比較。
由上至下的堆有序化——下沉
如果堆的有序狀態(tài)因?yàn)槟硞€(gè)結(jié)點(diǎn)變得比它的兩個(gè)子結(jié)點(diǎn)或者其中之一更小而被打破了,可以先找到子結(jié)點(diǎn)中的較大者(它也比父結(jié)點(diǎn)大),將它與父結(jié)點(diǎn)交換來恢復(fù)堆,和上面一樣,交換后可能還不是堆有序的狀態(tài),那就繼續(xù)交換,直到它不小于它的兩個(gè)子結(jié)點(diǎn)或者被交換到了堆的底部為止。位置為k的結(jié)點(diǎn)的左子結(jié)點(diǎn)和右子結(jié)點(diǎn)位置分別為2k 和2k + 1,由此可以寫出如下代碼,并為該方法起一個(gè)形象的方法名sink(下沉)。
private void sink(int k) {
// 父結(jié)點(diǎn)的位置k最大值為 N/2,若k有左子結(jié)點(diǎn)無右子結(jié)點(diǎn),那么2k = N;若兩個(gè)子結(jié)點(diǎn)都有,那么2k + 1 = N
// 有可能位置k只有左子結(jié)點(diǎn),依然要比較,用2k + 1 <= N這個(gè)的條件不會執(zhí)行比較,所以用2k <= N條件
while (2 * k <= N) {
int j = 2 * k;
// 可以取j = N -1,less(N -1, N);由于下標(biāo)從1開始,所以pq[N]是有元素的
if (j < N && less(j,j+1)) {
// 右子結(jié)點(diǎn)比左子結(jié)點(diǎn)大 ,取右子結(jié)點(diǎn)的下標(biāo)
j++;
}
// 左子結(jié)點(diǎn)或者右子結(jié)點(diǎn)和父結(jié)點(diǎn)比較
// 如果pq[k] >= pq[j],即父結(jié)點(diǎn)大于等于較大子結(jié)點(diǎn)時(shí),停止下沉
if (!less(k, j)) {
break;
}
// 否則交換
swap(k, j);
// 下沉后,下標(biāo)變成與之交換的元素下標(biāo)
k = j;
}
}
上浮和下沉的示意圖如下
有了上面作鋪墊,下面介紹插入和刪除最大元素就簡單多了。
插入和刪除最大元素
對于數(shù)組,在數(shù)組末端添加元素是最簡單的,為了維持堆有序的狀態(tài),添加到數(shù)組末端后,需要對其進(jìn)行上浮操作,將它放到合適的位置。
最大元素在堆頂pq[1],可以將其與數(shù)組中最后一個(gè)元素交換位置,因此最大元素到了數(shù)組末端,刪除就方便多了。之后為了維持堆的有序,記得對換到堆頂?shù)脑剡M(jìn)行下沉操作,將它放到合適的位置。
插入和刪除最大元素的操作示意圖如下。
代碼如下,實(shí)現(xiàn)中還添加了max()
方法,返回最大元素——堆的根結(jié)點(diǎn)pq[1]
public void insert(Key key) {
// 由于下標(biāo)從1開始算,存滿時(shí)N就等于pq.length -1
if (N == pq.length - 1) {
resize(pq.length * 2);
}
// 注意是++N,從pq[1]開始存
pq[++N] = key;
swim(N);
}
public Key max() {
if (isEmpty()) {
throw new NoSuchElementException("優(yōu)先隊(duì)列為空");
}
return pq[1];
}
public Key delMax() {
if (isEmpty()) {
throw new NoSuchElementException("優(yōu)先隊(duì)列為空");
}
Key max = pq[1];
// 第一個(gè)和最后一個(gè)元素交換
swap(1, N);
// 刪除位置N的元素,長度減1
pq[N--] = null;
// 刪除后要恢復(fù)堆有序狀態(tài)
sink(1);
if (N > 0 && N == pq.length / 4) {
resize(pq.length / 2);
}
return max;
}
delMax
就一定需要注意,sink(1);
的調(diào)用一定要在pq[N--] = null;
之前,確保下沉?xí)r候,堆中已經(jīng)沒有那個(gè)元素且N已經(jīng)減少1;否則,如果在刪除之前下沉,被交換過去的堆頂元素下沉后有可能又回到了被交換前的位置(即數(shù)組末端),之后再刪除就不是刪除最大元素了。
要是覺得一個(gè)個(gè)insert元素很麻煩,可以新增一個(gè)構(gòu)造方法,可以傳入一個(gè)數(shù)組,數(shù)組中所有元素傳遞給pq
,通過對pq中所有父結(jié)點(diǎn)進(jìn)行下沉操作,之后數(shù)組就是堆有序的了。
// 傳入數(shù)組,構(gòu)造有序堆
public MaxPQ(Key... keys) {
pq = (Key[]) new Comparable[keys.length +1];
N = keys.length;
for (int i = 0; i < N; i++) {
pq[i+1] = keys[i];
}
for (int k = N / 2; k >= 1 ; k--) {
sink(k);
}
}
上面代碼關(guān)鍵就在最后一個(gè)for循環(huán),k = N / 2
表示父結(jié)點(diǎn)的位置k最大值為 N/2,從這個(gè)結(jié)點(diǎn)開始由下至上對每個(gè)父結(jié)點(diǎn)進(jìn)行下沉操作,最后當(dāng)然是對根結(jié)點(diǎn)下沉,因此只要k >= 1
循環(huán)就持續(xù)。
稍微測試下,下面將依次打印5、4、3。
public static void main(String[] args) {
MaxPQ<Integer> maxPQ = new MaxPQ<>(4,3,1,2,5);
System.out.println(maxPQ.delMax());
System.out.println(maxPQ.delMax());
System.out.println(maxPQ.delMax());
}
最后來看一個(gè)完整的插入刪除操作,看看“堆”是怎么建立起來并維持著堆有序的狀態(tài)的。
可以刪除最小元素的MinPQ
MaxPQ轉(zhuǎn)換成MinPQ相當(dāng)簡單!
MinPQ需要使用小頂堆,小頂堆即所有父結(jié)點(diǎn)的左右子結(jié)點(diǎn)都要小于等于它,和大頂堆的定義是對稱的,所以只需將less
替換成greater
方法即可。
private boolean greater(int i, int j) {
return pq[i].compareTo(pq[j]) > 0;
}
就是將less中的<
改成大于>
,然后在所有使用less方法的地方換成greater就好了!MaxPQ的代碼比較分散,這里給出MinPQ的完整實(shí)現(xiàn)和測試。
package Chap9;
import java.util.NoSuchElementException;
public class MinPQ<Key extends Comparable<Key>> {
private Key[] pq;
// 優(yōu)先隊(duì)列元素個(gè)數(shù)
private int N;
public MinPQ() {
// pq[0]沒有使用
pq = (Key[]) new Comparable[2];
}
// 傳入數(shù)組,構(gòu)造有序堆
public MinPQ(Key... keys) {
pq = (Key[]) new Comparable[keys.length + 1];
N = keys.length;
for (int i = 0; i < N; i++) {
pq[i + 1] = keys[i];
}
for (int k = N / 2; k >= 1; k--) {
sink(k);
}
}
private void resize(int max) {
Key[] temp = (Key[]) new Comparable[max];
// 有效值區(qū)間為[1, N]
for (int i = 1; i <= N; i++) {
temp[i] = pq[i];
}
pq = temp;
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public void insert(Key key) {
// 由于下標(biāo)從1開始算,存滿時(shí)N就等于pq.length -1
if (N == pq.length - 1) {
resize(pq.length * 2);
}
// 注意是++N,從pq[1]開始存
pq[++N] = key;
swim(N);
}
public Key min() {
if (isEmpty()) {
throw new NoSuchElementException("優(yōu)先隊(duì)列為空");
}
return pq[1];
}
public Key delMin() {
if (isEmpty()) {
throw new NoSuchElementException("優(yōu)先隊(duì)列為空");
}
Key max = pq[1];
// 第一個(gè)和最后一個(gè)元素交換
swap(1, N);
// 刪除位置N的元素,長度減1
pq[N--] = null;
// 刪除后要恢復(fù)堆有序狀態(tài)
sink(1);
if (N > 0 && N == pq.length / 4) {
resize(pq.length / 2);
}
return max;
}
private void swim(int k) {
// k = 1說明當(dāng)前元素浮到了根結(jié)點(diǎn),它沒有父結(jié)點(diǎn)可以比較,也不能上浮了,所以k <= 1時(shí)候推出循環(huán)
while (k > 1 && greater(k / 2, k)) {
swap(k / 2, k);
// 上浮后,成為父結(jié)點(diǎn),所以下標(biāo)變成原父結(jié)點(diǎn)
k = k / 2;
}
}
private void sink(int k) {
// 父結(jié)點(diǎn)的位置k最大值為 N/2,若k有左子結(jié)點(diǎn)無右子結(jié)點(diǎn),那么2k = N;若兩個(gè)子結(jié)點(diǎn)都有,那么2k + 1 = N
// 有可能位置k只有左子結(jié)點(diǎn),依然要比較,用2k + 1 <= N這個(gè)的條件不會執(zhí)行比較,所以用2k <= N條件
while (2 * k <= N) {
int j = 2 * k;
// 可以取j = N -1,greater(N -1, N);由于下標(biāo)從1開始,所以pq[N]是有元素的
if (j < N && greater(j, j + 1)) {
// 右子結(jié)點(diǎn)比左子結(jié)點(diǎn)大 ,取右子結(jié)點(diǎn)的下標(biāo)
j++;
}
// 左子結(jié)點(diǎn)或者右子結(jié)點(diǎn)和父結(jié)點(diǎn)比較
// 如果pq[k] >= pq[j],即父結(jié)點(diǎn)大于等于較大子結(jié)點(diǎn)時(shí),停止下沉
if (!greater(k, j)) {
break;
}
// 否則交換
swap(k, j);
// 下沉后,下標(biāo)變成與之交換的元素下標(biāo)
k = j;
}
}
private boolean greater(int i, int j) {
return pq[i].compareTo(pq[j]) > 0;
}
private void swap(int i, int j) {
Key temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
public static void main(String[] args) {
MinPQ<Integer> minPQ = new MinPQ<>(4, 3, 1, 2, 5);
System.out.println(minPQ.delMin()); // 1
System.out.println(minPQ.delMin()); // 2
System.out.println(minPQ.delMin()); // 3
System.out.println("還剩" + minPQ.size() + "個(gè)"); // 2
System.out.println("當(dāng)前隊(duì)列最小元素:" + minPQ.min()); // 4
}
}
堆排序
用優(yōu)先隊(duì)列實(shí)現(xiàn)排序相當(dāng)簡單,一個(gè)比較容易想到的思路是:不斷刪除最小元素,并按順序填入原數(shù)組。如下
public static void main(String[] args) {
Integer[] a = {4, 3, 1, 2, 5};
MinPQ<Integer> minPQ = new MinPQ<>(a);
for (int i = 0; i < a.length; i++) {
a[i] = minPQ.delMin();
}
System.out.println(Arrays.toString(a)); // [1, 2, 3 ,4, 5]
}
這種做法當(dāng)然可以,但是需要創(chuàng)建MinPQ對象,引入了額外的空間,可不可以原地排序呢?
我們注意到,上面為MaxPQ寫過一個(gè)構(gòu)造方法,傳入一個(gè)數(shù)組,就能構(gòu)造成有序的堆,其實(shí)里面只用到了sink
方法!所以我們完全可以將sink方法提取出來,改成靜態(tài)方法。用基于大頂堆的優(yōu)先隊(duì)列,不斷將堆頂?shù)淖畲笤睾蛿?shù)組最后一個(gè)元素交換,然后對換到堆頂?shù)脑叵鲁?,如此反?fù),最大元素都按順序移動到了數(shù)組右邊,當(dāng)?shù)箶?shù)第二個(gè)元素的位置排定后,整個(gè)數(shù)組也就有序了。
package Chap9;
public class HeapSort {
public static void sort(Comparable[] a) {
// 堆的構(gòu)造
int N= a.length;
for (int k = N / 2; k >= 1; k--) {
sink(a, k, N);
}
// 最大元素交換到數(shù)組右邊
// 倒數(shù)第二個(gè)元素排定數(shù)組就已經(jīng)有序了,所以N = 1時(shí)只剩一個(gè)元素不用再操作了
while (N > 1) {
swap(a, 1, N--);
sink(a, 1, N);
}
}
private static void sink(Comparable[] a, int k, int N) {
// 父結(jié)點(diǎn)的位置k最大值為 N/2,若k有左子結(jié)點(diǎn)無右子結(jié)點(diǎn),那么2k = N;若兩個(gè)子結(jié)點(diǎn)都有,那么2k + 1 = N
// 有可能位置k只有左子結(jié)點(diǎn),依然要比較,用2k + 1 <= N這個(gè)的條件不會執(zhí)行比較,所以用2k <= N條件
while (2 * k <= N) {
int j = 2 * k;
// 可以取j = N -1,less(N -1, N);由于下標(biāo)從1開始,所以pq[N]是有元素的
if (j < N && less(a, j,j+1)) {
// 右子結(jié)點(diǎn)比左子結(jié)點(diǎn)大 ,取右子結(jié)點(diǎn)的下標(biāo)
j++;
}
// 左子結(jié)點(diǎn)或者右子結(jié)點(diǎn)和父結(jié)點(diǎn)比較
// 如果pq[k] >= pq[j],即父結(jié)點(diǎn)大于等于較大子結(jié)點(diǎn)時(shí),停止下沉
if (!less(a, k, j)) {
break;
}
// 否則交換
swap(a, k, j);
// 下沉后,下標(biāo)變成與之交換的元素下標(biāo)
k = j;
}
}
// 由于sort方法和sink方法都是從下標(biāo)1開始算,即認(rèn)為有元素的區(qū)間為[1, a.length],但實(shí)際上傳入的數(shù)組有元素區(qū)間為[0, a.length -1]
// 所以swap(a, p, q)實(shí)際交換的元素是a[p-1]和a[q -1]
private static void swap(Comparable[] a, int p, int q) {
Comparable temp = a[p-1];
a[p-1] = a[q-1];
a[q-1] = temp;
}
// // 由于sort方法和sink方法都是從下標(biāo)1開始算,即認(rèn)為有元素的區(qū)間為[1, a.length],但實(shí)際上傳入的數(shù)組有元素區(qū)間為[0, a.length -1]
// 所以less(a, i, j)實(shí)際比較的元素的是a[i-1]和a[j -1]
private static boolean less(Comparable[] a, int i, int j) {
return a[i-1].compareTo(a[j-1]) < 0;
}
// less方法變了,這個(gè)方法也得假定處理的區(qū)間是[1, a.length]
public static boolean isSorted(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
if (less(a, i + 1, i)) {
return false;
}
}
return true;
}
public static String toString(Comparable[] a) {
if (a.length == 0) {
return "[]";
}
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < a.length; i++) {
sb.append(a[i]);
if (i == a.length - 1) {
return sb.append("]").toString();
} else {
sb.append(", ");
}
}
return sb.toString();
}
public static void main(String[] args) {
Integer[] a = {9, 1, 5, 8, 3, 7, 4, 6, 2};
HeapSort.sort(a);
System.out.println(HeapSort.toString(a));
System.out.println(HeapSort.isSorted(a));
}
}
一定要注意一點(diǎn):sink方法直接沿用了上面優(yōu)先隊(duì)列的sink的實(shí)現(xiàn),而優(yōu)先隊(duì)列數(shù)組第一個(gè)位置a[0]是沒有使用的!sink方法認(rèn)為數(shù)組有元素的區(qū)間是[1, a.length],但實(shí)際上數(shù)組a的有元素區(qū)間是[0, a.length - 1],因此所有比較和交換方法中下標(biāo)都要減小1后再操作。比如sink方法中,swap(a, 2, 4),sink認(rèn)為是交換了第2個(gè)和第4個(gè)元素(a[0]沒有使用所以分別對應(yīng)a[2]、a[4]),但我們傳入的是普通數(shù)組,下標(biāo)是從0開始算的,所以實(shí)際交換的是a[1]和a[3]才對。
搞清楚了這個(gè)關(guān)系后,為了減少代碼改動,其實(shí)只需要在less和swap的內(nèi)部,下標(biāo)減1即可,傳入的參數(shù)還是不變(給sink方法看的),但實(shí)際上內(nèi)部比較的又是它們前面一位元素了。
下面是一個(gè)堆排序的軌跡圖,最左邊一列是對任意順序的數(shù)組進(jìn)行堆的構(gòu)造,堆有序后不通過下沉操作對數(shù)組排序。
下圖結(jié)合著上圖看,加深理解。下圖中標(biāo)紅的元素對應(yīng)著上面陰影部分中的下沉軌跡。紅色的元素參與了比較且被交換,黑色的元素參與了比較但沒有被交換,灰色的元素沒有參與比較。
堆排序總是將堆頂元素和數(shù)組末端元素交換,即使這兩個(gè)元素相等。在這種情況下,等值元素的相對位置一般發(fā)生了變化。所以堆排序是不穩(wěn)定的排序算法。
堆排序的時(shí)間復(fù)雜度為O(Nlg N),下表將各個(gè)排序算法的性能都做了比較。
by @sunhaiyu
2017.11.1