堆排序
什么是堆?
堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点(任一非叶子节点的键值都不大于或者不小于其左右孩子节点的键值)。堆分为大顶堆和小顶堆,分别满足如下性质:
小顶堆: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(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源码
/*
*堆排序算法
*@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);
}
}
}
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 lst
def 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:
break
def 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()