一文带你轻松把堆排序相关概念、原理及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时,返回的是数组的第一个元素,即为最小值或者最大值。

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