堆的定义

堆是具有下列性质的完全二叉树:每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆;或者每个节点的值都小于或等于其左右孩子的值,称为小顶堆。如下图举例:



通过堆的定义可知,根节点一定是对中所有节点的最大(小)值。较大(小)的节点靠近根节点(并不绝对,比如上图小顶堆中60, 40均小于70,但它并没有70靠近根节点)

按层序方式给节点从1开始编号,则节点之间满足下列关系:

{ki≥k2iki≥k2i+1
\left \{\begin{array}{cc}
k_i \geq k_{2i}\\
k_i \geq k_{2i+1}
\end{array}\right.
{ki​≥k2i​ki​≥k2i+1​​

或:

{ki≤k2iki≤k2i+1
\left \{\begin{array}{cc}
k_i \leq k_{2i}\\
k_i \leq k_{2i+1}
\end{array}\right.
{ki​≤k2i​ki​≤k2i+1​​

其中i满足:1 ≤\leq≤ i ≤\leq≤ n2\frac{n}{2}2n​

这里i为什么小于等于 n2\frac{n}{2}2n​呢?

根据完全二叉树的性质,对于一棵完全二叉树,如果某一个节点的下标i=1,则这个节点是二叉树的根,无双亲;如果i > 1,则其双亲是节点i2\frac{i}{2}2i​。那么对于有n个节点的二叉树来说,它的i值自然就是小于等于n2\frac{n}{2}2n​了。

堆排序算法原理

堆排序(Heap Sort)就是利用堆(这里我们假设利用的最大堆,最小堆类似)进行排序的方法,它的基本思想就是,将待排序的序列构造成一个大顶堆。此时整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中次大值,如此反复执行,就能得到一个有序序列。

算法流程:

  1. 将含有n个元素的待排序的序列 (r1r_1r1​, r2r_2r2​, r3r_3r3​, …, rnr_nrn​) 初始化成一个大顶堆,此堆为初始的无序区。
  2. 将堆顶元素r[1]和最后一个元素r[n]进行交换,交换完成后得到新的无序区:(r1r_1r1​, r2r_2r2​, r3r_3r3​, …, rn−1r_{n-1}rn−1​),和一个新的有序区 (rnr_nrn​)。
  3. 交换后的新的堆顶元素r[1]可能破坏堆的性质,所以重新将当前无序区调整为一个新堆。然后再将堆顶元素r[1]和无序区最后一个元素进行交换,得到一个新的无序区 (r1r_1r1​, r2r_2r2​, …, rn−2r_{n-2}rn−2​)和一个新的有序区 (rn−1r_{n-1}rn−1​, rnr_nrn​)。
  4. 重复上述过程,直到有序区的元素个数为n-1,整个排序过程完成

举例说明:

假设我们要排序的序列是{50, 10, 90, 30, 70, 40, 80, 60, 20},长度为9

将待排序序列初始化为一个堆:



则堆排序过程如下:



(ps:画图一度画疯…)

现在我们了解了堆排序的过程,但是还有两个问题:

  1. 如何减肥一个无序序列构建成一个堆?
  2. 如何在输出堆顶元素后,调整剩余元素成为一个新的堆?

我们所谓的将待排序的序列构造成一个大顶堆,其实就是从下往上、从右到左,将每个非终端(非叶子节点)节点当做根节点,将其和其子树调整成最大堆。

具体看代码:

public class MaxHeapSort {

	/**
* 已知elem[s....m]中记录的关键字除elem[s]之外满足堆的定义
* 本函数调整elem[s]的关键字,使elem[s.....m]成为一个大顶堆
* @param elem
* @param s
* @param m
*/
public void maxHeapAdjust(int[] elem, int s ,int m) {
int temp, j;
temp = elem[s];
for (j = 2 * s; j <= m; j *= 2) { //沿关键字较大的孩子节点向下筛选
if(j < m && elem[j] < elem[j + 1]) { //判断左孩子大还是右孩子大
++j; //j为关键字中较大的下标
}
if(temp >= elem[j]) {
break;
}
elem[s] = elem[j];
s = j;
} elem[s] = temp; //插入
} public void swap(int[] elem, int i, int j) {
int temp = elem[i];
elem[i] = elem[j];
elem[j] = temp;
} /**
* 对顺序表elem进行堆排序
* @param elem
*/
public void maxHeapSort(int[] elem) {
int length = elem.length - 1;
for (int i = length / 2; i > 0; i--) { //把elem构建成一个大顶堆
maxHeapAdjust(elem, i, length);
} for (int i = length; i > 1; i--) {
swap(elem, 1, i); //将堆顶记录和当前未经排序子序列的最后一个记录交换
maxHeapAdjust(elem, 1, i - 1); //将elem[1.....i-1]重新调整为一个大顶堆
}
} public static void main(String[] args) {
MaxHeapSort m = new MaxHeapSort();
int[] elem = {0, 50, 10, 90, 30, 70, 40, 80, 60, 20};
m.maxHeapSort(elem); for (int i = 1; i < elem.length; i++) {
System.out.print(elem[i] + ", ");
}
}
}

输出:

10, 20, 30, 40, 50, 60, 70, 80, 90,

堆排序复杂度分析

堆排序的运行时间主要是消耗在初始构建堆和重建堆时的反复筛选上。

在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端节点开始构建,将它和其孩子进行比较和若有必要的交换,对于每个非终端节点来说,其实最多进行两次比较和互换操作,因此真个构建堆的时间复杂度是O(n)。

在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间 (完全二叉树的某个节点到根节点的距离是[log2log_2log2​i] + 1),并且需要取n-1次堆顶记录,因此重建堆的时间复杂度为O(nlog⁡\loglogn)。

所以堆排序的整体时间复杂度为O(nlog⁡\loglogn)。由于堆排序对原始记录的排序状态并不敏感,因此无论是最好,最坏和平均时间复杂度均为O(nlog⁡\loglogn)。

空间复杂度上,只有一个用来交换的暂存单元,但是由于记录的比较与交换是跳跃式进行的,因此堆排序也是一种不稳定的排序方法

另外,由于初始构建堆所需的比较次数比较多,因此他不适合待排序序列个数较少的情况。

堆排序(最大堆)的理解和实现(Java)的更多相关文章

  1. Atitit  深入理解命名空间namespace  java c# php js

    Atitit  深入理解命名空间namespace  java c# php js 1.1. Namespace还是package1 1.2. import同时解决了令人头疼的include1 1.3 ...

  2. 理解和解决Java并发修改异常ConcurrentModificationException(转载)

    原文地址:https://www.jianshu.com/p/f3f6b12330c1 理解和解决Java并发修改异常ConcurrentModificationException 不知读者在Java ...

  3. 深入理解和探究Java类加载机制

    深入理解和探究Java类加载机制---- 1.java.lang.ClassLoader类介绍 java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字 ...

  4. 深入理解什么是Java泛型?泛型怎么使用?【纯转】

    本篇文章给大家带来的内容是介绍深入理解什么是Java泛型?泛型怎么使用?有一定的参考价值,有需要的朋友可以参考一下,希望对你们有所助. 一.什么是泛型 “泛型” 意味着编写的代码可以被不同类型的对象所 ...

  5. 深入理解JVM一java堆分析

    上一节介绍了针对JVM的监控工具,包括JPS可以查看当前所有的java进程,jstack查看线程栈可以帮助你分析是否有死锁等情况,jmap可以导出java堆文件在MAT工具上进行分析等等.这些工具都非 ...

  6. [转载] 深入理解Android之Java虚拟机Dalvik

    本文转载自: http://blog.csdn.net/innost/article/details/50377905 一.背景 这个选题很大,但并不是一开始就有这么高大上的追求.最初之时,只是源于对 ...

  7. 如何理解和使用Java package包

    Java中的一个包就是一个类库单元,包内包含有一组类,它们在单一的名称空间之下被组织在了一起.这个名称空间就是包名.可以使用import关键字来导入一个包.例如使用import java.util.* ...

  8. 深入理解JVM(6)——Java内存模型和线程

    Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(“即Ja ...

  9. 理解JVM之Java内存区域

    Java虚拟机运行时数据区分为以下几个部分: 方法区.虚拟机栈.本地方法栈.堆.程序计数器.如下图所示: 一.程序计数器 程序计数器可看作当前线程所执行的字节码行号指示器,字节码解释器工作时就是通过改 ...

随机推荐

  1. 【Maven】Nexus配置和使用

    Nexus安装 nexus安装,可以参照:[Maven]Nexus(Maven仓库私服)下载与安装 Nexus简单说明 用途:指定私服的中央地址.将自己的Maven项目指定到私服地址.从私服下载中央库 ...

  2. Selenium安装中的一些问题及解决办法-软硕1703班3组整理分享

    非常感谢软件工程硕士1703班3组同学的热心,他们将安装Selenium过程中踩过的坑替大家填上了.希望还没有来得及踩坑的,或者掉进坑里还没爬出来的小组,能顺利跨过去这个安装的坑. 如下是原文. Se ...

  3. 山东省第七届ACM竞赛 J题 Execution of Paladin (题意啊)

    题意:鱼人是炉石里的一支强大种族,在探险者协会里,圣骑士有了一张新牌,叫亡者归来,效果是召唤本轮游戏中7个已死鱼人.如果死掉的不足7个,那么召唤的数量就会不足7. 鱼人有很多,下面的4个是: 寒光智者 ...

  4. Java 增强 for 循环

    Java 增强 for 循环 Java5 引入了一种主要用于数组的增强型 for 循环. Java 增强 for 循环语法格式如下: for(声明语句 : 表达式) { //代码句子 } 声明语句:声 ...

  5. 20155212 2016-2017-2 《Java程序设计》第7周学习总结

    20155212 2016-2017-2 <Java程序设计>第7周学习总结 教材学习内容总结 Chapter13 取得系统时间的方法之一是System.currentTimeMillis ...

  6. Android属性动画之ValueAnimator的介绍

    之前两篇博客,介绍的是ObjectAnimator作用与某一个控件的某一个属性.但我们的ValueAnimator它本身并不会作用与任何一个属性,它本身也不会提供任何一种动画.它简单的来说,就是一个数 ...

  7. genymotion无法连接相机问题

    genymotion模拟器即时打开了相机的开关,也无法连接到相机.这是因为其他进程占用了相机,虚拟设备无法获得,可以尝试: 1.不关闭模拟器,重启adt的Eclipse 2.重启ADB,adb kil ...

  8. Fig723.asy

    import settings; outformat="pdf"; tex="xelatex"; usepackage("amsmath") ...

  9. Delphi XE7 用indy开发微信公众平台所有功能,可刷阅读,可刷赞,可加推广(除微支付)

    http://www.cnblogs.com/devinlee/p/4565933.html Delphi XE7 用indy开发微信公众平台所有功能,可刷阅读,可刷赞,可加推广(除微支付) 关注作者 ...

  10. 用C#开发的双色球走势图(原创)值得园友拥有(二)接上一篇

    昨晚由于时间的原因只写了一部分内容,今天将这一部分内容补充完毕,多谢各位园友的支持. 这是用C#开发的双色球走势图(原创)值得园友拥有 新的园友可以看昨晚写的内容,以免脱节.首先回复园友的评论,有说好 ...