排序算法总结(5)--堆排序

一、简介

1、什么是二叉堆

二叉堆是数组的树形表示,可以看成一个完全二叉树或近似的完全二叉树。树上每一个节点对应数组中的一个元素。除了最底层外,其他层都是满的,最后一层从左到右填充。在二叉堆中,每个节点的值都大于等于(或小于等于)其子节点的值,称作最大堆(最小堆)。堆是一种递归结构,每个节点的子树都是堆。
表示二叉堆的数组A有两个属性:A.length和A.heapSize。前者是数组A中的元素个数,后者表示有多少个堆元素存储在A中。通常0<=A.heapSize<=A.length。
树中每个节点都有三个方法:parent(i),left(i),right(i),分别是计算节点i的父节点、左子节点和右子节点。根节点用A[0]表示。
最大堆的逻辑结构和存储结构

2、堆的操作

以最大堆为例,每个节点的值都大于等于其子节点的值。

2.1 获取父节点、左子节点和右子节点的索引

public int parent(int i){
    if(i==0){
        return -1;
    }else{
        return (i-1)/2;
    }
}

public int left(int i){
    return 2*i+1;
}

public int right(int i){
    return 2*i+2;
}

2.2 maxHeapify:维持堆的性质

输入为数组A和下标i,保证以i为根节点的子树是最大堆。在树中,假定以left(i)和right(i)为根节点的二叉树都是最大堆,这时A[i]可能小于它的子节点,这样违背了最大堆的性质,需要通过让A[i]的值在堆中下沉,和其子节点中较大的元素交换位置,直到该元素下沉到其该有的位置。
这个方法有两种实现方式,递归和非递归

2.2.1 递归
public void maxHeapify(int[] array,int i){
        //维护最大堆,p86
        int l=left(i);
        int r=right(i);
        int largist=i;
        if (l<heapSize && array[i]<array[l]){
            largist=l;
        }
        if(r<heapSize && array[largist]<array[r]){
            largist=r;
        }
        if (largist!=i){
            int temp=array[i];
            array[i]=array[largist];
            array[largist]=temp;
            maxHeapify(array,largist);
        }   
    }
2.2.1 非递归
public void maxLoopHeapify(int[] array,int i){
        //维护最大堆的非递归算法,p87,6.2-5
        //维护最大堆,p86
        while(true){
            int l=left(i);
            int r=right(i);
            int largist=i;
            if (l<heapSize && array[i]<array[l]){
                largist=l;
            }
            if(r<heapSize && array[largist]<array[r]){
                largist=r;
            }
            if (largist!=i){
                int temp=array[i];
                array[i]=array[largist];
                array[largist]=temp;
                i=largist;
            }else break;
        }
}

时间复杂度O(lgn)
空间复杂度O(1)

2.2 bulidHeap:从无序的输入构造一个最大堆

首先将数组A转化为二叉树的结构,然后自底而上的对非叶子节点使用maxHeapify,因为叶子结点是元素个数为1的最大堆,不需要维护。在完全二叉树中, A[A.length/2,...,A.length1] 是叶子节点, A[0,...,A.length/21] 是非叶子节点。

public void bulidMaxHeap(int[] array){
    //建立最大堆,p87
    for(int j=heapSize/2-1;j>=0;j--){
        maxLoopHeapify(array,j);
    }
}

时间复杂度O(n)
空间复杂的O(1)

2.3 heapMax:获取堆中最大的元素

最大堆的根节点就是堆中最大的元素,所以获取堆中最大元素即为获取堆的根节点的元素。

public int heapMax(int[] array){
    //返回具有最大关键字的元素,p91
    return array[0];
}

时间复杂度O(1)
空间复杂的O(1)

2.4 heapExtratMax:获取堆中最大的元素,并移除

最大值即最大堆的根节点,这个和heapMax一样。然后要删除最大元素。将最大堆的根节点和最后一个元素交换,heapSize-1,最大值被删除。此时根节点的值小于等于其子节点的值,并且以子节点为根节点的二叉树都是最大堆,所以对A[0]使用maxHeapify,维护最大堆即可。

public int heapExtractMax(int[] array){
    //返回并去掉具有最大关键字的元素
    if (heapSize==0){
        System.out.println("队列为空");
        return -1;
    }
    int max=array[0];
    array[0]=array[heapSize-1];
    heapSize-=1;
    maxHeapify(array,0);
    return max;
}

时间复杂度O(lgn)
空间复杂的O(1)

2.5 heapIncreaseKey:增加堆中元素的值

在最大堆中,我们希望增加某个节点的值。首先将这个节点的值更新为新的值,因为增大该节点的值会违反最大堆的性质,所以需要将该节点“上浮”,直到合适的位置。从当前节点到根节点的路径上,将该节点不断的与其父节点比较,如果比父节点大,和父节点交换,直到该元素的值小于等于父节点为止。此时所有节点符合最大堆的性质。

public void heapIncreaseKey(int[] array,int i,int key){
    //将第i个元素的关键字增加到key,p91
    if(key<array[i]){
        System.out.println("需要改变的值比原来的还小");
    }else{
        array[i]=key;
        int p=parent(i);
        while(p>=0 && array[p]<key){
            //不用交换,只需赋值,因为array[i]=key
            array[i]=array[p];
            i=p;    
            p=parent(i);
        }
        array[i]=key;
    }
}

2.6 heapInsert:在最大堆中插入一个元素

在最大堆中插入一个元素,首先将堆的节点个数+1,将一个值为最小数的节点放在堆的末尾,然后将该节点的值增加到对应的值,使用heapIncreaseKey。

public int[] heapInsert(int[] array,int key){
    if (heapSize >= array.length - 1)  
        array=Arrays.copyOf(array, array.length*2);
    heapSize+=1;
    array[heapSize-1]=Integer.MIN_VALUE;
    heapIncreaseKey(array,heapSize-1,key);
    return array;
}

java数组不能改变长度,所以用ArrayList更合适。
时间复杂度O(lgn)
空间复杂度
使用数组int[] : int [] 长度不能改变,所以有时需要扩容,扩容是需要复制原来的数组,所以空间复杂度为 O(n)

三、堆的应用

3.1 堆排序

堆排序即使用二叉堆的性质对数组排序。首先从无序的数组构造一个最大堆,利用上文 bulidHeap,并且heapSize=A.length。最大堆的根节点为数组中的最大值,需要放在数组的最后,所以将根节点和数组最后一个元素A[A.length-1]交换,并将heapSize-1,最大值放在了数组最后,并且位置已定,不参与之后的计算。此时根节点的值可能不是最大的,需要maxLoopHeapify作用于根节点,维护堆的性质。一直重复之前的操作,直到heapSize==1。

public void heapSort(int[] array){
        //最大堆排序,p89
        bulidMaxHeap(array);
        for(int j=heapSize-1;j>=1;j--){
            int temp=array[j];
            array[j]=array[0];
            array[0]=temp;
            heapSize-=1;
            maxLoopHeapify(array,0);
        }
    }

时间复杂度:
最好情况:O(nlgn)
最坏情况:O(nlgn)
平均情况:O(nlgn)
空间复杂度:O(1),原址排序

3.2 优先队列

3.2.1 什么是优先队列

队列是一种先进先出(FIFO)的数据结构。只能在队尾插入元素,在队首删除元素。最大优先队列是一种特殊的队列,在队尾插入元素,但是在删除元素之前,需要将优先级最高的元素放在队首,然后删除。同理,最小优先队列在删除元素之前,需要将优先级最低的元素放在队首,然后删除。

3.2.2 优先队列的操作

以最大优先队列为例,需要使用最大堆实现。需要的操作有以下四种:
1、insert:在优先队列中插入一个元素。和最大堆中的 heapInsert一样。
2、getMax:获取具有最大值的元素。和最大堆中的heapMax一样。
3、extractMax:获取具有最大值的元素,并删除。和最大堆中的heapIncreaseKey一样。
3、increaseKey:增加优先队列中某个元素的值。和最大堆中heapIncreaseKey一样。

3.2.3 Java中的PriorityQueue

1、PriorityQueue是基于优先级堆的无界优先级队列;
2、优先级队列的元素按照其自然顺序进行排序,最小值在队头。或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。需要重写compare方法;
3、优先级队列不允许使用 null 元素。依靠自然顺序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。
4、此实现为入队和出队方法(offer、poll、remove() 和 add())提供 O(log(n)) 时间;为 remove 和 contains 方法提供线性时间;为获取方法(peek、element 和 size)提供固定时间。
5、构造方法:
构造方法
主要实现第4个,根据指定的比较器对元素排序
首先构造学生类,包括语文、数学、英语成绩

public class Student {
    // 姓名
    private String name;
    // 语文成绩
    private double chinese;
    // 数学成绩
    private int math;
    // 英语成绩
    private int english;

    public Student(String name, double chinese, int math, int english) {
        super();
        this.name = name;
        this.chinese = chinese;
        this.math = math;
        this.english = english;
    }

    public Student() {
        super();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getChinese() {
        return chinese;
    }

    public void setChinese(double chinese) {
        this.chinese = chinese;
    }

    public int getMath() {
        return math;
    }

    public void setMath(int math) {
        this.math = math;
    }

    public int getEnglish() {
        return english;
    }

    public void setEnglish(int english) {
        this.english = english;
    }

    public double getSum() {
        return this.chinese + this.math + this.english;
    }

}

按照语文成绩从大到小构造优先队列

package cn.itcast_07;

import java.util.Comparator;
import java.util.PriorityQueue;

public class StudentDemo {
    public static void main(String[] args) {
        // 根据语文成绩从大到小创建优先队列
        PriorityQueue<Student> ts = new PriorityQueue<Student>(new Comparator<Student>() {
            @Override
            //重写方法
            public int compare(Student s1, Student s2) {
                if(s1.getChinese()<s2.getChinese()){
                    return 1;
                }
                if(s1.getChinese()==s2.getChinese()){
                    return 0;
                }else return -1;
            }
        });

        Student s1 = new Student("A",11.1,12,12);
        Student s2 = new Student("B",12.2,12,12);
        ts.add(s1);
        ts.add(s2);
        Student s3=ts.poll();
        System.out.println(s3.getName() + "\t" + s3.getChinese() + "\t"
                + s3.getMath() + "\t" + s3.getEnglish());
        //遍历集合
        for (Student s : ts) {
            System.out.println(s.getName() + "\t" + s.getChinese() + "\t"
                    + s.getMath() + "\t" + s.getEnglish());
        }
    }
}

四、注意事项

1、堆排序的时间复杂度为 O(nlgn) ,首先建立堆的时间复杂度是 O(n) ,在对建立好之后,需要n-1次交换和维护堆的性质,所以这部分的时间复杂度是 O(nlgn) ,总的时间复杂度是 O(nlgn)
2、堆排序是不稳定的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值