前言:
正文:
3.1.0 堆排序的意义
许多 应用程序都需要处理有序的元素,但不一定要求它们全部有序,或是不一定要一次就将它们排序。很多情况下,我们会收集一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前键值最大的元素。(比如我们需要搜索的最优情况)
这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素和插入元素。这种类型叫做优先队列。
堆排序重要的意义其实并不在与排序本身,而是用一种数据结构来管理算法的思路,而堆数据结构也可以用来构造有限队列。“堆”这个词最初是在堆排序中提出的,但后来就逐渐之“废料收集存储区”,就像List和Java中堆的含义。
堆(二叉堆)数据结构是一种数据对象,他可以被视为一棵完全二叉树。树中每个节点域数组中存放该节点值的那个元素对应。树的每一层都是填满的,最后一层可能除外(最后一层从一个节点的左子树开始填)。
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;
}
}
3.1.1 基础知识
- 堆是一棵采用顺序存储结构的完全二叉树,k1是根结点;
- 堆的根结点是关键字序列中的最小(或最大)值,分别称为小(或大)根堆;
- 从根结点到每一叶子结点路径上的元素组成的序列都是按元素值(或关键字值)非递减(或非递增)的;
- 堆中的任一子树也是堆。
表示堆的数组A是一个具有两个属性的对象:
length[A]是数组中的元素个数,heap-size[A]是存放在A中的堆的元素个数。
Parent(i)
return i/2
Left(i)
return 2i;
Right(i)
return 2i+1;
二叉堆有两种:最大堆和最小堆(小根堆)。在两种堆中,节点内的数值都要满足堆特性。
最大堆:除顶点外,A[Parent(i)]>=A(i)
堆排序通常使用最大堆,而构造有限队列是用小根堆。
堆可以被看成是一棵树,结点在堆中的高度定义为从本结点到叶子的最长简单下降路径上边的数目;定义堆的高度为树根的高度。
- MAX-HEAPIFY过程,其运行时间为O(lgn),是保持最大堆性质的关键。
- BUILD-MAX-HEAP过程,以线性时间运行,可以在无序的输入数组基础上构造出最大堆。
- HEAPSORT过程,运行时间为O(nlgn),对一个数组原地进行排序
- MAX-HEAP-INSERT,HEAP-EXTRACT-MAX,HEAP-INCREASE-KEY和HEAP-MAXIMUM过程的运行时间为O(lgn),可以让堆结构作为优先队列使用。
3.1.3 保持堆的性质(MAX—HEAPIFY)
在下降算法之前我们需要两个函数:比较大小和交换
MAX-HEAPIFY(A, i)
l ← LEFT(i)
r ← RIGHT(i)
n ← heap-size[A]
if l ≤ n and A[l] > A[i]
then largest ← l
else largest ← i
if r ≤ n and A[r] > A[largest]
then largest ← r
if largest ≠ i<span style="white-space:pre"> </span>
then exchange A[i] ←→ A[largest]
MAX-HEAPIFY(A, largest)
private boolean less(in i, int j){//这里的i,j其实是指针
return pg[i].compareTo(pg[j])<0;
}
private void exch(int i, int j){
Key t=pq[i];pq[i]=pq[j];pq[j]=t;
}
private void sink(int k){
while(2*k<=N){//结束条件:到叶子节点,叶子节点最后一个序号是N
int j = 2*k;//将k的左孩子赋值给j
if(j<N&&less(j,j+1)) j++;//左孩子必须存在的情况情况下,和右孩子比较,如果左孩子较小则选右孩子,否则选左孩子(选择最大的)
if(!less(k,j)) break; //看看这个选择的最大的孩子是否比父亲大,这里主要是确定一下右孩子是否存在,因为刚才的比较并没有判断右孩子是否存在
exch(k,j);
k=j;
}
}
private void swim(int k){
while(k<1 && less(k/2,k)){
exch(k/2,k);
k=k/2;
}
}
3.1.4 建堆
我们可以自底向上地用MAX-HEAPIFY来将一个数组A[1..n]变成一个最大堆。
伪代码:
BUILD-MAX-HEAP(A)
heap-size[A] ← length[A]
for i ← 【length[A]/2】 downto 1
do MAX-HEAPIFY(A, i)
初始化:(后n/2向上取整节点都没有孩子,也就是说说他们都是平凡最大堆的根)
保持:调用函数MAX-HEAPIFY保持了(i+1,i+2,..n)的最大根的性质。
终止:i=0时候,每个都是最大堆的根,节点1就是一个最大堆的根。
BUILD-MAX-HEAP的运行时间的一个简单上界:每次调用MAX-HEADPIFY的时间为O(lgn),共有O(n)次调用,故运行时间是O(nlgn).
实际上,我们可以得到一个更加紧缺的界(由于一个n元素堆的高度为lgn 向下取整。并且在任意高度h上,至多有n/(2^(h+1)))个结点。
3.1.5 优先级队列
虽然堆排序算法是一个很漂亮的算法,但是在实际中,快速排序的一个实现往往优于堆排序。本博客主要是应用了堆数据结构来作为高效的优先队列。(priority queue)
优先级队列:一种用来维护有一组元素构成的集合S的数据结构,这一组元素中的每一个都有一个关键字key。一个【最大优先级队列】支持以下操作
- INSERT(S,x): 把元素x插入集合S. S<S∪{x}
- MAXIUM(S): 返回S中具有最大关键字的元素。
- EXTEACT_MAX(S): 去掉并返回S中的具有最大关键字的元素。
- INCREASE_KEY(S,x,k): 将元素x的关键字的值增加到k,这里的k值不能小于x的预案关键字的值。
应用场景
最大优先级队列的一个应用是在一台分时计算机上进行作业调度。(优先级选择)
最小优先级队列可被用于基于时间驱动的模拟器中。
public class MaxPQ<Key Extends Comparable<Key>>{
private Key[] pq;
private int N = 0;
public MaxPQ(int maxN){
pq = (Key[]) new Comparable[maxN+1];
}
pubic boolean isEmpty(){
return N==0;
}
public int size(){
return N;
}
public void insert(Key v){
pg[++N]=v;
swim(N);
}
public Key delMax(){
Key max = pg[1];
exch(1,N--);
pg[N+1]null;
sink(l);
return max;
}
private boolean less(int i, int j);
private void exch(int i, int j);
private void swim(int k);
private void sink(int k);
}
3.1.6 堆排序
- 构造阶段:原始数组重新组织安排进一个堆中
- 下沉排序阶段:从堆中按递减顺序取出所有元素并得到排序结果。
public static void sort(Comparable[] a){
int N = a.length;
for(int k=N/2;k>=1;k--){
sink(a,k,N);
while(N>1){
exch(a,1,N--);
sink(a,1,N);
}
}
}
3.1.7 评价
堆排序在怕徐负载型的研究中有重要的地位。
3.1.7.1 优点
- 堆排序是我们所知的唯一能够同时最优的利用空间和时间的方法(醉话情况下也能保证用~2NlgN次比较和恒定的额外空间)。
- 当空间很紧张的时候他很流行,因为只用几行就能实现较好的性能,甚至机器码也是。
- 用堆实现的优先队列在现代应用程序中越来越重要,因为它能在插入操作和删除最大元素操作混合的动态场景中保证对数级别的运行时间。
3.1.7.2 缺点
- 无法利用缓存:数组元素很少和相邻的其他元素进行比较,因此缓存未命中的次数要远远大于多数比较都在相邻元素间进行的算法。
后记