堆排序

堆排序

什么是堆?

堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点(任一非叶子节点的键值都不大于或者不小于其左右孩子节点的键值)。堆分为大顶堆和小顶堆,分别满足如下性质:
小顶堆: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 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()


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值