[算法]堆排序

前言

        堆排序(heapsort),是一种时间复杂度为O(nlgn)的排序方法。用到了数据结构“堆”。本篇文章将解答什么是“堆”?如何用“堆”进行排序?

二叉堆

首先来回答什么是“堆”。

        堆通常是一个可以被看做一棵完全二叉树的数组对象,它可以被看成一种近似二叉树的结构,除了最底层外,其他位置是完全充满的。

在本篇文章所讲述的堆排序里,用的是“二叉堆”,它是一种特殊的堆。

下面是一个例子:

上图是以二叉树的形式展示了二叉堆堆的内在关系,树中每个节点上面的编号代表着它在数组中的位置。

注意:为了将数组的位置与树中节点的编号对应,我们这里把数组的起始下标定为1。

        从图中可以看出,每个节点下属还有一个左节点和右节点,我们称之为左孩子和右孩子。不难发现一个序号为 i 的节点的左孩子序号为 2i右孩子序号为 2i+1;而左右孩子的父节点的序号为 i/2

即:

//父节点
int PARENT(int i) {
	return i / 2;
}

//左孩子
int LEFT(int i) {
	return 2 * i;
}

//右孩子
int RIGHT(int i) {
	return 2 * i + 1;
}

A.length 与 A.heap-size

        对于一个数组 A 来说,其中有两个特征量:数组的长度 A.length,形成的堆的大小 A.heap-size。值得注意的是,对于数组 A 来说,要建立一个堆不需要包含 A 的所有元素,但堆中的元素一定都属于A。因此,有 A.length ≥ A.heap-size。 可以说,给定一个数组 A,A.length就已经确定且不变了,但 A.heap-size 还要根据对应位置的元素是否符合堆的特性来确定形成多大的堆。这二者的区别到排序时你会有所体会。

二叉堆的分类

二叉堆可以分为两种形式:最大堆最小堆

在最大堆中,最大堆的性质是除了根以外的所有节点 i 都要满足 A[ PARENT( i ) ] ≥ A[ i ]。也就是说父节点要大于等于所有的子节点。

在最小堆中,除了根以外的所有节点 i 都要满足 A[ PARENT( i ) ] ≤ A[ i ]。也就是说父节点要小于等于所有的子节点。

在本章的堆排序中,我们以最大堆为例进行介绍,最小堆的原理大同小异。

二叉堆的基本操作

  • 维护堆 MAX-HEAPIFY( A, i )

MAX-HEAPIFY用于维护最大堆性质。其输入为数组 A 和 下标 i 。

在使用这个操作时,有一个重要前提:以 LEFT( i ) 和 RIGHT( i ) 为根的堆均为最大堆。也就是说,LEFT( i ) 和 RIGHT( i ) 分别是 i 的左子树的最大值以及右子树的最大值。MAX-HEAPIFY的操作就是在 A[ i ],A[ LEFT( i ) ],A[ RIGHT( i ) ] 中选出最大的作为根节点,从而维护最大堆的性质。

例如,我们对下面这个堆进行维护。想在A[ 2 ] = 4 这个位置上维护最大堆的性质,并且其左右子树均为最大堆。

通过4、8、3三个数的对比,发现8最大,那么8应该是根节点,所以我们顺其自然地把 8 和 4 互换。变成了下图: 

 这样操作会破坏掉左子树的最大堆状态,但只需要在这个位置重新进行维护堆的操作就好。

下面我们给出维护堆的伪代码:

MAX-HEAPIFY( A, i )

1        l = LEFT( i )   r = RIGHT( i )

2        if l ≤ A.heap-size and A[ l ] > A[ i ]        largest = l

3        else largest = i

4        if r ≤ A.heap-size and A[ r ] > A[ largest ]        largest = r

5        if largest ≠ i

6                swap A[ i ] and A[ largest ]

7                MAX-HEAPIFY( A, largest )

 如果你能理解上面那个例子,这个代码你一定能看懂。第1-4行是找出根节点以及左右孩子的最大项,然后第5行检查是否根节点就是最大,不是的话就和最大的交换,由于交换后会破坏子树的最大堆性质,那么第7行就是继续维护子树的最大堆性质。

  • 建堆 BUILD-MAX-HEAP( A )

建堆的目的就是将一个大小为 A.length 的数组转换为最大堆。这里面借助了维护堆操作 MAX-HEAPIFY( A, i ),但在这个操作是有前提的:i 节点的左右子树必须是最大堆,这就为建堆提供了自底向上的设计思路。也就是说,应该先把最底层的节点维护成最大堆,然后再维护他们的父节点,直到根节点。这个思路很清晰明了。

我们来看伪代码:

BUILD-MAX-HEAP( A )

1        A.heap-size = A.length

2        for i = ( A.length / 2 ) to 1

3                MAX-HEAPIFY( A, i )

  •  堆排序算法 HEAPSORT( A )

如果你已经清楚最大堆的性质,就会知道,最大二叉堆的根节点一定是整个堆元素的最大值。那么也就是根据这种特性,要把一个数组从小到大排序,只需要每次将最大的根节点元素从堆中取出,放在数组的最末尾,然后将数组最末尾的值来当作根节点,再重新维护一次堆。就像下图一样:

 进行这样的 A.length-1 次操作,我们就可以将整个数组从小到大进行排序。

伪代码如下:

HEAPSORT( A )

1        BUILD-MAX-HEAP( A )

2        for i = A.length to 2

3                swap A[ 1 ] with A[ i ]

4                A.heap-size - -

5                MAX-HEAPIFY( A, 1 )

 以上就是堆排序的算法了,对于时间复杂度的具体分析有些繁琐,有兴趣的话不妨自己推导一下。


最后附上整个C/C++实现的代码:

//查找父节点
int PARENT(int i) {
	return i / 2;
}

//左孩子
int LEFT(int i) {
	return 2 * i;
}

//右孩子
int RIGHT(int i) {
	return 2 * i + 1;
}

//维护堆(最大堆)
void Max_Heapify(int* a, const int size, int i) { //参数:数组,堆大小,父节点

	int l = LEFT(i);                              //获取左右孩子
	int r = RIGHT(i);
	int largest;                                  //创建最大值,largest负责记录三个数中最大的节点

	if (l <= size && a[l] > a[i])                 //比较左孩子
		largest = l;
	else
		largest = i;

	if (r <= size && a[r] > a[largest])           //比较右孩子
		largest = r;

	if (largest != i) {                           //将最大的节点作为父节点
		int t = a[i];
		a[i] = a[largest];
		a[largest] = t;
		Max_Heapify(a, size, largest);            //再将被替换的数的位置维护
	}

	return;
}

//维护堆(最小堆)
void Min_Heapify(int* a, const int size, int i) {  //参数:数组,堆大小,父节点

	int l = LEFT(i);                               //获取左右孩子
	int r = RIGHT(i);
	int smallest;                                  //创建最小值,smallest负责记录三个数中最大的节点

	if (l <= size && a[l] < a[i])                  //比较左孩子
		smallest = l;
	else
		smallest = i;

	if (r <= size && a[r] < a[smallest])           //比较右孩子
		smallest = r;

	if (smallest != i) {                           //将最大的节点作为父节点
		int t = a[i];
		a[i] = a[smallest];
		a[smallest] = t;
		Min_Heapify(a, size, smallest);            //再将被替换的数的位置维护
	}

	return;
}

//建堆(最大堆)
void Build_Max_Heap(int* a, const int length, int size) {    //参数:数组,数组长度,堆大小

	size = length;

	for (int i = length / 2; i >= 1; i--) {
		Max_Heapify(a, size, i);
	}

	return;
}

//建堆(最小堆)
void Build_Min_Heap(int* a, const int length, int size) {    //参数:数组,数组长度,堆大小

	size = length;

	for (int i = length / 2; i >= 1; i--) {
		Min_Heapify(a, size, i);
	}

	return;
}

//堆排序,从小到大
void Heap_Sort_Up(int* a, int length, int& size) {    //参数:数组,数组长度,堆大小
	Build_Max_Heap(a, length, size);                  //对数组进行整体构造堆
	for (int i = length; i >= 2; i--) {               //从最后一项开始到第二项

		int t = a[i]; a[i] = a[1]; a[1] = t;          //取出根节点
		size--;                                       //堆大小-1
		Max_Heapify(a, size, 1);                      //重新维护堆
	}
	return;
}

//堆排序,从大到小
void Heap_Sort_Down(int* a, int length, int& size) {
	Build_Min_Heap(a, length, size);                  //对数组进行整体构造堆
	for (int i = length; i >= 2; i--) {               //从最后一项开始到第二项

		int t = a[i]; a[i] = a[1]; a[1] = t;          //取出根节点
		size--;                                       //堆大小-1
		Min_Heapify(a, size, 1);                      //重新维护堆
	}
	return;
}

本文是个人对《算法导论》的学习摘要。如有问题,请大家在评论区批评指正! 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值