在讨论堆排序之前,我们先来讨论一下另外一种排序算法——插入排序。插入排序的逻辑相当简单,先遍历一遍数组找到最小值,然后将这个最小值跟第一个元素交换。然后遍历第一个元素之后的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. vue 链接

    https://www.jianshu.com/p/5d9b341d650f vue-cli入门(三)——人员管理实例 ps:如何创建vue-cli项目 https://www.v2ex.com/t/ ...

  2. controller返回js中文变成?解决方案

    在使用spring-mvc的mvc的时候既享受它带来的便捷,又头痛它的一些问题,比如经典的中文乱码问题.现在是用json作为客户端和服务端 的数据交换格式貌似很流行,但是在springmvc中有时候会 ...

  3. 执行.class文件

    java packageName.className即可 但是注意,如果是有包的,这段指令一定是packageName的上层目录(即bin目录)执行!

  4. python3之编码

    这个符号(#!)的名称,叫做"Shebang"或者"Sha-bang"Shebang这个符号通常在Unix系统的脚本中第一行开头中写到,它指明了执行这个脚本文件 ...

  5. linux 将大文件分解为多个小文件

    使用的命令为: split --bytes 500M --numeric-suffixes --suffix-length=3 foo foo_ 其中 --bytes 为小文件的大小, --suffi ...

  6. 关于C#客户端引用C++ dll的问题

    近期在做项目的过程中需要在Winform客户端项目引用由C++编译的DLL,于是对相关的内容进行了一些研究,有几点心得总结如下. 第一步是制作要引用的类库: (1)首先拿到C++的dll,需要注意的是 ...

  7. 转:MySQL InnoDB Add Index实现调研

    MySQL InnoDB Add Index实现调研 MySQL Add Index实现 MySQL各版本,对于add Index的处理方式是不同的,主要有三种: Copy Table方式 这是Inn ...

  8. 2015.2.27 UltraEdit中显示XML结构

    1选择菜单项 "视图"->"显示方式(着色文件类型)"->"XML": 2选择菜单项 "格式"->&q ...

  9. python window使用paramiko简单监控数据指标数据采集

    #!/usr/bin/python #-*- coding: utf-8 -*- #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ...

  10. CSS——display

    display的主要属性: 1.none: 不显示: 不保留其在页面中的位置,即相当于html文档中没有这个元素: visible:hidden虽然也不显示:但依然保存着他的位置和大小. 2.bloc ...