堆排序(Heap Sort)

Posted 小杜同学的嘚啵嘚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了堆排序(Heap Sort)相关的知识,希望对你有一定的参考价值。

堆这种数据结构的一种典型应用——优先队列(Priority Queue)


普通队列:先进先出;后进后出。

优先队列:出队顺序和入队顺序无关;和优先级相关。

优先队列最典型的应用就是在计算机的操作系统中执行任务,当操作系统执行多个任务时,操作系统是将cpu的执行周期划成了多个时间片,在每个时间片里只能执行一个任务,那么究竟先执行哪个任务呢?答案是每个任务都有一个优先级,操作系统将每次动态地选择一个优先级最高的任务开始执行,这个时候就需要使用优先队列。也就是说,所有的任务都进入优先队列,每一次由优先队列调度决定cpu执行哪个任务。

为什么是“动态地”?

因为每一次cpu处理任务的同时,还会有其他新的任务的进入。

既然优先队列对于动态的数据处理有很好的使用效果,那么对于静态的数据处理效果怎么样?

答案是在处理静态的数据上,优先队列也是很有优势的。

例如:在100000000个元素中选出前100名?

抽象一下就是:在n个元素中选出前m个元素?

容易想到的解决办法就是,对这n个元素进行排序,然后选出前m个元素。这时的时间复杂度为O(nlogn)。

但是如果我们使用优先队列,那么我们的时间复杂度将可以达到O(nlogm)。

如果要处理的数据基数庞大的话,那么优先队列的使用将使得整个处理过程快十几倍,甚至更明显。

优先队列的主要操作:


 

入队

出队(取出优先级最高的元素)

优先队列的实现(普通数组、顺序数组、堆):


虽然看起来,使用堆这种数据结构所产生的时间复杂度在入队时比不上普通数组,在出队时比不上顺序数组。可是平均来讲,使用堆来实现优先队列来处理一个系统任务的时间复杂度要大大地低于使用数组来实现。

最极端的情况下,对于总共有N个请求:

使用普通数组或者顺序数组,最差的情况:O(n2)

使用堆,时间复杂度可以稳定在:O(nlgn)

什么是堆?


 

堆是一种树形结构,其中最经典的就是二叉堆(Binary Heap)。它所对应的就是二叉树。

但是这个二叉树有几个特点(对于大根堆(或者叫最大堆)):

1.在该二叉树上任何一个节点总是不大于它的父亲节点;

2.堆对应的二叉树是一颗完全二叉树。

例:

注意,在上面列出的特点1中所讲,并不意味着层数越大数字越小(在最大堆中),例如上图中19和13的大小比较。

最大堆的实现:


 

实现堆可以使用一个节点附带两个指针的方式实现(树的实现方式)。

经典的实现方式是使用数组去存储堆。我们之所以可以使用数组去实现一个堆,原因在于,堆是一个完全二叉树。

如果我们给一个堆的每个元素标一下号的话。像这样:

我们可以看到,每一个节点的左边子节点是父节点的二倍,例如2==1*2;4==2*2;6==3*2。

每个节点的右边子节点是父节点的二倍加一,例如3==1*2+1;5==2*2+1;7==3*2+1。

注意,0号索引是不使用的。

即得到公式:

注意,除法为计算机除法,除不尽则取整。

代码(搭建最大堆大体框架):

package com.heap;

public class MaxHeap {

    private int[] arr;
    private int count;//堆中元素个数
    
    public MaxHeap(int capacity){
        arr=new int[capacity];//由用户指定该最大堆的容量
        count=0;
    }
    
    //返回该最大堆的元素个数
    public int size(){
        return count;
    }
    
    
    
    public static void main(String[] args) {

        MaxHeap heap=new MaxHeap(100);
        System.out.println(heap.size());
        
    }

}

如何将一个元素加入到最大堆?(优先序列入队)


 

例如将一个元素(52)加入到之前的最大堆中:

这时候,新插入的元素位于我们存储该最大堆的数组的末尾,在即是示意图中的位置。

我们可以看到,在新插入元素所在的最小子树(即16、15、52元素所在的树)中,并不符合最大堆的定义。

那么我们要做的是将新插入元素和其父结点进行比较大小,若新插入结点比其父结点大,则交换两者之间的位置,然后再比较新插入结点交换后的位置和它当前的父结点比较大小,若还是比其父结点大则继续交换,直到新插入的结点在其当前位置上不大于其当前父结点。

代码贴在下面。

 如何将从最大堆中取出一个元素?(优先序列出队)


 

注意,从堆中取出一个元素,只能取出根结点的那个元素,对于最大堆来说就是整个堆中的最大值。

例如,我们要从下面最大堆中取出一个元素,自然取出的就是62。然后我们将最后一个元素换到根结点,同时count--。

此时的最大堆已经不再符合最大堆的定义,我们需要做的是将现在位于根结点的16一步一步地挪到合适的位置。

首先我们要比较当前根结点和其两个子结点中的较大者(即16和52比较),若子结点大则交换两者位置,交换位置后继续和其当前子结点中的较大者比较,若还是比子结点小则继续交换,直到不再比子结点中的较大值小为止。

代码:

package com.heap;

public class MaxHeap {

    private int[] arr;
    private int count;//堆中元素个数
    private int capacity;
    
    public MaxHeap(int capacity){
        arr=new int[capacity];//由用户指定该最大堆的容量
        this.capacity=capacity;
        count=0;//数量初始化为0
    }
    
    //判断当前堆是否为空
    public Boolean isEmpty(){
        return count==0;
    }
    
    //返回该最大堆的元素个数
    public int size(){
        return count;
    }
    
    //插入元素到最大堆
    public void insert(int num){
        if(count+1>capacity)
            System.out.println("容量已满,不能插入");
        else{
            count++;
            arr[count]=num;
            shifUp(count);
        }
            
    }
    
    //将堆中新插入元素调整到合适位置
    private void shifUp(int k) {
        //如果当前元素比其父结点大,则交换一下两者的位置
        if(k>1&&arr[k/2]<arr[k]){
            int temp=arr[k/2];
            arr[k/2]=arr[k];
            arr[k]=temp;
        }
        else
            return;//循环总结条件
        k=k/2;
        shifUp(k);
    }
    
    //从最大堆中取出元素
    public int extractMax(){
        if(count>0){
            int item=arr[1];//数组是从索引1开始存储的
            
            arr[1]=arr[count];//将数组最末端元素放到根结点位置
            count--;//数量减一
            
            shitDown(1);//将当前根结点的元素向下移动到合适位置
            return item;
        }
        else
        {
            System.out.println("该最大堆为空");
            return -1;
        }
    }
    
    
//将指定位置元素向下移动到合适位置
    private void shitDown(int k) {
        //要想向下移动元素,那么该元素必须有孩子,当其有左孩子就说明它有孩子
        //在此轮循环中,交换arr[k]和arr[j]的位置
        while(2*k<=count){
            int j=2*k;//初始化j表示左孩子
            //如果它有右孩子并且右孩子大于左孩子,则用j表示右孩子
            if(j+1<=count&&arr[j+1]>arr[j])
                j=j+1;
            //若其较大的孩子比该结点大则交换两者位置
            if(arr[j]>arr[k]){
                int temp=arr[k];
                arr[k]=arr[j];
                arr[j]=temp;

                //将k定位到交换后的当前位置
                k=j;
            }else
                break;
            
        }
    }

    public static void main(String[] args) {

        MaxHeap heap=new MaxHeap(100);
        heap.insert(3);
        heap.insert(15);
        heap.insert(23);
        heap.insert(7);
        heap.insert(4);
        heap.insert(8);
        System.out.println(heap.size());
        System.out.println(heap.extractMax());
        System.out.println(heap.size());

        
        
    }

}

 如何使用堆实现排序?


 

实现基础排序(将原数组按从小到大排序):

//基础堆排序
    public static void MaxHeapSort1(){
        int[] arr = new int[100];
        //创建一个随机数组
        for(int i=0;i<100;i++)
            arr[i]=(int)(Math.random()*1000);
        //由于在堆中元素是从索引1开始存储的,所以我们在定义堆时需要指定的大小为数组的长度加一
        MaxHeap heap=new MaxHeap(arr.length+1);
        //将数组元素逐个加入到最大堆里
        for(int i=0;i<arr.length;i++)
            heap.insert(arr[i]);
        //将数组从小到大排序
        for(int i=arr.length-1;i>=0;i--)
            arr[i]=heap.extractMax();
        //将数组输出
        for(int i=0;i<arr.length;i++)
            System.out.println(arr[i]);
        
    }

在上面的基础堆排序示例代码中,我们实现堆排序的方式是将原数组一个一个地加入最大堆的,这个步骤是可以优化的,我们优化的方式就是“堆化(heapify)”。

我们知道数组是可以表示堆的,那么对于一个普通数组来说,对应它的索引也可以表示成一个堆,只不过这个堆未必满足最大堆或者最小堆,很大的概率它会是一个没有什么规律的堆。我们先来看一个普通数组组成的堆:

其中蓝色的部分叫叶子结点。对于叶子结点来说,它就哥们一个,它自己本身就是一个最大堆。

注意,这个数组依旧是从索引1开始存储的。

那么我们从后往前看,第一个非叶子结点索引为5(值为22),怎么求出它的呢?

用最后一个元素的索引除2即是第一个非叶子结点的索引,例如(10/2==5,如果最后一个元素的索引为11的话,则为11/2==5)(注意这里是计算机除法,除不尽去余取整)

也就是说最后一个元素的父结点就是第一个非叶子结点。

我们使用上文中的shiftDown()方法从第一个非叶子结点到最后一个非叶子结点(也就是根结点)去调整每一个非叶子结点,使得每个以当前非叶子结点为根结点的堆调整为最大堆。

这样当我们调整到根结点的时候,这个数组就已经是最大堆了,这就叫做“堆化(heapify)”。

具体代码:

在MaxHeap类中再设定一个构造方法:

//构造方法2
    //使用堆化的方式对传入的数组进行构建最大堆
    public MaxHeap(int[] arr){
        this.arr=new int[arr.length+1];//注意最大堆的索引从1开始,所以需要多开辟一个位置
        for(int i=0;i<arr.length;i++)
            this.arr[i+1]=arr[i];
        //将堆内元素数量设置为arr.length
        count=arr.length;
        //从第一个非叶子结点进行堆化
        for(int k=count/2;k>=1;k--)
            shiftDown(k);
        
    }

具体调用方法代码:

//使用“堆化”优化的堆排序
    public static void MaxHeapSort2(int[] arr){
        MaxHeap heap=new MaxHeap(arr);
        
        //以下和MaxHeapSort1()方法中一样
        //将数组从小到大排序
        for(int i=arr.length-1;i>=0;i--)
            arr[i]=heap.extractMax();
        //将数组输出
        for(int i=0;i<arr.length;i++)
            System.out.println(arr[i]);
        
    }

相关时间复杂度:


将n个元素逐个插入到一个空堆中,算法复杂度是O(nlogn)。

heapify的过程,算法复杂度为O(n)。

 

优化堆排序:(原地堆排序)


 

在上面的堆排序算法中,都需要先将数组中的元素放到堆中,然后再把堆中的元素取出来。整个程序中又额外的开辟了n个空间。

事实上我们通过上面的理论方法,完全可以使一个数组在原地完成堆排序。而不需要任何的额外空间。

基本思想:

我们知道一个普通的数组就可以表示成一个堆。

我们可以应用之前讲到的堆化(heapify)是我们的数组构建成一个最大堆。

在这个最大堆中第一个元素就是这个数组的最大值。

但是显然它的最终位置应该是这个数组的最后一个位置。于是,我们可以将位于数组第一个位置的V元素和位于数组末尾的W元素进行交换位置。

这个时候除V元素之外的数组并未处在最大堆的状态,我们需要对W元素进行shirtDown()操作,以使得除V元素之外的数组恢复最大堆状态。

操作完成后,这个时候的首位元素又成了当前数组(除最后一位已经排好的元素外)的最大值,然后将它与此时末尾元素进行交换位置。

再进行对W元素进行shiftDown()操作,再交换首末位置元素,以此类推,直到所有元素都排序完。

由于一般数组都是从0开始索引的,所以相应的堆排序操作父结点和孩子结点的关系就会发生相应的变化。(存在了1这个偏移量)

代码:

package com.heap;

public class HeapSort2 {

    //将k位置的元素进行向下移动到合适位置
    //该最大堆中现在共有n个元素
    private static void shiftDown2(int[] arr,int n,int k){
        //共有n个元素,那最后一个元素的索引信息为n-1
        //即<n就说明存在
        //当该位置存在左孩子的时候
        //这个循环交换的是arr[k]和arr[j]
        while(2*k+1<n){
            int j=2*k+1;
            //如果有右孩子并且右孩子比左孩子大
            if(j+1<n&&arr[j+1]>arr[j])
                j+=1;
            if(arr[k]<arr[j]){
                int temp=arr[k];
                arr[k]=arr[j];
                arr[j]=temp;
            }else
                return;
            //更新k的定位,继续向下比较
            k=j;
            
        }
    }
    
    
    
    public static void heapSort(int[] arr){
        
        //heapify
        //对整个数组进行堆化
        for(int i=0;i<arr.length;i++){
            shiftDown2(arr,arr.length,i);
        }
        
        //将最大值和末尾元素交换位置
        //然后对交换到首位的元素进行shiftDown操作
        for(int j=arr.length-1;j>0;j--){
            int temp=arr[0];
            arr[0]=arr[j];
            arr[j]=temp;
            
            shiftDown2(arr,j,0);
        }
    }
    
    public static void main(String[] args) {
        
        int[] arr=new int[100];
        for(int i=0;i<arr.length;i++)
            arr[i]=(int)(Math.random()*100);
        heapSort(arr);
        
        for(int i=0;i<arr.length;i++)
            System.out.println(arr[i]);

    }

}

 

以上是关于堆排序(Heap Sort)的主要内容,如果未能解决你的问题,请参考以下文章

堆排序(heap sort)

Heap Sort堆排序

堆排序(Heap Sort)

堆排序(Heap Sort)

堆排序Heap sort

PAT甲题题解1098. Insertion or Heap Sort (25)-(插入排序和堆排序)