在讨论堆排序之前,我们先来讨论一下另外一种排序算法——插入排序。插入排序的逻辑相当简单,先遍历一遍数组找到最小值,然后将这个最小值跟第一个元素交换。然后遍历第一个元素之后的n-1个元素,得到这n-1个元素中的最小值,即整个序列的次小值,将其跟第二个元素交换。接下来对后n-2个元素进行相同的操作,直到得到有序序列。

很显然,插入排序的时间复杂度是O(n2)。在n个关键字中选出最小值,至少进行n-1次比较,然而,继续在剩余的n-1个关键字中选择次小值就并非一定要进行n-2次比较,若能利用前n-1次比较所得信息,则可以减少以后各趟选择排序中所用的比较次数。堆排序正是利用了之前比较信息的一种排序算法,从而提高了效率。

鉴于篇幅和编辑的难度,这里不会非常详细的介绍堆排序的细节,如果想了解更多的话,可以看看数据结构的书籍或其他文章。

堆排序是利用堆这种数据结构进行排序的一种算法。堆的定义如下:n个元素序列{k1,k2,...,kn}当且仅当满足以下关系时,称之为堆

情况1:ki <= k(2i) && ki <= K(2i+1)

情况2:ki >= k(2i) && ki >= k(2i+1)

满足情况1的我们称之为小顶堆,满足情况2的我们称之为大顶堆。

若将此序列对应的一维数组(即以一维数组作此序列的存储结构)看成一个完全二叉树,则堆的定义表明,完全二叉树中所有终端结点的值均不大于(或不小于)其左右孩子结点的值。由此,堆顶元素必为n个元素序列中的最小值或最大值。

若在输出堆顶的最小值(或最大值)之后,使得剩余n-1个元素的序列又建成一个堆,则得到n个元素中的次小值(或次大值)。如此反复执行,便能得到一个有序序列,这个过程称之为堆排序。

那么现在就需要处理两个问题了:

1. 怎么样由无序序列建成一个堆?

2. 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

我们假设我们在使用大顶堆的情况。

首先,我们先考虑问题2,通常把堆顶元素跟最后一个元素交换,这样就把最大值放到了序列的最后。现在我们要调整前n-1个元素成为一个新的堆。现在堆顶元素的左右子树都为堆,则仅需自上至下进行调整即可。首先以堆顶元素和其左、右子树根结点进行比较,将三者中最大的放到堆项:(a)如果堆顶本来就最大,不用交换,并且现在已经是一个堆了,因为左、右子树都是堆,调整可以退出了。(b)如果三者中最大的元素为左右子树根结点中的一个,则要和堆顶结点交换,被交换的子树根结点所在子树被破坏,不再是堆,所以又要进行相同过程的调整,如此往复,直至(a)的情形或叶子结点。我们称这个自堆顶至叶子的调整过程为“筛选”。如果有完全二叉树的图结合着看的话,效果会比较好。

然后问题1,从一个无序序列建堆的过程就是一个反复“筛选”的过程。若将些序列看成是一个完全二叉树,则最后一个非终端结点是第n/2(下取整)个元素,由些“筛选”只需从第n/2(下取整)个元素开始,一直到要第一个元素,即树根,堆顶。

下面是筛选的代码,即调整堆的过程:(注意在由于数组下标从0开始的,所以计算下标的时候要注意一下)

        static void HeapAdjust(int[] numbers, int index, int length)
{
for (int childIndex = * index + ; childIndex <= length; childIndex *= )
{
if (childIndex < length && numbers[childIndex] < numbers[childIndex + ])
{
// childIndex为两棵子树的根结点中较大的那个的下标
childIndex++;
} if (numbers[index] >= numbers[childIndex])
{
// 如果堆顶已经为三者(目前堆顶元素,堆顶元素左子树的根结点,堆顶元素右子树的根结点)最大值,
// 则堆已调整好,可以结束了。
break;
} // 如果堆顶不是三者(目前堆顶元素,堆顶元素左子树的根结点,堆顶元素右子树的根结点)最大值
// 则要进行交换
Switch<int>(ref numbers[index], ref numbers[childIndex]);
}
} static void Switch<T>(ref T a, ref T b)
{
T temp;
temp = a;
a = b;
b = temp;
}

上面的方法中有一个可以优化的地方,即直到最后调整成为一个新堆的时候,才能确定原先的堆顶元素所在新的位置。比如,堆顶元素r和其一个孩子结点交换后,孩子结点所在的子树要进行新的调整,此时孩子结点所在的子树的根结点是r,r 又跟其一个孩子结点交换之后,新的堆构成了。那么,r的第一次交换其实就是可以优化的,只要事先保存了r的值,就不用交换了,只需为堆顶元素赋值即可,而不用把r的值再赋给其本来要交换的那个结点了。这个自己理解吧,和快速排序中的交换优化一模一样。所以代码可以优化为:

        static void HeapAdjust(int[] numbers, int index, int length)
{
int temp = numbers[index];
for (int childIndex = * index + ; childIndex <= length; childIndex *= )
{
if (childIndex < length && numbers[childIndex] < numbers[childIndex + ])
{
childIndex++;
} if (temp >= numbers[childIndex])
{
break;
} numbers[index] = numbers[childIndex];
index = childIndex;
} numbers[index] = temp;
}

调整的代码完成之后,下面是堆排序的代码,这里我没有把由无序序列构建堆的过程封装到另一个方法里面,而是直接写了,你如果想另写一个方法的话,当然可以了。

        static void HeapSort(int[] numbers)
{
// 得到大顶堆
for (int i = numbers.Length / - ; i >= ; i--)
{
HeapAdjust(numbers, i, numbers.Length - );
} // 开始堆排序
// 1. 即将堆顶元素(最大值)跟最后一个元素交换,此时最大元素已经就绪,放到了最后
// 2. 现在只需要关注前n-1个结点就可了,由于上一步将取后一个元素放到了根结点,所以前n-1个结点不再是大顶堆了,
// 所以现在要调整堆为一个大顶堆,即筛选
// 3. 一次筛选完成之后把堆顶元素再和最后一个交换,次大数就绪
// 4. 循环这个过程,最终得到有序序列
int temp;
for (int i = numbers.Length - ; i > ; )
{
temp = numbers[i];
numbers[i] = numbers[];
numbers[] = temp;
i--;
HeapAdjust(numbers, , i);
}
}

下面是一个调用堆排序并输出排序结果的例子:

        static void Main(string[] args)
{
int[] numbers = { , , , , , , , };
HeapSort(numbers); foreach (int i in numbers)
{
Console.Write(i.ToString() + " ");
}

Console.Read();
}

最后,堆排序是一种不稳定的排序算法。时间复杂度为O(n*logn),只需一个记录大小的辅助空间,即空间复杂度为O(1)。堆排序方法对记录数较少的文件并不值得提倡,但对于n比较大的文件还是很有效的。

小小c#算法题 - 7 - 堆排序 (Heap Sort)的更多相关文章

  1. 小小c#算法题 - 9 - 基数排序 (Radix Sort)

    基数排序和前几篇博客中写到的排序方法完全不同.前面几种排序方法主要是通过关键字间的比较和移动记录这两种操作来实现排序的,而实现基数排序不需要进行记录项间的比较.而是把关键字按一定规则分布在不同的区域, ...

  2. 小小c#算法题 - 8 - 归并排序 (Merging Sort)

    “归并”的含义是将两个或两个以上的有序序列组合成一个新的有序序列.这个“归并”可以在O(n+m)的数量级上实现,但这同时也需要O(n+m)的空间复杂度.具体为:首先分配一个新的长度为n+m的空序列,然 ...

  3. Python入门篇-数据结构堆排序Heap Sort

    Python入门篇-数据结构堆排序Heap Sort 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.堆Heap 堆是一个完全二叉树 每个非叶子结点都要大于或者等于其左右孩子结点 ...

  4. 数据结构 - 堆排序(heap sort) 具体解释 及 代码(C++)

    堆排序(heap sort) 具体解释 及 代码(C++) 本文地址: http://blog.csdn.net/caroline_wendy 堆排序包括两个步骤: 第一步: 是建立大顶堆(从大到小排 ...

  5. 小小c#算法题 - 11 - 二叉树的构造及先序遍历、中序遍历、后序遍历

    在上一篇文章 小小c#算法题 - 10 - 求树的深度中,用到了树的数据结构,树型结构是一类重要的非线性数据结构,树是以分支关系定义的层次结构,是n(n>=0)个结点的有限集.但在那篇文章中,只 ...

  6. 堆排序 Heap Sort

    堆排序虽然叫heap sort,但是和内存上的那个heap并没有实际关系.算法上,堆排序一般使用数组的形式来实现,即binary heap. 我们可以将堆排序所使用的堆int[] heap视为一个完全 ...

  7. 算法----堆排序(heap sort)

    堆排序是利用堆进行排序的高效算法,其能实现O(NlogN)的排序时间复杂度,详细算法分析能够点击堆排序算法时间复杂度分析. 算法实现: 调整堆: void sort::sink(int* a, con ...

  8. 数据结构与算法---堆排序(Heap sort)

    堆排序基本介绍 1.堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序. 2.堆是具有以下性质的完全二叉树:每个 ...

  9. 小小c#算法题 - 6 - 快速排序 (QuickSort)

    快速排序是排序算法中效率比较高的一种,也是面试常被问到的问题. 快速排序(Quick Sort)是对冒泡排序的一种改进.它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字 ...

随机推荐

  1. Redis底层探秘(四):整数集合及压缩列表

    整数集合 整数集合(intset)是集合键的底层实现之一,当一个集合只包含 整数值元素,并且这个集合的元素数量不多时,Redis就会使用郑书记和作为集合键的底层实现. 整数集合的实现 整数集合是red ...

  2. java程序员图文并茂细说Unity中调用Android的接口

    http://bbs.csdn.net/topics/391876421 最近做一个项目,为同事提供接口,能使他在Unity中调用Android中的函数来实现QQ登陆并获取用户信息.按照一些书上和一些 ...

  3. IE8提示console未定义

    在开发的过程中由于调试的原因,在代码中加入console.info("xxxx"),而未进行删除 在IE8下测试该代码所在的页面报错,如下: 需要注意的是,使用console对象查 ...

  4. [转]JavaScript之引用类型

    Object类型 Object类型是JavaScript中使用最多的一种类型.虽然Object的实例不具备多少功能,但对于在应用程序中存储和传输数据而言,它确实是非常理想的选择. 创建Object实例 ...

  5. c++11之一: 基于范围的for循环

    #include <iostream> using namespace std; int main(){ ]{,,,,}; for (int& e: ary) e *= ; for ...

  6. ASP.NET网站性能提升的几个方法

    1. HTTP 压缩 HTTP 压缩通常用于压缩从服务端返回的页面内容.它压缩HTTP请求和响应,这个会是巨大的性能提升.我的项目是基于Window Server 2003开发的,可以参考这篇文章. ...

  7. uboot中的TEXT_BASE

    转载:http://blog.csdn.net/xxblinux/article/details/6281295 我们都知道U-BOOT分为两个阶段,第一阶段是(~/cpu/arm920t/start ...

  8. JAX-WS注解

    JAX-WS注解: javax.jws.WebService @WebService应用于类或者接口上面,该类便是一个对外访问WebService,默认情况里面所有的public方法都是可以对外提供访 ...

  9. 关于jsp和html页面中的三种弹出框

    代码: <!-- 引入jquery 由于在下面使用jquery选择器,与弹出框无关 --> <script type="text/javascript" src= ...

  10. Celery-4.1 用户指南: Periodic Tasks (定时任务)

    简介 celery beat 是一个调度器:它以常规的时间间隔开启任务,任务将会在集群中的可用节点上运行. 默认情况下,入口项是从 beat_schedule 设置中获取,但是自定义的存储也可以使用, ...