數據結構與算法-排序(六)堆排序(Heap Sort)


摘要

堆排序需要用到一種數據結構,大頂堆。大頂堆是一種二叉樹結構,本質是父節點的數大於它的左右子節點的數,左右子節點的大小順序不限制,也就是根節點是最大的值。

這里就是不斷的將大頂堆的根節點的元素和尾部元素交換,交換到大頂堆沒有可以被交換的元素為止。后面再說大頂堆的邏輯。

邏輯

首先將序列通過大頂堆排序。然后不斷的從堆中取出頂部元素放在尾部,直到大頂堆元素為空。

流程

  1. 對序列進行原地建堆操作
  2. 重復下面操作,直到堆元素數量為 1
    1. 交換堆頂元素與尾元素
    2. 堆的元素數量減 1
    3. 對 0 位置進行 1 次 自下而上的下濾

下面在代碼中解釋原地建堆自下而上的下濾這兩個詞的邏輯。

實現

首先進行原地建堆。原地建堆是先將序列按照大頂堆的排序邏輯處理序列。

大頂堆的序列邏輯是父節點的值大於它的左右子節點的值,可以想象成一個二叉樹。這里的原地排序用到了siftDown方法,而且在循環中只循環到序列一半數量,為什么?這個在下面看siftDown方法時詳細探究一下。


// 原地建堆
// 自下而上的下濾
heapSize = array.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
	siftDown(i);
}

交換堆頂和尾部元素,然后將需要比較的序列元素數量減少1,並將要進行比較的序列再使用siftDown方法過濾,保持序列的大頂堆的性質。然后繼續開始的交換,直到可以比較的序列數量為 1 就截止。

while (heapSize > 1) {
	// 交換堆頂元素和尾部元素
	swap(0, --heapSize);
		
	// 對 0 位置進行 siftDown(恢復堆的性質)
	siftDown(0);
}

大頂堆的 siftDown 方法

這里來探究一下siftDown(下濾)。

二叉樹的父節點和子節點的關系符合這樣的公式

  • leftChilder = partner * 2 + 1
  • rightChilder = parnter * 2 + 1 + 1
  • half (葉子)節點的數量是總節點數量的 1/2

siftDown 方法主要是將 index 位置上的元素放在合適的位置上。那么什么位置是合適的位置呢

依據大頂堆的父節點值大於左右子節點的值的性質來看,只要是保證 index 位置的元素大於它的左右子節點就好。

看下面代碼,如果 index < half 才進行循環比較,那么就有一個問題,index >= half 為什么不用比較

這就要提到很巧妙的點,首先看大頂堆的性質,左右子節點沒有具體順序的要求,其次子節點的值小於父節點。那么就可以依據二叉樹的葉子節點性質,如果index的位置是在葉子節點位置,那么就本來比它的父節點要小,就不用比較(這個是建立在序列本來符合大頂堆的順序,出現一個位置的元素有變化時進行的過濾處理)。

這也是上面的原地排序中,只從一半的位置開始,是因為從這個位置開始,肯定會給它的子節點比較,過濾出大的,並放在合適位置。

代碼中有三個巧妙的點

  1. 循環從序列的一半位置開始比較,如果位置不在前半部分,就不進行比較,這個在上面分析過
  2. 在比較的時候,獲取到它左右子節點中最大的節點比較。在獲取右子節點的時候看右子節點是否存在rightIndex<heapSize。因為大頂堆是符合完全二叉樹的(盡量往左子樹安排元素)。
  3. 說是二叉樹,但是沒有實際的節點,還是一個線性序列,通過公式來獲取左右子樹的位置,這個就是心中有樹,沒有樹也是樹
	
/*
 * 讓 index 位置的元素下濾
 */
private void siftDown(int index) {
	E element = array[index];

	int half = heapSize >> 1; // 取出非葉子節點
	// 第一個葉子結點的索引 == 非葉子節點的數量
	// 必須保證 index 是非葉子節點
	while (index < half) {
		// index 的節點有2種情況
		// 1、只有左子節點
		// 2、同時有左右子節點
			
		// 默認左子節點跟它進行比較
		int childIndex = (index << 1) + 1;
		E child = array[childIndex];
		// 右子節點
		int rightIndex = childIndex + 1;
		if (rightIndex < heapSize && cmp(array[rightIndex], child) > 0) {
			child = array[ childIndex = rightIndex];
		}
			
		if (cmp(child, element) < 0) break;
			
		// 將子節點存放到index位置
		array[index] = child;
		// 重新設置 index
		index = childIndex;
	}
	array[index] = element;
}

時間和空間復雜度

  • 最好、平均時間復雜度:O(nlogn)
  • 最壞時間復雜度:O((nlogn)
  • 空間復雜度:O(1)
  • 屬於不穩定排序

題外話

這次的排序用到了二叉樹大頂堆的一些知識,可能看下來有諸多疑問,這里就先請諸位看官有個印象,后續我會分享二叉樹的知識,然后在回過頭來看堆排序,會讓你思路大開。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM