数据结构与算法——堆排序

一、开篇说明

上篇博客讲述了各个排序算法包括选择、插入、希尔、归并及快速排序算法,详细可见https://blog.csdn.net/weixin_36279234/article/details/102881104。这篇博客补充上篇博客留下未讲述的堆排序。主要介绍堆的概念,以及基于堆实现的优先队列,因为堆排序就是基于它的思想来实现的,最后介绍堆排序的算法。

二、算法详解

2.1 优先队列

       在很多应用中,比如生活中息息相关的手机,要处理应用程序的优先级,当手机用来打游戏时,突然来电话了,手机中来电的优先级肯定要比游戏的高,手机就需要先处理来电的事件。在这种情况下,就需要一个合适的数据结构来实现它,他需要支持两种操作:删除最大的元素和插入新元素,这种数据类型叫做优先队列。这里将介绍基于二叉堆数据结构的一种优先队列的经典实现方法。以实现高效地删除最大元素和插入元素操作。

初级实现:

(1)无序数组实现

实现优先队列的简单方法之一就是基于栈的代码实现。插入元素和push()方法一样,要实现删除最大元素,可以添加一段类似选择排序的代码,通过内循环找出最大元素放在数组边界,然后调用pop()方法删除最大元素

(2)有序数组实现

这种方式就是在insert方法中添加一段是数组中元素向右移动的代码,以使插入的元素在合适的位置中,使数组有序。删除最大的元素就和栈的pop()方法一样。

对以上两种初级实现的性能来说,无序数组的插入即栈的push()方法是常数操作时间,而删除元素则是线性时间的(因为选择排序需要对整个数组进行比较)。有序数组则相反,插入的时候进行排序即线性时间的操作,而删除元素则是常数时间。接下来将讨论基于堆的数据结构来实现以保证这两种操作都能更快的执行(对数级别)

2.2 二叉堆(简称堆)

2.2.1 定义

当一颗二叉树的每个节点大于等于它的两个字节点时,他被称为堆有序

二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储(不使用数组中的第一个位置)

下图是一棵堆有序的完全二叉树,它的每个节点(除叶子节点外)都大于或等于它的两个子节点,根节点是堆有序的二叉树中的最大节点即存储的是最大元素,我们将这样一组数据从上到下从左到右的存储在一个数组中,根节点放在数组下标为1的位置,以此类推。

2.2.2 算法

/**
 * 基于堆的泛型优先队列的实现
 * @param <Key>
 */
public class MaxPQ<Key extends Comparable<Key>> {

    private Key[] pq;
    private int N = 0;

    public MaxPQ(int max) {
        pq = (Key[]) new Comparable[max+1];
    }

    /**
     * 向堆中插入一个元素
     * @param k
     */
    public void insert(Key k) {
        pq[++N] = k;
        swin(N);
    }

    /**
     * 返回最大元素
     * @return
     */
    public Key max() {
        return pq[1];
    }

    /**
     * 删除并返回最大元素
     * @return
     */
    public Key delMax() {
        Key max = pq[1];
        exch(1, N--);
        pq[N+1] = null;
        sink(1);
        return max;
    }

    /**
     * 判断队列是否为空
     * @return
     */
    public boolean isEmpty() {
        return N == 0;
    }

    /**
     * 返回元素个数
     * @return
     */
    public int size() {
        return N;
    }

    public void show() {
        for (int i = 0; i <= N; i++) {
            System.out.print(pq[i]+",");
        }
        System.out.println();
    }

    /**
     * 比较两个元素的大小
     * @param i
     * @param j
     * @return
     */
    private boolean compare(int i, int j) {
        return pq[i].compareTo(pq[j]) < 0;
    }

    /**
     * 交换两个元素
     * @param i
     * @param j
     */
    private void exch(int i, int j) {
        Key temp = pq[i];
        pq[i] = pq[j];
        pq[j] = temp;
    }

    /**
     * 上浮一个元素,使它找到合适的位置
     * @param k
     */
    private void swin(int k) {
        while (k > 1 && compare(k/2, k)) {
            exch(k/2, k);
            k = k/2;
        }
    }

    /**
     * 下沉一个元素,使它找到合适的位置
     * @param k
     */
    private void sink(int k) {
        while (N >= 2*k) {
            int j = 2*k;
            if (j < N && compare(j, j+1))
                j++;
            if (!compare(k, j))
                break;
            exch(k, j);
            k = j;
        }
    }

    private static char randomChar() {
        Character[] c = {'A','B','C','D','E','F','G','H','I','J','K','V','L','M','N',
                'O','P','Q','R','S','T','U','V','W','X','Y','Z'};
        Random random = new Random();
        int i = random.nextInt(26);
        return c[i];
    }

    public static void main(String[] args) {
        int n = 11;
        MaxPQ pq = new MaxPQ(n);
        for (int i = 0; i < n; i++) {
            pq.insert(randomChar());
        }
        System.out.println(pq.delMax());
        pq.show();
    }

}

这里给读者剖析两个最重要的方法:swin()方法和sink()方法

swin()方法由下至上的上浮一个元素,如图,索引为5的元素为T大于它的父节点P元素调用exch()方法交换它们在数组间的位置,在与它的父节点元素S比较,显然T>P,故交换它们的位置,最后变成堆有序。

sink()方法是由上至下的下沉一个元素,如图索引为2的元素H,先比较他的两个子节点元素大小,在这里显然S>P,再用H去跟大的元素S进行比较,显然H<S,故交换它们的位置,如果它小于两个子节点的元素就结束循环,以此类推,直到整个二叉树变成堆有序。

在上述基于二叉堆实现的优先队列中,每次插入元素都放在数组的最后一位,调用swin()将这个元素放在数组中合适的位置,每次删除最大的元素时,直接返回数组中第二个元素(再次说明,数组中第一个元素为空不使用),再将数组中最后一个元素和第二个元素交换位置,调用sink()方法,使整个二叉树堆有序。

2.2.3 堆排序

通过上面介绍的二叉堆,就不难想到堆排序如何实现的,每次就只需要取数组中的第二个元素然后删除它就可以了,这样输出的元素就是有序的了。

(1)堆的构造

一种简单的方式是从左到右遍历数组调用swin()方法来保证整个二叉树为堆有序。但是有一个更聪明的方法就是从右至左用sink()方法构造二叉堆,即从数组的n/2(n为数组的长度)处开始调用sink()方法,一直到数组索引为1的位置,这样就可以保证二叉树为堆有序。这样将减少比较次数提高效率。

(2)算法实现

/**
 * 堆排序
 */
public class HeapSort extends Sort {

    @Override
    protected void sort(Comparable[] a) {
        int N = a.length;
        //初始化堆的构造
        for (int k = N/2; k >= 1; k--) {
            sink(a, k, N);
        }
        //堆排序
        while (N > 1) {
            exch(1, --N, a);
            sink(a, 1, N);
        }

    }

    /**
     * 下沉元素
     * @param a 数组
     * @param k 指定的下沉的元素
     * @param N 数组大小
     */
    private void sink(Comparable[] a, int k, int N) {
        while (--N >= 2*k) {
            int j = 2*k;
            if (j < N && compare(a[j], a[j+1]))
                j++;
            if (!compare(a[k], a[j]))
                break;
            exch(k, j, a);
            k = j;
        }
    }

    public static void main(String[] args) {
        Comparable[] arr = {null,'S','O','R','T','E','X','A','M','P','L','E'};
        new HeapSort().sort(arr);
        show(arr);
    }
}

Sort类型请见上篇博客,sort()方法中for循环保证数组是一个堆有序的数组即堆的构造。下图是堆的构造的轨迹

while循环将最大元素a[1]和a[n]交换并调用sink方法修复堆,如此往复,整个数组就变成了有序的了。如图

2.2.3 堆排序性能

堆排序的空间复杂度就为1,从整个排序过程来看,分为两部分,第一部分为堆的构造,第二部分为排序。堆的构造在至少需要N次比较;而排序时间复杂度为2N*lgN,这个来自于每次下沉需要执行两个比较,一次是找出当前节点子节点的较大元素,一次用来与较大子节点比较价差是否需要交换位置,比较的次数为lgN,两个比较为2lgN,N*2lgN表示需要循环N次。

三、参考资料

算法第四版

 

           不忘初心,死抠细节。仅以此博献给我伟大的java语言,如有不当之处,欢迎大神指正。谢谢!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值