堆排序

来源:互联网 发布:淘宝店铺怎么优化标题 编辑:程序博客网 时间:2024/05/01 23:16

堆排序

什么是堆?

堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点(任一非叶子节点的键值都不大于或者不小于其左右孩子节点的键值)。堆分为大顶堆和小顶堆,分别满足如下性质:
小顶堆:Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]
大顶堆:Key[i]>=Key[2i+1]&&key>=key[2i+2]
由上述性质可知:大顶堆的堆顶元素(也就是根节点)的键值肯定是所有元素的键值中最大的,小顶堆的堆顶元素的键值是所有元素的键值中最小的。

堆节点的访问

通常堆是通过一维数组来实现的。在起始数组为 0 的情形中:

  • 父节点i的左子节点在位置 (2*i+1);
  • 父节点i的右子节点在位置 (2*i+2);
  • 子节点i的父节点在位置 floor((i-1)/2);

堆排序的思想

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。利用大顶堆(小顶堆)堆顶元素的键值最大(最小)这一特性,使得每次从无序中选择最大元素(最小元素)变得简单。堆排序(HeapSort)是一树形选择排序。堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系(参见二叉树的顺序存储结构),在当前无序区中选择关键字最大(或最小)的记录

堆排序过程

(1)用大根堆排序的基本思想
  • 先将初始序列R[1..n]建成一个大根堆,此堆为初始的无序区
  • 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
  • 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将 R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n- 1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。直到无序区只有一个元素为止。
(2)大根堆排序算法的基本操作:
  •  初始化操作:将R[1..n]构造为初始堆;
  • 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
注意:
  • 只需做n-1趟排序,选出较大的n-1个关键字即可以使得序列递增有序。
  • 用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻堆排序中无序区总是在有序区之前,且有序区是在原序列的尾部由后往前逐步扩大至整个序列为止。

堆的操作

通过以上的讨论,在最大堆的数据结构中,堆中的最大值总是位于根节点。在堆排序的过程中主要有下几种操作:

  • 最大堆调整(Max_Heapify):将堆的末端子结点作调整,使得子结点永远小于父结点
  • 创建最大堆(Build_Max_Heap):将堆所有数据重新排序
  • 堆排序(HeapSort):移除位在第一个数据的根结点,并做最大堆调整的递归运算
下面通过一个例子来详细的阐述一下整个堆排序的过程。
给定一个整形数组a[]={16,7,3,20,17,8},对其进行堆排序。
首先根据该数组元素构建一个完全二叉树,得到

然后需要调整这棵二叉树,使之成为一个最大堆,过程如下:
(1) 首先从第一个非叶子节点从开始调整,在这里就是值为3的那个节点。由于在这个节点其左孩子的值为8, 大于其父节点的值,不符合最大堆的定义。所以在这里要将它们交换,交换后整个二叉树如下:

(2)此时,对于下一个非叶子节点(也就是值为7的节点)来说,其左右孩子节点的值均大于它。所以在这里要从它的孩子节点中选择值最大的一个与其交换。

(3)在进行上一步交换之后,根节点的值(16)小于其左孩子的值(20),不符合最大堆的性质,所以再将它们交换。

(4)值为20的节点和值为16的节点交换后,导致以值为16的节点为根节点的子树不满足最大堆的性质。所以再将16和17交换。

那么到此为止,我们便通过一个无序的数组构建了一个最大堆。在这个过程中每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换。但是在交换之后,可能会造成其它的地方不满足最大堆的性质,这时就要再一次调整所交换的节点,直至完成最大堆的创建。
构建了一个最大堆之后,就可以利用其性质来进行排序了。下面我们来看一下堆排序的过程:
(1) 首先,将根节点与最后一个节点交换

(2) 此时,3位于堆顶,破坏了最大堆的性质,那么就需要进行最大堆的调整,使之再成为一个最大堆:
       
(3)在将堆顶元素和最后一个节点交换

(4)此时最大堆的性质又遭到了破坏,所以需要再一次进行最大堆调整
 
(5)再将堆顶元素(16)与最后一个节点(3)交换

(6) 再进行最大堆的调整

(7)将堆顶元素(8)和最后一个节点(3)交换

(8)最大堆调整

(9)堆顶元素和最后一个节点交换

至此便完成了整个堆排序。从上述的过程我们可以知道,堆排序其实也是一种选择排序,是一种树形选择排序。但是直接选择排序中, 为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面 的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。

Heapify的实现

因为构造初始堆必须使用到调整堆的操作,先讨论Heapify的实现,再讨论如何构造初始堆(即BuildHeap的实现)Heapify函数思想方法每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R交换后,新的无序区R[1..i-1]中只有 R[1]的值发生了变化,故除R[1]可能违反堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是R[low..high]时,只须调整以 R[low]为根的树即可。

"筛选法"调整堆
R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。若R[low].key不小于这两个孩子结点的关键字, 则R[low]未违反堆[性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它的两个孩子结点中关键字较大者进行交换,即 R[low]与R[large](R[large].key=max(R[2low].key,R[2low+1].key))交换。交换后又可能使结点 R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。此过程直至 当前被调整的结点已满足性质,或者该结点已是叶子为止。上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。因此,有人将此方法称为"筛选法"。

算法分析

堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
  • 数据结构:数组
  • 最坏时间复杂度: O(nlogn)
  • 最优时间复杂度: O(nlogn)
  • 平均时间复杂度: 
  • 最差空间复杂度: O(n)共需, O(1)辅助空间
  • 由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
  • 堆排序是就地排序,辅助空间为O(1),
  • 它是不稳定的排序方法。
  • 不是最佳排序算法

算法源码

C源码

#include<stdio.h>#include<malloc.h>void max_heapify(int *a, int heap_size, int i){int l, r, tmp, largest;l = 2*i+1;r = 2*i+2;largest = i;if(l<=heap_size-1 && a[i]<a[l])largest = l;if(r<=heap_size-1 && a[largest]<a[r])largest = r;if(i != largest){tmp = a[i];a[i] = a[largest];a[largest] = tmp;max_heapify(a, heap_size, largest);}}void build_heap(int *a, int heap_size){int i;for(i=(heap_size-1)/2; i>=0;i--){max_heapify(a,heap_size,i);}}void heap_sort(int *a, int heap_size){int i, tmp;build_heap(a,heap_size);for(i=heap_size-1;i>=1;i--){tmp = a[0];a[0] = a[heap_size-1];a[heap_size-1] = tmp;heap_size -= 1;max_heapify(a, heap_size, 0);}}int main(){int a[]={3,6,4,9,8,2,5,1,10,7};int size = 10;printf("Before sort:");for(int i=0; i<=size-1; i++){printf("%3d",a[i]);}printf("\n");heap_sort(a,size);printf("After sort:");for(int i=0;i<=size-1;i++)printf("%3d",a[i]);printf("\n"); return 0;}

C++源码

#include <iostream>#include<algorithm>using namespace std;void HeapAdjust(int *a,int i,int size)  //调整堆 {    int lchild=2*i;       //i的左孩子节点序号     int rchild=2*i+1;     //i的右孩子节点序号     int max=i;            //临时变量     if(i<=size/2)          //如果i是叶节点就不用进行调整     {        if(lchild<=size&&a[lchild]>a[max])        {            max=lchild;        }            if(rchild<=size&&a[rchild]>a[max])        {            max=rchild;        }        if(max!=i)        {            swap(a[i],a[max]);            HeapAdjust(a,max,size);    //避免调整之后以max为父节点的子树不是堆         }    }        }void BuildHeap(int *a,int size)    //建立堆 {    int i;    for(i=size/2;i>=1;i--)    //非叶节点最大序号值为size/2     {        HeapAdjust(a,i,size);        }    } void HeapSort(int *a,int size)    //堆排序 {    int i;    BuildHeap(a,size);    for(i=size;i>=1;i--)    {        //cout<<a[1]<<" ";        swap(a[1],a[i]);           //交换堆顶和最后一个元素,即每次将剩余元素中的最大者放到最后面           //BuildHeap(a,i-1);        //将余下元素重新建立为大顶堆           HeapAdjust(a,1,i-1);      //重新调整堆顶节点成为大顶堆    }} int main(int argc, char *argv[]){     //int a[]={0,16,20,3,11,17,8};    int a[100];    int size;    while(scanf("%d",&size)==1&&size>0)    {        int i;        for(i=1;i<=size;i++)            cin>>a[i];        HeapSort(a,size);        for(i=1;i<=size;i++)            cout<<a[i]<<"";        cout<<endl;    }    return 0;}

Java源码

/**堆排序算法*@author fgs*/public class HeapSort{public static void main(String[] args){int[] a = new int[] {3,6,4,9,8,2,5,1,10,7};heapSort(a);for(int i=0; i<=a.length-1;i++)System.out.println(a[i]+"");}/* * @parameter a 包含待排序数据的数组 * @parameter start 构建最大堆的起始位置 * @parameter end 构建最大堆的结束位置 */public static void heapAdjust(int[] a, int start, int end){int tmp =0;tmp = a[start];for(int i =2*start; i<end; i*=2){if (i<end && a[i]<a[i+1])i++;if(tmp>=a[i])break;a[start] = a[i];start = i;}a[start] = tmp;}//筛选法调整堆public static void heapSort(int[] a){for(int i = a.length/2-1; i>=0; i--)heapAdjust(a, i, a.length-1);for(int j = a.length-1; j>=0; j--){int tmp = 0;tmp = a[0];a[0] = a[j];a[j] = tmp;heapAdjust(a, 0, j-1);}}}
Python源码

def heap_sort(lst):    '堆排序算法'    for start in range((len(lst)-2)//2, -1, -1):        #从第一个非叶节点开始调整堆,使之成为一个最大堆        heapify(lst,start, len(lst) -1)    for end in range(len(lst)-1,0, -1):        #交换堆顶元素和堆中最后一个元素        lst[0], lst[end] = lst[end], lst[0]        #调整堆使之再成为最大堆        heapify(lst,0,end-1)    return lstdef heapify(lst,start, end):    root = start    while True:        child = 2 * root + 1 #左孩子        if child > end: break        if child + 1 <= end and lst[child] < lst[child+1]:            child +=1            #从该节点以及其左右孩子节点中选择值最大的节点,成为新子树的根,            #使其满足最大堆的性质。        if lst[root] < lst[child]:            lst[root], lst[child] = lst[child], lst[root]            #调整以被交换的子节点为根的子树            root = child        else:            breakdef main():    lst = [3,6,4,9,8,2,5,1,10,7]    heap_sort(lst)    for i in range(0, len(lst), 1):        print(lst[i])if __name__ == "__main__":    main()


原创粉丝点击