堆排序算法原理及实现

堆排序是排序中一种比较重要的算法,和快速排序一样,其复杂度也是O(nlogn);同时也是一种原地排序算法:在任何时候,数组中只有常数个元素存储在输入数组以外。堆这种数据结构是处理海量数据比较常见的结构,海量数据的TOP K问题一般可以通过分治法+hash+堆这种数据结构解决。值得注意的是,这里将的“堆”准确的说是二叉堆,逻辑上是一棵类似完全二叉树的数据结构。与内存管理中提到的“堆”是两个不同的概念,后者堆内存类似链表的数据结构。下面首先介绍堆数据结构特性,然后将如何建立最大堆或最小堆,最后再讲解堆排序原理及其实现。

    二叉堆数据结构在物理存储上一般表示为一种数组对象,该数组中的数据按照其逻辑结构树的广度优先算法(队列优先)来存储对应的值。树中的每个结点与数组中存放该结点值的那个元素对应。树的每一层都是填满的,最后一层可能除外(最后一层从一个结点的左子树开始填)。表示堆的数组A是一个具有两个属性的对象: length[A]是数组中元素个数, heap-size[A]是存放在A中的堆的元素个数,就是说,虽然A[0...length[A]-1]中都可以包含有效值,但A[heap-size[A] - 1]之后的元素都不属于相应的堆,此处heap-size[A] <= length[A]。接下来树的根均从A[0]开始,这里与C语言数组索引下标保持一致,索引从0开始计算。给定某个结点的下标i,其父结点PARENT(i),左儿子LEFT(i)和右儿子RIGHT(i)的下标可以简单计算出来:

#define PARENT(i)    i/2

#define LEFT(i)     2*i + 1

#define RIGHT(i)   2*i + 2

一个最大堆(大根堆)可以被看做一棵二叉树和一个数组。如上图所示,逻辑结构为二叉树,物理存储一般为数组。圆圈内的数字表示树中每个结点存储的值,结点上方的数字表示对应的数组下标。数组上下的连线表示父子关系,且父结点总在子结点的左边。图中这棵树的高度为3,存储值为8的左右孩子分别为2与4。

堆分为大根堆与小根堆。在这两种堆中,结点内数值要满足堆特性,其细节则视堆的种类而定。在最大堆中,最大堆特性指的是除了根结点之外的每个结点i,其父结点不小于其左右孩子结点。最小堆则相反,最小堆特性是指除了根以外的每个结点i,其父结点不大于其左右孩子结点。因此在最大堆中,根元素为最大值,而最小堆中,根元素为最小值。

    堆可以看做一颗树,结点在堆中的高度定义为从本结点到叶子的最长简单下降路径上边的数目;定义堆的高度为树根的高度。具有n个元素的堆是基于一棵完全二叉树的,因而其高度为O(lgn)。我们将看到,堆结构上的一些基本操作的运行时间至多与树的高度成正比,为O(lgn)。

如何保持堆的特性?

MAX-HEAPIFY是对最大堆进行操作的重要的子程序。其输入为一个数组A和下标i,当MAX-HEAPIFY被调用时,我们假定以LFEF(i)和RIGHT(i)为根的两棵二叉树都是最大堆,但这时A[i]可能小于其子女,这样就违反了最大堆性质。MAX-HEAPIFY让A[i]在最大堆中“下降”,使以i为根的子树成为最大堆。

MAX-HEAPIFY(A, i)

1  l <------ LEFT(i)

2  r <------ RIGHT(i)

3  if l <= heap-size[A] and A[l] > A[i]

4      then  largest <------ l

5      else largest  <------ i

6  if r <= heap-size[A] and A[r] > A[largest]

7      then largest <------ r

8  if largest != i

9      then exchange A[i] <------> A[largest]

10          MAX-HEAPIFY(A, largest)



MAX-HEAPIFY实际上每一步在父结点和其孩子结点中寻找最大值作为最大堆根结点, 即在算法每步中,从元素A[i],A[LEFT(i)] 和 A[RIGHT(i)]中找出最大的,并将其下标存在largest中,如果A[i]是最大的,则以i为根的子树已是最大堆,程序结束。否则,i的某个子结点中有最大元素,则交换A[i]和A[largest],从而使i及其子女满足堆性质。下标为largest的结点在交换后的值是A[i],以该结点为根的子树又有可能违反最大堆性质。因而要对该子树递归调用MAX-HEAPIFY。

当heap-size[A] = 10 时,MAX-HEAPIFY(A,1)(索引从0开始)的作用过程如上图所示,(a)为初始构造最大堆,在结点i=1处A[1]违反了最大堆性质,因为它不大于它的两个子女。在(b)中通过交换A[1]与A[3],在结点1处恢复了最大堆性质,但又在结点3处违反了最大堆性质。现在递归调用MAX-HEAPIFY(A, 3),置i=3, (c)中交换了A[3与A[8],结点3的最大堆性质得到恢复,递归调用MAX-HEAPIFY(A, 8)对该数据结构不会再引起任何变化。

当MAX-HEAPIFY作用在一棵以结点i为根的、大小为n的子树上时,其运行时间为调整元素A[i]、A[LEFT(i)]和A[RIGHT(i)]的关系时所用时间O(1),再加上对以i的某个子结点为根的子树递归调用MAX-HEAPIFY所需时间。i结点的子树大小至多为2n/3(最坏情况发生在最底层恰好半满的时候),那么MAX-HEAPIFY的运行时间可由下式描述: T(n) <= T(2n/3) + O(1),该递归式的解为T(n) = O(lgn)。或者说,MAX-HEAPIFY作用一个高度为h的结点所需的运行时间为O(h)。


如何建堆?

我们可由自底向上地用MAX-HEAPIFY来将一个数组A[0...n-1](此处n=length[A])变成一个最大堆。子数组A[n/2...n-1]中的元素都是树中的叶子,因此每个都可看作是只含一个元素的堆。过程BUILD-MAX-HEAP对树中的每一个其他结点都调用一次MAX-HEAPIFY。

BUILD-MAX-HEAP(A)

1    heap-size[A] <------ length[A]

2    for i <------ length[A]/2 - 1 downto 0

3            do MAX-HEAPIFY(A, i)

堆排序算法

开始时,堆排序算法先用BUILD-MAX-HEAP将输入数组A[0...n-1](此处n=length[A])构成一个最大堆。因为数组中最大元素在根A[0],则可以通过它与A[n-1]互换来达到最终正确的位置。现在,如果从堆中“去掉”结点n-1(通过减小heap-size[A]),可以很容易地将A[1...n-2]建成最大堆。原来根的子女仍然是最大堆,而新的根元素可能违背了最大堆性质。这时调用MAX-HEAPIFY(A, 0)就可以保持这一性质,在A[0...(n-2)]中构造出最大堆。堆排序算法不断重复这个过程,堆的大小由n-1一直降到2。

HEAPSORT(A)

1    BUILD-MAX-HEAP(A)

2    for i <------ length[A] - 1 downto 1

3            do exchange A[0] <------> A[i]

4                heap-size[A] <------ heap-size[A] - 1

5                MAX-HEAPIFY(A, 0)

HEAPSORT过程的时间代价为O(nlgn)。其中调用BUILD-MAX-HEAP的时间为O(n), n-1次HEAP-MAX-HEAPIFY调用中每一次的时间代价为O(lgn)。下面我们给出了初始化最大堆建立之后堆排序的一个例子。图中每个最大堆与算法第2~5行的for循环的每次迭代的开始对应。

 

        研究完了堆排序算法原理及算法流程,下面主要来用C代码实现,既然伪码都有了,那么实现起来就easy了!下面是堆排序算法的C代码实现过程(这里,我们随机产生15个整数,然后对其进行排序,验证其算法的准确性):

// Name        :堆排序算法
// Author      : @CodingGeek
// Version     : 1.0
// Copyright   : Your copyright notice
// Description : 堆排序算法C代码实现
//============================================================================

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define PARENT(i) (i)/2
#define LEFT(i) 2 * (i) + 1
#define RIGHT(i) 2 * (i + 1)

void swap(int &a, int &b)
{
	int temp = a;
	 a = b;
	 b = temp;
}

void max_heapify(int arr[], int len, int index)
{
    int left = LEFT(index);
    int right = RIGHT(index);
    int largest = index;

    if (left < len && arr[left] > arr[index])
    {
    	largest = left;
    }

    if (right < len && arr[right] > arr[largest])
    {
    	largest = right;
    }

    if (largest != index)
    {
        swap(arr[largest], arr[index]);
        max_heapify(arr, len, largest);
    }
}


void build_max_heap(int arr[], int len)
{
	int i = 0;
	if (len <= 1)
	{
		return;
	}

	for (i = len/2 - 1; i >= 0; i--)
	{
		max_heapify(arr, len, i);
	}

}

void heap_sort(int arr[], int len)
{
	int i = 0;
	if (arr == NULL || len <= 1)
	{
		return;
	}
	build_max_heap(arr, len);
	for (i = len - 1; i >= 1; --i)
	{
		swap(arr[0], arr[i]);
		max_heapify(arr, --len, 0);
	}
}


int main(void)
{

    const int arr_num = 15;
    int *pstArr =  new int[arr_num];
	srand(time(NULL));

    for (int i = 0; i < arr_num; i++)
    {
    	pstArr[i] = rand() % 100;
    	printf("%d ", pstArr[i]);
    }
    printf("*************\n");
    heap_sort(pstArr, arr_num);
    for (int i = 0; i < arr_num; i++)
    {
       	printf("%d ", pstArr[i]);
    }

    delete []pstArr;


    return 0;
}

运行程序,我们可以得出堆排序算法之前与之后的打印结果:




阅读更多
上一篇插入排序增强版
下一篇AVL树原理思想及其实现
博主设置当前文章不允许评论。

没有更多推荐了,返回首页

关闭
关闭
关闭