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

    个人心得:算法书中的第一个例题就来了一个下马威,虽然题意很好理解但是做起来确实这么不顺手,所以自己对于搜索和堆栈理解的并不是很好, 以前也是很多这样的题目无法实施,这题要做的很明确就是输出正确的能依靠 ...

  2. 洛谷P4721 【模板】分治 FFT(分治FFT)

    传送门 多项式求逆的解法看这里 我们考虑用分治 假设现在已经求出了$[l,mid]$的答案,要计算他们对$[mid+1,r]$的答案的影响 那么对右边部分的点$f_x$的影响就是$f_x+=\sum_ ...

  3. poj 1201 Intervals——差分约束裸题

    题目:http://poj.org/problem?id=1201 差分约束裸套路:前缀和 本题可以不把源点向每个点连一条0的边,可以直接把0点作为源点.这样会快许多! 可能是因为 i-1 向 i 都 ...

  4. SqlServer 用户和权限操作

    use [master] GO --创建用户,Test,密码Test CREATE LOGIN [TestUser] WITH PASSWORD=N'Test', DEFAULT_DATABASE=[ ...

  5. java代码异常处理篇-----循环

    总结:注意一个方法:nextLine();它表示:执行当前行,返回跳过的输入信息. package com.da; import java.util.InputMismatchException; i ...

  6. AngularJS:包含

    ylbtech-AngularJS:包含 1.返回顶部 1. AngularJS 包含 在 AngularJS 中,你可以在 HTML 中包含 HTML 文件. 在 HTML 中包含 HTML 文件 ...

  7. Excel开发学习笔记:发布VSTO下的Excel开发项目

    遇到一个数据处理自动化的问题,于是打算开发一个基于excel的小工具.在业余时间一边自学一边实践,抽空把一些知识写下来以备今后参考,因为走的是盲人摸象的野路子,幼稚与错误请多包涵. 开发环境基于VST ...

  8. 人脸检测学习笔记(数据集-DLIB人脸检测原理-DLIB&OpenCV人脸检测方法及对比)

    1.Easily Create High Quality Object Detectors with Deep Learning 2016/10/11 http://blog.dlib.net/201 ...

  9. .Net 之Tuple 类

    Tuple是什么 按照Msdn 上说:提供用于创造元组对象的静态方法.从字面意思并不能理解他的作用:   Tuple 是个静态类,提供8个静态泛型方法:T 可以是值类型,也可是引用类型:   使用场景 ...

  10. Python多进程-进程间数据的共享

    不同的进程不能同时修改一份数据,但是不同的进程能对一份数据进行修改 可通过Manager来实现进程间的数据共享 # -*- coding:utf-8 -*- __author__ = "Mu ...