一文帶你輕鬆把堆排序相關概念、原理及JAVA實現搞的明明白白

流暢閱讀本文的前置條件是:瞭解樹,二叉樹這兩種數據結構。

堆與堆排序

首先要明確:堆排序是指利用大頂堆(或者小頂堆)來實現排序,達到使數據有序的目的。而非是將排序的結果存儲在堆這樣一個數據結構中。

二叉堆:用數組存儲的完全二叉樹,也稱堆。

堆中的元素的存儲特性:下標爲i的元素,其左孩子的下標爲2*i+1,右孩子的下標爲2*i+2。

如下圖所示的存儲在數組中的堆,實質是一個完全二叉樹。

其下標爲0的元素,左孩子(lc)存儲在下標爲1的位置, 右孩子(rc)存儲在下標爲2的位置。
在這裏插入圖片描述
如上圖數組中存儲的完全二叉樹可以用以下樹圖表示:(數節點中存儲的數是數組的下標)
在這裏插入圖片描述
“堆”這個概念很形象的表述了其數據結構。如果覺得不容易理解,可以試着想象一下,在現實生活中,一堆蘋果,肯定是由上至下逐層增多,且中間並無空隙的,因此存儲上用的是空間連續的數組,而非鏈式。而一顆蘋果樹,是有長長的枝杈的,因此,樹的節點間的關係是用引用即指針來實現,可以將指針想象成枝杈,節點想象成果實。

“堆”的概念正是源於堆排序。

“堆”代表的另一個常見概念:垃圾收集存儲機制。

例如在java語言的JVM中,堆內存空間(JavaHeapSpace)就是垃圾回收的主要對象,JVM設計了堆來存儲需要被回收的內存對象數據。

堆頂

即完全二叉樹的根,也即存儲堆數據的數組A的第一個元素A[0]。

小頂堆

小頂堆:完全二叉樹中的每個結點的值都小於或等於者其左右孩子結點的值。因此,根是此樹中的值最小的結點。即A[0]最小。

大頂堆則是每個節點都大於其左右孩子節點,因此根是最大節點。

堆排序

堆排序,就是利用小頂堆或者大頂堆來實現數據的排序。

實現方法(正序-利用小頂堆):

  • 將所有數據分爲兩部分,一部分是已經有序的數據,存儲在數組B中;另一部分是還未排好序的無序數據,存儲在數組A中;
  • 在堆排序過程中,對於存儲堆元素的數組A有兩個size,一個是數組本身的size,另一個是有效堆數據個數heap_size。因爲,在排序過程中,有序數組B中元素不斷擴充,而構成堆的無序數據的個數則在不斷減少。也就是說,雖然數組A中,A[0…A.length]可能一直都存儲有數據,但是排序過程中,只有A[0…A.heap_size]中存儲的是堆的有效元素。
  • 在未排序時,A.size=A.heap_size; 排序過程中,A.size=A.heap_size + B.size;
  • 排序前,所有元素都是無序的,全都存儲在數組A中,這時需要將所有元素構建成小頂堆。
  • 取數組A中的堆頂元素(最小值),追加到有序數據即數組B的尾部。
  • 每輪取出堆頂元素後,將堆的最後一個有效元素(即完全二叉樹的最後一個節點)放置到堆頂位置(即完全二叉樹的根,也即A[0]),這時堆必然已經不是小頂堆了,那就繼續調整堆,使之成爲一個小頂堆
  • 即每輪都從無序數據中取出一個最小值,那麼第一輪取得的是所有元素的最小值,放到有序部分中去;第二輪取得的是無序數據中的最小值,也是所有元素中的次小值,再追加到有序數據尾部,即將次小值追加到了最小值之後,這樣就形成了正序排序。
  • 直到剩餘無序元素個數爲1
import static java.lang.System.out;
import java.util.*;
public class HeapSort {

	public static void main(String[] args){

		// 存儲無序數據的數組,堆元素數組
		int [] heapArr = new int[10];
		// 隨機生成一些待排序整數
		Random  r = new Random();
		for(int i=0;i<heapArr.length;i++){
			heapArr[i] = r.nextInt(100);				
		}

		out.println(Arrays.toString(heapArr));

		// 有效堆元素在數組中的的最大下標,即剩餘待排序數據的個數
		int	heap_size=heapArr.length;
		
		//  將待排序元素構成成小頂堆
		createMinTopHeap(heapArr);

		out.println(new StringBuffer("構建出的小頂堆:").append(Arrays.toString(heapArr)));

		// 存儲有序數據的數組
		int [] sortedData = new int[heapArr.length];		
		
		for(int i=0;i<heapArr.length-1;i++){

			//取堆頂元素,即無序數據中的最小值,追加到有序元素數組中
			sortedData[heapArr.length-heap_size]=heapArr[0];

			out.println(new StringBuffer("sortedData:").append(Arrays.toString(sortedData)));
			
			//將有效堆元素中的最後一個放到剛被取走的堆頂位置
			heapArr[0]=heapArr[heap_size-1];

			heap_size--;

			//堆頂節點有變化,已經不再是小頂堆,需要再次將其調整爲小頂堆
			adjust(heapArr,0,heap_size-1);
		}
		
		sortedData[heapArr.length-heap_size]=heapArr[0];
		out.println(new StringBuffer("sortedData:").append(Arrays.toString(sortedData)));

		for(int i=1;i<sortedData.length;i++){
			if(sortedData[i]<sortedData[i-1])  {
				out.println("不是有序的數據");	
			}

		}

	}

	/**
	構建小頂堆
	*/
	static void createMinTopHeap(int [] a){
		Objects.requireNonNull(a);

		// 從最後一個非葉子節點開始,由下層至上層將所有非葉子節點的子樹調整爲小頂堆		
		// 最後一個元素(下標爲a.length-1)的父節點,即最後一個非葉子節點
		// 根據堆的存儲特性:下標爲i的元素,其左孩子下標爲2*i+1,其右孩子下標爲2*i+2。
		// 則有,下標爲n的元素,如果n不能能整除2,則說明其是父節點的左孩子,父節點的下標爲(n-1)/2;否則父節點下標爲(n-2)/2
		
		int lastNodeIdx = a.length-1;
		
		int parentOfLastNode = (lastNodeIdx|1)==0 ?  ((lastNodeIdx-2)>>1) : ((lastNodeIdx-1)>>1)  ;

		for(int i=parentOfLastNode; i>=0; i--){
		
			//  將以此節點爲根的的子樹調整爲小頂堆
			adjust(a,i,a.length-1);					
		}					
	}

	/**
	調整堆,使之成爲小頂堆
	a 存儲堆元素的數組
	r 要調整的子樹的根節點的下標
	heap_size  數組中實際有效堆元素的最大下標
	*/
	static void	adjust(int [] a,int r,int heap_size){

		if(r==heap_size) return;		

		// 比較 父、左子、右子 三個節點的大小,找出值最小的的節點下標
		int left = 2*r+1;
		int right = left+1;
		int min = r;
		if(left <= heap_size){
			min = a[left]<a[r]?left:r;
			if(right<=heap_size){
				min = a[right]<a[min]?right:min;
			}		
		}
		
		if(min!=r){

			// 如果父節點不是最小節點,則交換,使父節點最小
			swap(a,min,r);

			// 交換後,被交換的子節點的位置上的數據有更新,需要將此節點的子樹重新調整成小頂堆
			adjust(a,min,heap_size);
		}
	}
	
	/**
	交換i,j兩個位置中的值
	*/
	public static void swap(int [] a,int i,int j){
		if(i!=j){
			int tmp = a[i];
			a[i]=a[j];
			a[j]=tmp;
		}		
	}
}	

優先隊列

堆排序的效率相比插入排序仍然略差,但是堆有另外的高效的用途——優先隊列。

在java中,PriorityQueue類實現了優先隊列。(PriorityQueue實現了Queue接口,有add / remove / element操作,)

優先隊列並不要求在某一時間點,隊列中的所有元素都是有序的。只要求在remove或者element操作時,獲得的是所有元素中的最小值或者最大值。

因此,只需在每次更新隊列時(插入、刪除、修改元素的值),將所有元素構建或者調整爲小頂堆、或者大頂堆,那麼接着進行element / remove時,返回的是數組的第一個元素,即爲最小值或者最大值。

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