排序算法 | 堆排序算法原理及实现和优化

在学习堆排序之前,首先需要了解堆的含义:在含有 n 个元素的序列中,如果序列中的元素满足下面其中一种关系时,此序列可以称之为

  • ki ≤ k2i 且 ki ≤ k2i+1(在 n 个记录的范围内,第 i 个关键字的值小于第 2i 个关键字,同时也小于第 2i+1 个关键字)
  • ki ≥ k2i 且 ki ≥ k2i+1(在 n 个记录的范围内,第 i 个关键字的值大于第 2i 个关键字,同时也大于第 2i+1 个关键字)

对于堆的定义也可以使用完全二叉树来解释,因为在完全二叉树中第 i 个结点的左孩子恰好是第 2i 个结点,右孩子恰好是 2i+1 个结点。如果该序列可以被称为堆,则使用该序列构建的完全二叉树中,每个根结点的值都必须不小于(或者不大于)左右孩子结点的值。

以无序表 {49,38,65,97,76,13,27,49} 来讲,其对应的堆用完全二叉树来表示为:

图 3 无序表对应的堆

提示:堆用完全二叉树表示时,其表示方法不唯一,但是可以确定的是树的根结点要么是无序表中的最小值,要么是最大值。

堆排序的原理

堆排序的基本思想是:通过将无序表转化为堆,可以直接找到表中最大值或者最小值,然后将其提取出来,令剩余的记录再重建一个堆,取出次大值或者次小值,如此反复执行就可以得到一个有序序列,此过程为堆排序。

堆排序过程的代码实现需要解决两个问题:

  • 如何将得到的无序序列转化为一个堆;
  • 在输出堆顶元素之后(完全二叉树的树根结点),如何调整剩余元素构建一个新的堆。

首先先解决第 2 个问题。图 3 所示为一个完全二叉树,若去除堆顶元素,即删除二叉树的树根结点,此时用二叉树中最后一个结点 97 代替,如下图所示:

此时由于结点 97 比左右孩子结点的值都大,破坏了堆的结构,所以需要进行调整:首先以堆顶元素 97 同左右子树比较,同值最小的结点交换位置,即 27 和 97 交换位置:

由于替代之后破坏了根结点右子树的堆结构,所以需要进行和上述一样的调整,即令 97 同 49 进行交换位置:

通过上述的调整,之前被破坏的堆结构又重新建立。从根结点到叶子结点的整个调整的过程,被称为“筛选”。

解决第一个问题使用的就是不断筛选的过程,如下图所示,无序表 {49,38,65,97,76,13,27,49} 初步建立的完全二叉树,如下图所示:

在对上图做筛选工作时,规律是从底层结点开始,一直筛选到根结点。对于具有 n 个结点的完全二叉树,筛选工作开始的结点为第 ⌊n/2⌋个结点(此结点后序都是叶子结点,无需筛选)。

所以,对于有 8 个结点的完全二叉树,筛选工作从第 4 个结点 97 开始,由于 97 > 49 ,所以需要相互交换,交换后如下图所示:

然后再筛选第 3 个结点 65 ,由于 65 比左右孩子结点都大,则选择一个最小的同 65 进行交换,交换后的结果为:

然后筛选第 2 个结点,由于其符合要求,所以不用筛选;最后筛选根结点 49 ,同 13 进行交换,交换后的结果为:

交换后,发现破坏了其右子树堆的结构,所以还需要调整,最终调整后的结果为:

堆排序的实现

在实现中用到了"数组实现的二叉堆的性质"。在第一个元素的索引为 0 的情形中:

  • 性质一:索引为 i 的左孩子的索引是 (2*i+1);
  • 性质二:索引为 i 的右孩子的索引是 (2*i+2);
  • 性质三:索引为 i 的父结点的索引是 floor((i-1)/2);

所以利用最大堆进行排序的整个过程为:

  1. 初始化堆:将数列a[0…n]构造成最大堆;
  2. 交换数据:将a[0]和a[n]交换,使a[n]是a[0…n]中的最大值;然后将a[0…n-1]重新调整为最大堆。接着,将a[1]和a[n-1]交换,使a[n-1]是a[0…n-1]中的最大值;然后将a[0…n-2]重新调整为最大值。依次类推,直到整个数列都是有序的。
public class HeapSort {

    public static void main(String[] args) {
        int[] array = {5, 9, 1, 9, 5, 3, 7, 6, 1};// 待排序数组
        sort(array);
        print(array);
    }

    /**
     * 从小到大排序
     */
    public static void sort(int array[]) {
        int n = array.length - 1;
        // 从(n/2) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。
        for (int i = n / 2; i >= 0; i--) {
            heapAdjust(array, i, n);
        }

        // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
        for (int i = n; i > 0; i--) {
            // 交换array[0]和array[i]。交换后,array[i]是array[0...i]中最大的。
            swap(array, 0, i);
            
            // 调整array[0...i-1],使得array[0...i-1]仍然是一个最大堆。
            // 即,保证array[0]是array[0...i-1]中的最大值。
            heapAdjust(array, 0, i - 1);
        }
    }

    /**
     * 最大堆的向下调整算法。
     * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
     * 其中,N为数组下标索引值,如数组中第1个数对应的N为0。
     *
     * @param array 待排序的数组
     * @param s     被下调节点的起始位置(一般为0,表示从第1个开始)
     * @param n     截至范围(一般为数组中最后一个元素的索引)
     */
    public static void heapAdjust(int array[], int s, int n) {
        // 下面循环中每轮循环的父节点值或者理解为下轮循环的父节点值
        int temp = array[s];
        for (int i = 2 * s + 1; i <= n; i *= 2 + 1) {
            // i 是左孩子,i+1 是右孩子
            if (i < n && array[i] < array[i + 1]) {
                i++;    // 左右两孩子中选择较大者,即array[i + 1]
            }
            if (temp >= array[i]) {
                break;  // 父节点值最大,调整结束
            }

            // 将父节点和相应的左右孩子节点中最大的值给父节点
            array[s] = array[i];
            // 下轮循环的父节点索引
            s = i;
        }
        array[s] = temp;    // 最后将temp值赋给array[s]
    }

    /** 交换数组中两个元素的位置 */
    public static void swap(int array[], int low, int high) {
        int temp = array[low];
        array[low] = array[high];
        array[high] = temp;
    }

    /** 打印数组 */
    public static void print(int array[]) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + "   ");
        }
        System.out.println();
    }
}
堆排序的特点及性能

提示:代码中为了体现构建堆和输出堆顶元素后重建堆的过程,堆在构建过程中,采用的是堆的第二种关系,即父亲结点的值比孩子结点的值大;重建堆的过程也是如此。

堆排序在最坏的情况下,其时间复杂度仍为O(nlogn)。这是相对于快速排序的优点所在。同时堆排序只需要一个用于记录交换(temp)的辅助存储空间,所需的运行空间很小。

堆排序是不稳定的算法,它不满足稳定算法的定义。它在交换数据的时候,是比较父结点和子节点之间的数据,所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化。

算法稳定性 – 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值