排序算法七:选择排序之堆排序

时间:2021-09-09 01:17:51

排序算法七:选择排序之堆排序

声明:引用请注明出处http://blog.csdn.net/lg1259156776/


引言

在我的博文《“主宰世界”的10种算法短评》中给出的首个算法就是高效的排序算法。本文将对排序算法做一个全面的梳理,从最简单的“冒泡”到高效的堆排序等。

上博文讲述了选择排序中的简单排序算法,本文介绍的堆排序是树性选择排序,采用堆这个数据结构来辅助排序。


排序相关的的基本概念

  • 排序:将一组杂乱无章的数据按一定的规律顺次排列起来。
    • 数据表( data list): 它是待排序数据对象的有限集合。
    • 排序码(key):通常数据对象有多个属性域,即多个数据成员组成,其中有一个属性域可用来区分对象,作为排序依据。该域即为排序码。每个数据表用哪个属性域作为排序码,要视具体的应用需要而定。
  • 分类
    • 内排序:指在排序期间数据对象全部存放在内存的排序;
    • 外排序:指在排序期间全部对象个数太多,不能同时存放在内存,必须根据排序过程的要求,不断在内、外存之间移动的排序。

排序算法的分析

排序算法的稳定性

如果在对象序列中有两个对象 r[i] r[j] ,它们的排序码 k[i]==k[j] 。如果排序前后,对象 r[i] r[j] 的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。

排序算法的评价

时间开销

  • 排序的时间开销可用算法执行中的数据比较次数与数据移动次数来衡量。
  • 算法运行时间代价的大略估算一般都按平均情况进行估算。对于那些受对象排序码序列初始排列及对象个数影响较大的,需要按最好情况和最坏情况进行估算

空间开销

算法执行时所需的附加存储。


堆排序

关于堆这一数据结构的阐释可以参看我的博文《数据结构(三):非线性逻辑结构-特殊的二叉树结构:堆、哈夫曼树、二叉搜索树、平衡二叉搜索树、红黑树、线索二叉树》。为了保证完整性,这里简要介绍一下。

堆的概念

堆(heap order)是一种特殊的表,如果将它看做是一颗完全二叉树的层次序列,那么它具有如下的性质:每个节点的值都不大于其孩子的值,或每个节点的值都不小于其孩子的值,前者为小根堆,后者为大根堆。
排序算法七:选择排序之堆排序

堆排序

基本思想

堆排序(HeapSort)是一树形选择排序。堆排序的特点是:在排序过程中,将 R[1...n] 看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。

优 点

直接选择排序中(可以参看《排序算法六:选择排序之简单选择排序》),为了从 R[1...n] 中选出关键字最小的记录,必须进行 n1 次比较,然后在 R[2..n] 中选出关键字最小的记录,又需要做 n2 次比较。事实上,后面的 n2 次比较中,有许多比较可能在前面的 n1 次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。堆排序可通过树形结构保存部分比较结果,可减少比较次数。

堆排序图示

排序算法七:选择排序之堆排序

排序算法七:选择排序之堆排序

排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序

排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序
排序算法七:选择排序之堆排序

算法c plus plus描述

#include <iostream>

using namespace std;

void siftDown( int *a, int k, int N);

void swap(int *m, int *n)
{
int tmp;
tmp = *m;
*m = *n;
*n = tmp;
}

void heapsort( int a[], int N){
/* heapify */
for (int k = N/2; k >= 0; k--) {
siftDown( a, k, N);
}

while (N-1 > 0) {
/* swap the root(maximum value) of the heap
with the last element of the heap */

swap(a[N-1], a[0]);
/* put the heap back in max-heap order */
siftDown(a, 0, N-1);
/* N-- : decrease the size of the heap by one
so that the previous max value will
stay in its proper placement */

N--;
}
}

void siftDown( int *a, int k, int N){
while ( k*2 + 1 < N ) {
/* For zero-based arrays, the children are 2*i+1 and 2*i+2 */
int child = 2*k + 1;

/* get bigger child if there are two children */
if ((child + 1 < N) && (a[child] < a[child+1])) child++;

if (a[k] < a[child]) { /* out of max-heap order */
swap( a[child], a[k] );
/* repeat to continue sifting down the child now */
k = child;
}
else
return;
}
}

int main()
{
int i;
int a[] = {19, 17, 16, 12, 9, 15, 1, 2, 11, 7, 3, 10, 14};
const size_t sz = sizeof(a)/sizeof(a[0]);
for (i = 0; i < sz; i++)
cout << a[i] << " ";
cout << endl;
cout << "----------------------------------" << endl;

heapsort(a, sz);

for (i = 0; i < sz; i++)
cout << a[i] << " ";
cout << endl;

return 0;
}

输出为:

19 17 16 12 9 15 1 2 11 7 3 10 14
----------------------------------

1 2 3 7 9 10 11 12 14 15 16 17 19

首先利用siftdown将测试数据建堆,然后将堆顶的数据跟尾部数据交换,并重新调整堆和减少堆中数据的数量。而shiftdown实现的是将堆中某个输入的数据节点处的整个二叉树枝的堆的调整(默认只有该位置与其左右孩子可能需要调整,其左右孩子的二叉树枝是满足堆结构的)。因此再利用shiftdown建堆的时候,得先从最右下边的父节点开始调整,因为它调整完毕后,就满足堆结构要求,之后基于它的调整才能使用siftdown来完成。

算法分析

堆排序的时间复杂度是O(nlgN),与快速排序达到相同的时间复杂度。但是在实际应用中,我们往往采用快速排序而不是堆排序。这是因为快速排序的一个好的实现,往往比堆排序具有更好的表现。堆排序的主要用途,是在形成和处理优先级队列方面。另外,如果计算要求是类优先级队列(比如,只要返回最大或者最小元素,只有有限的插入要求等),堆同样是很适合的数据结构。
 堆排序方法对记录数较少的文件并不值得提倡,但对n较大的文件还是很有效的。因为其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。
 堆排序在最坏的情况下,其时间复杂度也为O(nlogn)。相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需一个记录大小的供交换用的辅助存储空间。

堆排序并不是一个稳定的排序。主要是借助了堆这个数据结构,一旦调整好堆的结构,就能保证root对应的是最大的项,在实现中随着尾部的数据排序,逐渐减小堆中数据的数量,并不断的将顶部的数排在尾部已经排好序的数据前面,就这样当堆数量减少到1的时候排序也就完成了。


2015-9-26 艺少