1.前言
本篇博客将接着之前的内容,继续学习排序算法-堆排序。我们将从堆的定义入手,学习这个新的数据结构以及其在排序算法中的应用。
2.堆的定义
数据结构二叉堆能够很好地实现优先队列的操作。在二叉堆的数组中,我们将所有元素画成一棵二叉树,当每个根节点的元素都要大于或等于其子节点的两个元素,我们称之为堆有序。从任意结点向上,我们都能得到一列非递减的元素;从任意结点向下,我们都能的到一列非递增的元素。
2.1二叉堆表示法
二叉堆是一组能够用堆有序的完全二叉树排列的元素,并且在数组中按照层级存储(不使用数组的第一个元素),在一个堆中,位置为K的结点的父结点的位置为K/2,而它的两个子结点的位置分别为2*K,2*K+1。在不使用指针的情况下,我们可以通过计算数组的索引在树中上下移动。
2.2 堆的算法
我们用长度为N+1的数组表示一个大小为N的堆,堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态恢复。我们称这个过程叫做堆的有序化。
在堆的有序化过程中我们会遇到两种情况 ,当某个结点的优先级上升,我们需要由下至上恢复堆的顺序;当某个结点的优先级下降,我们需要由上至下的恢复堆的顺序。
2.2.1 由下至上的堆有序化(上浮)
如果堆的有序状态因为某个结点变得比它的父结点更大而被打破,那么我们就需要通过交换它和它的父结点来修复堆,我们一遍遍使用同样的操作不断上移,直到我们遇到一个更大的父结点 。
private void swim ( int k ){
while ( k > 1 && less ( k/2 , k ){
exch ( k/2 , k ) ;
k = k / 2 ;
}
} private void swim ( int k ){
while ( k > 1 && less ( k/2 , k ){
exch ( k/2 , k ) ;
k = k / 2 ;
}
}
2.2.2 由上至下的堆有序化(下沉)
如果堆的有序状态因为某个结点变得比它的两个子结点或是其中之一更小而被打破了,那么我们可以通过将它和它的两个子结点中的较大者交换来恢复堆,我们将结点不断下移直到他的子结点都比它小或是或是到达了叶子结点。
private void sink ( int k ){
while ( 2 * k <= N ){
int j = 2 * k ;
if( j < N && less ( j , j+1 ))
j++ ;
if ( ! less( k , j ))
break;
exch( k , j ) ;
k = j ;
}
}
2.2.3 插入元素以及删除最大元素
插入元素。我们将新元素加到数组的末尾,增加堆的大小并让这个新元素上浮到合适的位置。
删除最大元素。我们从数组的顶端删去最大的元素,并将数组的最后一个元素放到顶端,减少堆的大小并让元素下沉到合适的位置。
堆的Java代码实现:
package DataStructure;
public class MaxPQ <Key extends Comparable<Key>> {
private Key[] pq ;
private int N = 0 ;
public MaxPQ( int maxN ){
pq = ( Key[]) new Comparable[maxN + 1 ] ;
}
public boolean isEmpty( ){
return N == 0 ;
}
public int size( ){
return N ;
}
private boolean less ( int i , int j ){
return pq[i].compareTo(pq[j]) < 0 ;
}
private void exch ( int i , int j ){
Key temp = pq[i] ;
pq[i] = pq[j] ;
pq[j] = temp ;
}
private void swim ( int k ){
while ( k > 1 && less ( k/2 , k ){
exch ( k/2 , k ) ;
k = k / 2 ;
}
}
private void sink ( int k ){
while ( 2 * k <= N ){
int j = 2 * k ;
if( j < N && less ( j , j+1 ))
j++ ;
if ( ! less( k , j ))
break;
exch( k , j ) ;
k = j ;
}
}
public void insert ( Key v ){
pq[++N] = v ;
swim(N) ;
}
public Key delMax ( ){
Key max = pq [1] ;
exch( 1 , N -- ) ;
pq [ N+ 1 ] = null ;
sink( 1 );
return max ;
}
}
3.堆排序
堆排序可以分为两个阶段:(1)在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中(2)在下沉阶段排序,我们只需要依次取出堆顶元素,并构造新的堆即可。
3.1 堆的构造
我们可以从右至左用sink()函数构造子堆,数组的每个位置都已经是一个子堆的根结点了,sink()对于这些子堆也适用。如果一个结点的两个结点都已经是堆了,那么在该结点上调用sink()可以将它们变成一个堆。因此我们只需要扫描数组中一半的元素(K/2即是所有的根节点个数)
3.2 下沉排序
堆排序的主要工作是在第二阶段完成的,我们将堆中的最大元素删除,然后放入堆缩小后数组空出的位置(即数组的尾端),这部分和选择排序有点相像。
java代码实现的int类型的堆排序:
最大值堆:
package DataStructure;
public class MyMaxPQ {
private int[ ] pq ;
private int N ;
public MyMaxPQ( int MaxN ){
pq = new int[ MaxN + 1 ] ;
}
public boolean isEmpty( ){
return N == 0 ;
}
public int size( ){
return N ;
}
private void swim( int k ){
while( k > 1 && pq[k] > pq[ k/2 ] ){
int temp = pq[k];
pq[k] = pq[k/2];
pq[k/2] = temp ;
}
}
public void sink( int k , int N){
while( 2 * k <= N ){
int j = 2* k;
if( j < N && pq[j] < pq[j+1 ]){
j++ ;
}
if( pq[j ] < pq[k])
break ;
int temp = pq[j];
pq[j] = pq[k] ;
pq[k] = temp ;
k = j;
}
}
public void insert( int a ){
pq[++N] = a ;
swim(N) ;
}
//堆排序不用delete
public void sortMax( ){
int N = pq.length - 1;
for ( int i = N/2 ; i >= 1 ; i -- ){
sink(i , N );
}
while( N>1 ){
int temp = pq[1];
pq[1] = pq[N];
pq[N] = temp ;
sink( 1 , --N );
}
}
public int[] getPq() {
return pq;
}
}
Main函数:
package SortWays;
import DataStructure.MyMaxPQ;
import java.util.Scanner;
public class DuiSort {
public static void main ( String args[ ]){
Scanner scanner = new Scanner(System.in );
int n =scanner.nextInt();
MyMaxPQ myMaxPQ = new MyMaxPQ( n ) ;
for( int i = 0 ; i < n ; i++ ){
myMaxPQ.insert(scanner.nextInt());
}
myMaxPQ.sortMax();
int result[] = myMaxPQ.getPq();
for( int i = 1 ; i<= n ; i++ ){
System.out.print(result[i]+" ");
}
}
}
运行结果:
排序过程示意图:
4.关于堆的一些命题
(1)一棵大小为N的完全二叉树高度为lgN
(2)对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1),删除最大元素的操作需要不超过2lgN次的比较。
证明:两种操作都需要在根节点和堆底之间的移动元素,而路径的长度不超过lgN,对于路径上的每个结点,删除最大元素需要进行两次比较(除了堆底元素),一次用来找出较大的子结点,一次用来判断该子结点是否需要上浮
(3)将N个元素排序,堆排序只需要少于(2NlgN+2N)次比较(以及一半次数的交换)
证明:2N来自于堆的构造,2NlgN来自于每次下沉最大可能需要2lgN次的比较
5.后语
关于排序算法的学习应该是要告一段落了,之后会总结对比一下各个算法的时间复杂度,空间复杂度,稳定性等特征。最近在做笔试题的时候遇到了有关红黑树的知识,于是打算接下来学习java中树这一数据结构