堆排序

1:堆

毫無疑問,排序兩個字沒必要去死磕,這裏的重點,在於排序的方式,堆排序,就是以堆的形式去排序,毫無疑問,瞭解堆很重要。

那麼,什麼是堆呢?

這裏,必須引入一個完全二叉樹的概念,然後過渡到堆的概念。

上圖,就是一個完全二叉樹,其特點在於:

  1. 從作爲第一層的根開始,除了最後一層之外,第N層的元素個數都必須是2的N次方;第一層2個元素,第二層4個,第三層8個,以此類推。
  2. 而最後一行的元素,都要緊貼在左邊,換句話說,每一行的元素都從最左邊開始安放,兩個元素之間不能有空閒,具備了這兩個特點的樹,就是一棵完全二叉樹。

那麼,完全二叉樹與堆有什麼關係呢?

我們假設有一棵完全二叉樹,在滿足作爲完全二叉樹的基礎上,對於任意一個擁有父節點的子節點,其數值均不小於父節點的值;這樣層層遞推,就是根節點的值最小,這樣的樹,稱爲小根堆。

同理,又有一棵完全二叉樹,對於任意一個子節點來說,均不大於其父節點的值,如此遞推,就是根節點的值是最大的,這樣的數,稱爲大根堆。

如上圖,左邊就是大根堆;右邊則是小根堆,這裏必須要注意一點,只要求子節點與父節點的關係,兩個節點的大小關係與其左右位置沒有任何關係。

明確下大根堆,小根堆的概念,繼續說堆排序。

現在對於堆排序來說,我們先要做的是,把待排序的一堆無序的數,整理成一個大根堆,或者小根堆,下面討論以大根堆爲例子。

給定一個列表array=[16,7,3,20,17,8],對其進行堆排序(使用大根堆)。

接下來內容是轉載部分,自己繪圖功底太差:其中綠色部分爲自己的註解。

步驟一 構造初始堆。將給定無序序列構造成一個大頂堆(一般升序採用大頂堆,降序採用小頂堆)。

  a.假設給定無序序列結構如下

2.此時我們從最後一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。

此處必須注意,我們把6和9比較交換之後,必須考量9這個節點對於其子節點會不會產生任何影響?因爲其是葉子節點,所以不加考慮;但是,一定要熟練這種思維,寫代碼的時候就比較容易理解爲什麼會出現一次非常重要的交換了。

4.找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。

在真正代碼的實現中,這時候4和9交換過後,必須考慮9所在的這個節點位置,因爲其上的值變了,必須判斷對其的兩個子節點是否造成了影響,這麼說不合適,實際上就是判斷其作爲根節點的那棵子樹,是否還滿足大根堆的原則,每一次交換,都必須要循環把子樹部分判別清楚。

這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。

牢記上面說的規則,每次交換都要把改變了的那個節點所在的樹重新判定一下,這裏就用上了,4和9交換了,變動了的那棵子樹就必須重新調整,一直調整到符合大根堆的規則爲截。

此時,我們就將一個無序序列構造成了一個大頂堆。

步驟二 將堆頂元素與末尾元素進行交換,使末尾元素最大。然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反覆進行交換、重建、交換。

a.將堆頂元素9和末尾元素4進行交換

這裏,必須說明一下,所謂的交換,實際上就是把最大值從樹裏面拿掉了,剩下參與到排序的樹,其實只有總結點的個數減去拿掉的節點個數了。所以圖中用的是虛線。

b.重新調整結構,使其繼續滿足堆定義

c.再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.

後續過程,繼續進行調整,交換,如此反覆進行,最終使得整個序列有序

下面,附上我的代碼,也是從文末鏈接中模仿過來的,但是親自敲過一遍,印象深刻。

public class HeapSort {
	public static void main(String[] args) {
		int[] array = new int[] { 2, 1, 4, 3, 6, 5, 8, 7 };
		// 接下來就是排序的主體邏輯
		sort(array);
		System.out.println(Arrays.toString(array));
	}
 
	/**
	 * 
	 * @description 本方法只有一個參數,那就是待排序的array
	 * @author
	 * @param
	 * @return
	 * @time 2018年3月9日 下午2:24:45
	 */
	public static void sort(int[] array) {
		// 按照完全二叉樹的特點,從最後一個非葉子節點開始,對於整棵樹進行大根堆的調整
		// 也就是說,是按照自下而上,每一層都是自右向左來進行調整的
		// 注意,這裏元素的索引是從0開始的
		// 另一件需要注意的事情,這裏的建堆,是用堆調整的方式來做的
		// 堆調整的邏輯在建堆和後續排序過程中複用的
		for (int i = array.length / 2 - 1; i >= 0; i--) {
			adjustHeap(array, i, array.length);
		}
 
		// 上述邏輯,建堆結束
		// 下面,開始排序邏輯
		for (int j = array.length - 1; j > 0; j--) {
			// 元素交換
			// 說是交換,其實質就是把大頂堆的根元素,放到數組的最後;換句話說,就是每一次的堆調整之後,都會有一個元素到達自己的最終位置
			swap(array, 0, j);
			// 元素交換之後,毫無疑問,最後一個元素無需再考慮排序問題了。
			// 接下來我們需要排序的,就是已經去掉了部分元素的堆了,這也是爲什麼此方法放在循環裏的原因
			// 而這裏,實質上是自上而下,自左向右進行調整的
			adjustHeap(array, 0, j);
		}
	}
 
	/**
	 * 
	 * @description 這裏,是整個堆排序最關鍵的地方,正是因爲把這個方法抽取出來,才更好理解了堆排序的精髓,會儘可能仔細講解
	 * @author
	 * @param
	 * @return
	 * @time 2018年3月9日 下午2:54:38
	 */
	public static void adjustHeap(int[] array, int i, int length) {
		// 先把當前元素取出來,因爲當前元素可能要一直移動
		int temp = array[i];
		// 可以參照sort中的調用邏輯,在堆建成,且完成第一次交換之後,實質上i=0;也就是說,是從根所在的最小子樹開始調整的
		// 接下來的講解,都是按照i的初始值爲0來講述的
		// 這一段很好理解,如果i=0;則k=1;k+1=2
		// 實質上,就是根節點和其左右子節點記性比較,讓k指向這個不超過三個節點的子樹中最大的值
		// 這裏,必須要說下爲什麼k值是跳躍性的。
		// 首先,舉個例子,如果a[0] > a[1]&&a[0]>a[2],說明0,1,2這棵樹不需要調整,那麼,下一步該到哪個節點了呢?肯定是a[1]所在的子樹了,
		// 也就是說,是以本節點的左子節點爲根的那棵小的子樹
		// 而如果a[0}<a[2]呢,那就調整a[0]和a[2]的位置,然後繼續調整以a[2]爲根節點的那棵子樹,而且肯定是從左子樹開始調整的
		// 所以,這裏面的用意就在於,自上而下,自左向右一點點調整整棵樹的部分,直到每一顆小子樹都滿足大根堆的規律爲止
		for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
			// 讓k先指向子節點中最大的節點
			if (k + 1 < length && array[k] < array[k + 1]) {
				k++;
			}
 
			// 如果發現子節點更大,則進行值的交換
			if (array[k] > temp) {
				swap(array, i, k);
				// 下面就是非常關鍵的一步了
				// 如果子節點更換了,那麼,以子節點爲根的子樹會不會受到影響呢?
				// 所以,循環對子節點所在的樹繼續進行判斷
				i = k;
				// 如果不用交換,那麼,就直接終止循環了
			} else {
				break;
			}
		}
	}
 
	/**
	 * 交換元素
	 * 
	 * @param arr
	 * @param a
	 *            元素的下標
	 * @param b
	 *            元素的下標
	 */
	public static void swap(int[] arr, int a, int b) {
		int temp = arr[a];
		arr[a] = arr[b];
		arr[b] = temp;
	}
}

小頂推

/**
 * @Author: 白雄雄
 * @Date: 2019/9/10 13:48
 */
public class SmailHeap {
    public static void main(String[] args) {
        int[] array = new int[]{1,3,2,7,4,0,5,10};
        heapSort(array);
        System.out.println(Arrays.toString(array));

    }

    private static void heapSort(int[] array) {
        for (int i = array.length / 2 - 1; i >= 0; i--) {
            adjustHeap(array,i,array.length);
        }

        for (int j = array.length - 1; j >= 0; j--) {
            int temp = array[0];
            array[0] = array[j];
            array[j] = temp;
            adjustHeap(array,0,j);
        }
    }

    private static void adjustHeap(int[] array, int i, int len) {
        int temp = array[i];
        for (int k = i * 2 + 1; i < len; k = 2 * k + 1) {
            if (k + 1 < len && array[k] > array[k + 1]) {
                k++;
            }
            if (k<len && array[k] < temp) {
                array[i] = array[k];
                i = k;
            } else {
                break;
            }
        }
        array[i] = temp;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章