漫谈算法2优先队列与堆排序
Posted Java架构师进阶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了漫谈算法2优先队列与堆排序相关的知识,希望对你有一定的参考价值。
本文是“漫谈算法”系列的第二篇,主要内容是介绍优先队列的基本形式和初级实现,引出堆排序,并提出基于堆排序的优先队列实现,并介绍一种高级形式的优先队列:索引优先队列。
考虑以下问题:输入N个数字,需要从中找出最大(或是最小的)前M个整数。在某些应用场景下,N十分巨大甚至无限而M很小,文章后面均以寻找最大元素为准,最小类似。解决问题的一种方法是将输入排序然后输出前M个最大的元素,但N很大时排序代价会十分高昂。这里,我们可以使用一种合适的数据结构-优先队列,它支持两种操作:删除最大元素和插入元素,它能够高效地选择队列的最大元素,而避免排序。
优先队列是一种抽象数据类型,表示了一组数值和对这些值的操作,其中最重要的操作为插入元素insert()和删除最大元素delMax(),同时为保证灵活性,我们要在API中实现泛型,用Comparable接口的数据类型作为元素类型,定义的API如下所示:
public class MaxPQ<Key extends Comparable<Key>> {
//3种构造函数:空参、创建初始容量cap的优先队列、由元素数组创建优先队列
public MaxPQ() { }
public MaxPQ(int cap) {}
public MaxPQ(Key[] a) {}
//插入元素key
public void insert(Key k) {}
//删除并返回最大元素
public Key delMax() {}
//返回最大元素
public Key getMax() {}
//返回队列是否为空
public boolean isEmpty() {}
//返回队列中元素个数
public int size() {}
}
有序或无序的数组或链表是实现优先队列的起点,它们可以简单地实现MaxPQ,如有序数组实现MaxPQ,insert()插入时将所有较大的元素向右移动一格以保证数组有序,delMax()删除最大元素时直接返回并删除数组最后一位,无序数组、链表等初级实现类似。
对于优先队列来说,上述初级操作中,插入元素和删除最大元素两个操作之一在最坏情况下需要线性时间来完成,而一种基于堆的实现能保证这两个操作能够更快地执行,如下表所示。
优先队列的各种实现的时间复杂度
数据结构 | insert() |
delMax() |
有序数组 | N | 1 |
无序数组 | 1 | 1 |
无序列表 | 1 | N |
堆 | logN |
logN |
当二叉树的每个结点都大于等于(或小于等于)其两个子结点时,称为堆有序,而二叉堆是一组堆有序的完全二叉树,并在数组中按层级存储(不使用数组中第一个位置),即结点k的父结点位置为[k/2],子结点位置为2k和2k+1。且若父结点大于等于(小于等于)其子结点时,称为大(小)根堆,大根堆中元素最大的就是二叉树的根结点,且一个大小为N的完全二叉树的高度级别为logN。因此,我们可以用大根堆实现对数级别的优先队列。
我们用长度为N+1的私有数组pq[N+1]表示大小为N的堆,堆元素放在pq[1]到pq[N]中。在对一个初始数组完成堆有序的建堆过程,常常会遇到两种操作:1,上浮swim:当堆有序状态因为某结点比它父结点更大而被打破时,那就需要通过交换它和它的父结点进行修复,同时,交换后这个结点可能还比现有的父结点更大。
因此,需要一次次地进行比较交换,直到这个结点遇到更大的父结点为止。2,下沉sink:当堆有序状态因为某结点比它的两个子结点之一更小而被打破,同理,也需要不断比较交换这个结点和它的子结点,直到遇到更小的结点。
优先队列的堆实现:基于swim()和sink()两个操作,我们可以轻松地实现优先队列的两个主要操作。插入元素:将新元素插入到数组末尾,并利用swim()让新元素上浮到合适位置。删除最大元素:从数组顶端删除最大元素pq[1]并将数组最后元素放到顶端,再利用sink()将顶端元素下沉到合适位置。优先队列的具体堆实现如下:
public class MaxPQ<Key extends Comparable<Key>> {
private int n; // 元素个数
private Key[] pq; // 优先队列
public MaxPQ() {
this(1);
}
public MaxPQ(int cap) {
this.n = 0;
this.pq = (Key[]) new Comparable[cap + 1];
}
//基于数组创建优先队列
public MaxPQ(Key[] a) {
this.n = a.length;
this.pq = (Key[]) new Comparable[n + 1];
for (int i = 1; i <= n; i++)
pq[i] = a[i-1];
// 堆有序状态
for(int i = n/2; i >= 1; i--)
sink(i);
}
public void insert(Key k) {
// 堆底插入元素,上浮,增大数组
pq[++n] = k;
swim(n);
}
public Key delMax() {
Key r = pq[1];
// 根结点与堆底结点交换,根结点下沉构造堆有序,减小数组
exch(1, n--);
sink(1);
pq[n + 1] = null; // 防止对象游离
return r;
}
public Key getMax() {
return pq[1];
}
public boolean isEmpty() {
return n == 0;
}
public int size() {
return n;
}
//堆操作:swim()和sink
//下标k的元素上浮:k与其父结点比较,优先级大上浮
private void swim(int k) {
while (k > 1 && less(k / 2, k)) {
exch(k, k / 2);
k = k / 2;
}
}
//下标k的元素下沉:结点k与其子结点比较,若小下沉
private void sink(int k) {
while (2 * k <= n) {
int j = 2 * k;
//
if (j < n && less(j, j + 1))
j++;
if (!less(k, j))
break;
exch(k, j);
k = j;
}
}
//交换
private void exch(int i, int j) {
Key temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
//比较
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
}
在很多应用中,允许用户直接引用优先队列中的元素很有必要,而不是只能获得队列中的最大元素。做到这一点的是索引优先队列IndexMaxPQ,它为每一个元素设置一个索引,它的实现基于三个数组:堆数组pq,元素数组keys和逆向索引qp;keys存储元素,堆数组中存储的是元素在keys数组中的索引。
即keys[pq[i]]能得到存储的元素,qp则是逆向索引,它能由索引pq[i]得到索引指向元素在堆数组中的下标。插入元素和删除最大元素操作与MaxPQ操作类似,也不再具体实现了。
最后基于上面的堆操作,我们可以实现一种经典的排序算法-堆排序,堆排序可以分为两个阶段。在建堆过程中,我们将原始数组从底向上进行下沉操作,使得整个数组满足堆有序;在排序阶段,每次将堆顶最大元素与堆尾元素交换,缩小数组,再下沉堆顶元素,使得整个数组有序。其具体实现如下:
public class HeapSort {
public static void Sort(Comparable[] a) {
int n = a.length;
// 自底向上构建堆:下沉操作
for (int i = n / 2; i >= 1; i--)
sink(a, i, n);
// 每次根结点与堆底元素交换,再下沉;最后整个数组有序
while (n > 1) {
exch(a, 1, n--);
sink(a, 1 , n);
}
}
//a[i]在Comparable[1..n]数组中做下沉操作
private static void sink(Comparable[] a, int i, int n) {
while (2 * i <= n) {
int j = 2 * i;
if (j < n && less(a, j, j + 1))
j++;
if (!less(a, i, j))
break;
exch(a, i, j);
i = j;
}
}
//其他操作如上
}
未来社,专注于未来CTO的培养。
如果大家感兴趣可以加技术助手Tina的微信咨询(二维码在页面底部)
以上是关于漫谈算法2优先队列与堆排序的主要内容,如果未能解决你的问题,请参考以下文章