堆排序(未完结)

作者:disappearedgod
时间:2014-4-14

前言:


本篇根据Thoms H.Cormen & Charles E. Leiserson etc.《Introduction to algorithms》第二版有关内容顺序编写,为了实现方面对内容进行微调,一些内容参考Robert Sedgewick&Kevin Wayne《Algorithms》第四版代码。
鸣谢:wikipedia 中英文有关内容。

正文:


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。当MAX-HEAPIFY被调用时,我们假定:
Left(i) Right(i)为根的两颗二叉树是最大堆(但这是A[i]可能小于其子女,也就是说,违反了最大堆性质。这是我们需要一个下降算法,让数字从父亲下降到儿子。

在下降算法之前我们需要两个函数:比较大小和交换
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;
  }
}

例题:
(算法导论6.2-6)
证明对一个大小为n的堆,MAX-HEAPIFY的最坏运行时间为Ω(lgn).
If you put a value at the root that is less than every value in the left and right subtrees, then MAX-HEAPIFY will be called recursively until a leaf is reached. To make the recursive calls traverse the longest path to a leaf, choose values that make MAX-HEAPIFY always recurse on the left child. It follows the left branch when the left child is ≥ the right child, so putting 0 at the root and 1 at all the other nodes, for example, will accomplish that. With such values, MAX-HEAPIFY will be called h times (where h is the heap height, which is the number of edges in the longest path from the root to a leaf), so its running time will be (h) (since each call does θ(1) work), which is θ(lg n). Since we have a case in which MAX-HEAPIFY’s running time is θ(lg n), its worst-case running time is Ω(lg n).



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 缺点

  • 无法利用缓存:数组元素很少和相邻的其他元素进行比较,因此缓存未命中的次数要远远大于多数比较都在相邻元素间进行的算法。

后记



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值