數(shù)據(jù)結(jié)構(gòu)與算法--優(yōu)先隊(duì)列和堆排序

數(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í)行。

image

堆的定義

在二叉堆的數(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)。這種堆稱為大頂堆。相反地,我們可以定義小頂堆。

image

完全二叉樹表示二叉堆特別方便,如上圖所示,每個(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,以此類推。

image

可以觀察到規(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;
    }
}

上浮和下沉的示意圖如下

image

有了上面作鋪墊,下面介紹插入和刪除最大元素就簡單多了。

插入和刪除最大元素

對于數(shù)組,在數(shù)組末端添加元素是最簡單的,為了維持堆有序的狀態(tài),添加到數(shù)組末端后,需要對其進(jìn)行上浮操作,將它放到合適的位置。

最大元素在堆頂pq[1],可以將其與數(shù)組中最后一個(gè)元素交換位置,因此最大元素到了數(shù)組末端,刪除就方便多了。之后為了維持堆的有序,記得對換到堆頂?shù)脑剡M(jìn)行下沉操作,將它放到合適的位置。

插入和刪除最大元素的操作示意圖如下。

image

代碼如下,實(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)的。

image
image

可以刪除最小元素的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ù)組排序。

image

下圖結(jié)合著上圖看,加深理解。下圖中標(biāo)紅的元素對應(yīng)著上面陰影部分中的下沉軌跡。紅色的元素參與了比較且被交換,黑色的元素參與了比較但沒有被交換,灰色的元素沒有參與比較。

image

堆排序總是將堆頂元素和數(shù)組末端元素交換,即使這兩個(gè)元素相等。在這種情況下,等值元素的相對位置一般發(fā)生了變化。所以堆排序是不穩(wěn)定的排序算法

堆排序的時(shí)間復(fù)雜度為O(Nlg N),下表將各個(gè)排序算法的性能都做了比較。

image

by @sunhaiyu

2017.11.1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容