八大排序算法(六)——优先队列、堆和堆排序

6.1 API

优先队列是一种抽象数据类型,它表示了一组值和对这些值的操作。优先队列最重要的操作就是删除最大元素和插入元素。

6.2 初级实现
6.2.1 数组实现(无序)

或许实现优先队列最简单方法就是基于下压栈的代码。insert()方法的代码和栈的完全一样。要实现删除最大元素,可以添加一段类似选择排序的内循环的代码,将最大元素和边界元素交换然后删除它,和栈的pop()方法的实现一样。和栈类似,也可以加入调整数组大小的代码来保证数据结构中至少含有四分之一的元素而又永远不会溢出。

6.2.2 数组实现(有序)

在insert()方法中添加代码,将所有的较大的元素向右边移动一格以使数组保持有序。这样,最大的元素总会在数组的一边,优先队列删除最大元素的操作和栈的pop操作一样了。

6.2.3 链表表示法

和刚才类似,可以用基于链表的下压栈的代码作为基础,而后可以选择修改push()来保证所有元素为逆序并用pop()来删除并返回链表的首元素。

实现栈或是队列与实现优先队列的最大不同在于对性能的要求。对于栈和队列,能够在常数时间内完成所有操作;而对于优先队列,初级实现中,插入和删除最大元素这两个操作之一在最坏情况下需要线性时间来完成。

6.3 堆的定义

数据结构二叉堆可以很好地实现优先队列的基本操作。在二叉堆的数组中,每个元素都要保证大于等于另外两个特点的元素。相应地,这些位置的元素又至少要大于等于数组中的另外两个元素,以此类推。

当一个二叉树的每个节点都大于等于它的两个子节点时,它被称为堆有序。

二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按层级存储。

在堆中,位置k的节点的父节点的位置为k/2(向下取整),而它的两个子节点的位置分别为2k和2k+1。这样再不使用指针的情况下,也可以通过计算数组的索引在树中上下移动:从a[k]向上一层就令k=k/2,向下一层则令k等于2k或2k+1。

用数组实现完全二叉树的结构是很严格的,但它的灵活性已经足以高效地实现优先队列。用它们将能实现对数级别的插入元素和删除最大元素的操作。

一颗大小为N的完全二叉树的高度为logN(向下取整)。

6.4 堆的算法

堆的操作会进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态恢复。这个过程叫做堆的有序化。

堆的有序化过程中会遇到两种情况。当某个节点的优先级上升(或在堆底加入一个新元素),我们需要由下至上恢复堆的顺序。当某个节点的优先级下降(如将根节点替换成一个较小的元素),需要由上至下恢复堆的顺序。

6.4.1 由下至上堆有序化(上浮)
private void swim(int k) {
		while (k > 1 && a[k / 2] < a[k]) {
			swap(a[k / 2], a[k]);
			k = k / 2;
		}
	}
6.4.2 由上至下有序化(下沉)
private void sink(int k) {
		while (2 * k <= N) {
			int j = 2 * k;
			if (j < N && a[j] < a[j + 1]) {
				j++;
			}
			if (!a[k] < a[j]) {
				break;
			}
			swap(a[k], a[j]);
			k = j;
		}
	}

以上是高效实现优先队列的基础。

插入元素。将新元素加到数组末尾,增加堆的大小并让这个新元素上浮到合适的位置。

删除最大元素。从数组顶端删除最大的元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置。

它对优先队列API的实现能够保证插入元素和删除最大元素这两个操作的用时和队列的大小仅成对数关系。

优先队列由一个基于堆的完全二叉树表示,存储于数组pq[1..N]中,pq[0]没有使用。在insert()中,我们将N加1并将新元素添在数组最后,然后用swim()恢复堆的秩序。在delMax()中,从pq[1]得到返回的元素,然后将pq[N]移动到pq[1],将N减一并用sink()恢复堆的秩序。同时还将不再使用的pq[N+1]设为null,以便系统回收它所占的空间。

对于一个含有N个元素的基于堆的优先队列,插入元素操作只需要不超过logN+1次比较,删除最大元素的操作需要不超过2logN次比较。

6.5 堆排序

堆排序可以分为两个阶段。在堆的构造过程中,将原始数组重新安排进一个堆中;然后在下沉排序阶段,从堆中按递减顺序取出所有元素并得到排序结果。

6.5.1 堆的构造

用下沉操作由N个元素构造堆只需要少于2N次比较以及少于N次交换。

private void sort(Comparable[] a) {
		int N = a.length;
		for (int k = N / 2; k >= 1; k--) {
			sink(a, k, N);
		}
		while (N > 1) {
			swap(a, 1, N--);
			sink(a, 1, N);
		}
	}
6.5.2 下沉排序

堆排序的主要工作都是在第二阶段完成的。将堆中最大的元素删除,然后放入堆缩小后数组中空出的位置。这个过程与选择排序有些类似(按照降序而非升序取出所有元素),但所需比较要少得多,因为堆提供了一种从未排序部分找到最大元素的有效防范。

将N个元素排序,堆排序只需少于2NlogN+2N次比较(以及一半次数的交换)。

6.5.3 先下沉后上浮

大多数在下沉排序期间重新插入堆的元素会被直接加入到堆底。正好可以通过免去检查元素是否到达正确位置来节省时间。在下沉中总是直接提升较大的子节点到堆底,然后再使元素上浮到正确的位置。这样几乎将比较次数减少一半,接近归并排序所需的比较次数(随机数组)。但这种方法需要额外的空间,因此在实际应用中只有当比较操作代价较高时才有用。

堆排序在排序复杂度研究中有重要地位,因为它是所知唯一能够同时最优地利用空间和时间的方法——在最坏的情况下它也能保证使用~2NlogN次比较和恒定的额外空间。当空间十分紧张时它很流行,因为它只要几行就能实现较好的性能。但现代系统的很多应用很少使用它,因为它无法利用缓存。数组元素很少和相邻的其他元素进行比较,因此缓存未命中的次数要远远高于大多数比较都在相邻元素之间的算法。

另一方面,用堆实现的优先队列在现代应用程序中越来越重要,因为它能在插入操作和删除最大元素操作混合的动态场景中保证对数级别的运行时间。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值