详解堆排序算法原理及Java版的代码实现

概述
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
堆的定义如下:具有n个元素的序列(k1,k2,...,kn), 当且仅当满足:

时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)或最大项(大顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点(有子女的结点)的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。
(a)大顶堆序列:(96, 83, 27, 38, 11, 09)
(b)小顶堆序列:(12, 36, 24, 85, 47, 30, 53, 91)

初始时把要排序的n 个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素。然后对剩下的n-1个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到最后得到有n个节点的有序序列。称这个过程为堆排序。

步骤&实例
实现堆排序需解决两个问题:
(1)如何将n 个待排序的数建成堆;
(2)输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。
建堆方法(小顶堆):
对初始序列建堆的过程,就是一个反复进行筛选的过程。
n 个结点的完全二叉树,则最后一个结点是第n/2个结点的子树。
筛选从第n/2个结点为根的子树开始(n/2是最后一个有子树的结点),使该子树成为堆。
之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程
无序序列:(49, 38, 65, 97, 76, 13, 27, 49)

(a) 无序序列,初始二叉树,97(第8/2=4个结点)为最后一个结点(49)的父结点。
(b) 97>=49,替换位置,接下来对n/2的上一个结点65进行筛选。
(c) 13<=27且65>=13,替换65和13的位置,接下来对38进行替换(都大于它,不需操作),对49进行筛选。
(d) 13<=38且49>=13,替换49和13的位置,49>=27,替换49和27的位置。
(e) 最终得到一个堆,13是我们得到的最小数。
调整堆的方法(小顶堆):
设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶,堆被破坏,其原因仅是根结点不满足堆的性质。
将根结点与左、右子树中较小元素的进行交换。
若与左子树交换:如果左子树堆被破坏,则重复方法(2).
若与右子树交换,如果右子树堆被破坏,则重复方法(2).
继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
调整堆只需考虑被破坏的结点,其他的结点不需调整。

代码实现(Java)
运行代码结合注释与上面的实例步骤进行对比思考。

package com.coder4j.main;

public class HeapSort {

  /**
   * 调整为小顶堆(排序后结果为从大到小)
   *
   * @param array是待调整的堆数组
   * @param s是待调整的数组元素的位置
   * @param length是数组的长度
   *
   */
  public static void heapAdjustS(int[] array, int s, int length) {
    int tmp = array[s];
    int child = 2 * s + 1;// 左孩子结点的位置
    System.out.println("待调整结点为:array[" + s + "] = " + tmp);
    while (child < length) {
      // child + 1 是当前调整结点的右孩子
      // 如果有右孩子且小于左孩子,使用右孩子与结点进行比较,否则使用左孩子
      if (child + 1 < length && array[child] > array[child + 1]) {
        child++;
      }
      System.out.println("将与子孩子 array[" + child + "] = " + array[child] + " 进行比较");
      // 如果较小的子孩子比此结点小
      if (array[s] > array[child]) {
        System.out.println("子孩子比其小,交换位置");
        array[s] = array[child];// 把较小的子孩子向上移动,替换当前待调整结点
        s = child;// 待调整结点移动到较小子孩子原来的位置
        array[child] = tmp;
        child = 2 * s + 1;// 继续判断待调整结点是否需要继续调整

        if (child >= length) {
          System.out.println("没有子孩子了,调整结束");
        } else {
          System.out.println("继续与新的子孩子进行比较");
        }
        // continue;
      } else {
        System.out.println("子孩子均比其大,调整结束");
        break;// 当前待调整结点小于它的左右孩子,不需调整,直接退出
      }
    }
  }

  /**
   * 调整为大顶堆(排序后结果为从小到大)
   *
   * @param array是待调整的堆数组
   * @param s是待调整的数组元素的位置
   * @param length是数组的长度
   *
   */
  public static void heapAdjustB(int[] array, int s, int length) {
    int tmp = array[s];
    int child = 2 * s + 1;// 左孩子结点的位置
    System.out.println("待调整结点为:array[" + s + "] = " + tmp);
    while (child < length) {
      // child + 1 是当前调整结点的右孩子
      // 如果有右孩子且大于左孩子,使用右孩子与结点进行比较,否则使用左孩子
      if (child + 1 < length && array[child] < array[child + 1]) {
        child++;
      }
      System.out.println("将与子孩子 array[" + child + "] = " + array[child] + " 进行比较");
      // 如果较大的子孩子比此结点大
      if (array[s] < array[child]) {
        System.out.println("子孩子比其大,交换位置");
        array[s] = array[child];// 把较大的子孩子向上移动,替换当前待调整结点
        s = child;// 待调整结点移动到较大子孩子原来的位置
        array[child] = tmp;
        child = 2 * s + 1;// 继续判断待调整结点是否需要继续调整

        if (child >= length) {
          System.out.println("没有子孩子了,调整结束");
        } else {
          System.out.println("继续与新的子孩子进行比较");
        }
        // continue;
      } else {
        System.out.println("子孩子均比其小,调整结束");
        break;// 当前待调整结点大于它的左右孩子,不需调整,直接退出
      }
    }
  }

  /**
   * 堆排序算法
   *
   * @param array
   * @param inverse true 为倒序排列,false 为正序排列
   */
  public static void heapSort(int[] array, boolean inverse) {
    // 初始堆
    // 最后一个有孩子的结点位置 i = (length - 1) / 2, 以此向上调整各结点使其符合堆
    System.out.println("初始堆开始");
    for (int i = (array.length - 1) / 2; i >= 0; i--) {
      if (inverse) {
        heapAdjustS(array, i, array.length);
      } else {
        heapAdjustB(array, i, array.length);
      }
    }
    System.out.println("初始堆结束");
    for (int i = array.length - 1; i > 0; i--) {
      // 交换堆顶元素H[0]和堆中最后一个元素
      int tmp = array[i];
      array[i] = array[0];
      array[0] = tmp;
      // 每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
      if (inverse) {
        heapAdjustS(array, 0, i);
      } else {
        heapAdjustB(array, 0, i);
      }
    }
  }

  public static void main(String[] args) {
    int[] array = { 49, 38, 65, 97, 76, 13, 27, 49 };
    heapSort(array, false);
    for (int i : array) {
      System.out.print(i + " ");
    }
  }

}

运行结果:

初始堆开始
待调整结点为:array[3] = 97
将与子孩子 array[7] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[2] = 65
将与子孩子 array[5] = 13 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[1] = 38
将与子孩子 array[3] = 49 进行比较
子孩子均比其大,调整结束
待调整结点为:array[0] = 49
将与子孩子 array[2] = 13 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[6] = 27 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
初始堆结束
待调整结点为:array[0] = 97
将与子孩子 array[2] = 27 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[6] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 97
将与子孩子 array[1] = 38 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[3] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 65
将与子孩子 array[1] = 49 进行比较
子孩子比其小,交换位置
继续与新的子孩子进行比较
将与子孩子 array[4] = 76 进行比较
子孩子均比其大,调整结束
待调整结点为:array[0] = 76
将与子孩子 array[2] = 49 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 97
将与子孩子 array[1] = 65 进行比较
子孩子比其小,交换位置
没有子孩子了,调整结束
待调整结点为:array[0] = 76
将与子孩子 array[1] = 97 进行比较
子孩子均比其大,调整结束
待调整结点为:array[0] = 97
97 76 65 49 49 38 27 13

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

(0)

相关推荐

  • 堆排序算法的讲解及Java版实现

    堆是数据结构中的一种重要结构,了解了"堆"的概念和操作,可以快速掌握堆排序. 堆的概念 堆是一种特殊的完全二叉树(complete binary tree).如果一棵完全二叉树的所有节点的值都不小于其子节点,称之为大根堆(或大顶堆):所有节点的值都不大于其子节点,称之为小根堆(或小顶堆). 在数组(在0号下标存储根节点)中,容易得到下面的式子(这两个式子很重要): 1.下标为i的节点,父节点坐标为(i-1)/2: 2.下标为i的节点,左子节点坐标为2*i+1,右子节点为2*i+2. 堆

  • java实现的各种排序算法代码示例

    折半插入排序 折半插入排序是对直接插入排序的简单改进.此处介绍的折半插入,其实就是通过不断地折半来快速确定第i个元素的 插入位置,这实际上是一种查找算法:折半查找.Java的Arrays类里的binarySearch()方法,就是折半查找的实现,用 于从指定数组中查找指定元素,前提是该数组已经处于有序状态.与直接插入排序的效果相同,只是更快了一些,因 为折半插入排序可以更快地确定第i个元素的插入位置 代码: package interview; /** * @author Administrat

  • 图文讲解Java中实现quickSort快速排序算法的方法

    相对冒泡排序.选择排序等算法而言,快速排序的具体算法原理及实现有一定的难度.为了更好地理解快速排序,我们仍然以举例说明的形式来详细描述快速排序的算法原理.在前面的排序算法中,我们以5名运动员的身高排序问题为例进行讲解,为了更好地体现快速排序的特点,这里我们再额外添加3名运动员.实例中的8名运动员及其身高信息详细如下(F.G.H为新增的运动员): A(181).B(169).C(187).D(172).E(163).F(191).G(189).H(182) 在前面的排序算法中,这些排序都是由教练主

  • Java实现冒泡排序算法及对其的简单优化示例

    原理 冒泡排序大概是所有程序员都会用的算法,也是最熟悉的算法之一. 它的思路并不复杂: 设现在要给数组arr[]排序,它有n个元素. 1.如果n=1:显然不用排了.(实际上这个讨论似乎没什么必要) 2.如果n>1: (1)我们从第一个元素开始,把每两个相邻元素进行比较,如果前面的元素比后面的大,那么在最后的结果里面前者肯定排在后面.所以,我们把这两个元素交换.然后进行下两个相邻的元素的比较.如此直到最后一对元素比较完毕,则第一轮排序完成.可以肯定,最后一个元素一定是数组中最大的(因为每次都把相对

  • 三种简单排序算法(使用java实现)

    一.冒泡排序 算法思想:遍历待排序的数组,每次遍历比较相邻的两个元素,如果他们的排列顺序错误就交换他们的位置,经过一趟排序后,最大的元素会浮置数组的末端.重复操 作,直到排序完成. 示例演示: 算法实现: for(int i=0;i<array.length-1;i++){//最多排序n-1次 for(int j=0;j<array.length-i-1;j++){//需要交换的次数 if(array[j]>array[j+1]){ int temp=array[j]; array[j]

  • 详解堆排序算法原理及Java版的代码实现

    概述 堆排序是一种树形选择排序,是对直接选择排序的有效改进. 堆的定义如下:具有n个元素的序列(k1,k2,...,kn), 当且仅当满足: 时称之为堆.由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)或最大项(大顶堆). 若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点(有子女的结点)的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的. (a)大顶堆序列:(96, 83, 27, 38, 11, 09) (b)小顶堆序列:(12, 36,

  • 详解Dijkstra算法原理及其C++实现

    目录 什么是最短路径问题 Dijkstra算法 实现思路 案例分析 代码实现 什么是最短路径问题 如果从图中某一顶点(称为源点)到达另一顶点(称为终点)的路径可能不止一条,如何找到一条路径使得沿此路径上各边上的权值总和达到最小. 单源最短路径问题是指对于给定的图G=(V,E),求源点v0到其它顶点vt的最短路径. Dijkstra算法 Dijkstra算法用于计算一个节点到其他节点的最短路径.Dijkstra是一种按路径长度递增的顺序逐步产生最短路径的方法,是一种贪婪算法. Dijkstra算法

  • 图文详解感知机算法原理及Python实现

    目录 写在前面 1.什么是线性模型 2.感知机概述 3.手推感知机原理 4.Python实现 4.1 创建感知机类 4.2 更新权重与偏置 4.3 判断误分类点 4.4 训练感知机 4.5 动图可视化 5.总结 写在前面 机器学习强基计划聚焦深度和广度,加深对机器学习模型的理解与应用.“深”在详细推导算法模型背后的数学原理:“广”在分析多个机器学习模型:决策树.支持向量机.贝叶斯与马尔科夫决策.强化学习等. 本期目标:实现这样一个效果 1.什么是线性模型 线性模型的假设形式是属性权重.偏置与属性

  • 详解Bagging算法的原理及Python实现

    目录 一.什么是集成学习 二.Bagging算法 三.Bagging用于分类 四.Bagging用于回归 一.什么是集成学习 集成学习是一种技术框架,它本身不是一个单独的机器学习算法,而是通过构建并结合多个机器学习器来完成学习任务,一般结构是:先产生一组"个体学习器",再用某种策略将它们结合起来,目前,有三种常见的集成学习框架(策略):bagging,boosting和stacking 也就是说,集成学习有两个主要的问题需要解决,第一是如何得到若干个个体学习器,第二是如何选择一种结合策

  • 详解MD5算法的原理以及C#和JS的实现

    目录 一.简介 二.C# 代码实现 三.js 代码实现 一.简介 MD5 是哈希算法(散列算法)的一种应用.Hash 算法虽然被称为算法,但实际上它更像是一种思想.Hash 算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是 Hash 算法. 算法目的就是,把任意长度的输入(又叫做预映射 pre-image),通过散列算法变换成固定长度的输出,该输出就是散列值. 注意,不同的输入可能会散列成相同的输出,所以不能从散列值来确定唯一的输入值. 散列函数简单的说就是:一种将任意长度的消息压缩

  • 详解编译器编译原理

    详解编译器编译原理 什么是gcc  什么是gcc:gcc是GNU Compiler Collection的缩写.最初是作为C语言的编译器(GNU C Compiler),现在已经支持多种语言了,如C.C++.Java.Pascal.Ada.COBOL语言等. gcc支持多种硬件平台,甚至对Don Knuth 设计的 MMIX 这类不常见的计算机都提供了完善的支持 gcc主要特征  1)gcc是一个可移植的编译器,支持多种硬件平台 2)gcc不仅仅是个本地编译器,它还能跨平台交叉编译. 3)gcc

  • 详解K-means算法在Python中的实现

    K-means算法简介 K-means是机器学习中一个比较常用的算法,属于无监督学习算法,其常被用于数据的聚类,只需为它指定簇的数量即可自动将数据聚合到多类中,相同簇中的数据相似度较高,不同簇中数据相似度较低. K-MEANS算法是输入聚类个数k,以及包含 n个数据对象的数据库,输出满足方差最小标准k个聚类的一种算法.k-means 算法接受输入量 k :然后将n个数据对象划分为 k个聚类以便使得所获得的聚类满足:同一聚类中的对象相似度较高:而不同聚类中的对象相似度较小. 核心思想 通过迭代寻找

  • 详解Unique SQL原理和应用

    1.什么是Unique SQL 用户执行SQL语句时,每一个SQL语句文本都会进入解析器(Parser),生成"解析树"(parse tree).遍历解析树中各个结点,忽略其中的常数值,以一定的算法结合树中的各结点,计算出来一个整数值,用来唯一标识这一类SQL,这个整数值被称为Unique SQL ID,Unique SQL ID相同的SQL语句属于同一个"Unique SQL". 例如,用户先后输入如下两条SQL语句: select * from t1 where

  • 详解Dijkstra算法之最短路径问题

    一.最短路径问题介绍 问题解释: 从图中的某个顶点出发到达另外一个顶点的所经过的边的权重和最小的一条路径,称为最短路径 解决问题的算法: 迪杰斯特拉算法(Dijkstra算法) 弗洛伊德算法(Floyd算法) SPFA算法 这篇博客,我们就对Dijkstra算法来做一个详细的介绍 二.Dijkstra算法介绍 2.1.算法特点 迪科斯彻算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树.该算法常用于路由算法或者作为其他图算法的一个子模块. 2.2.算法的

  • 详解Python字符串原理与使用的深度总结

    目录 什么是 Python 字符串 ASCII 表与 Python 字符串字符 字符串属性 字符串方法 字符串操作 写在最后 今天我们来学习字符串数据类型相关知识,将讨论如何声明字符串数据类型,字符串数据类型与 ASCII 表的关系,字符串数据类型的属性,以及一些重要的字符串方法和操作,超级干货,不容错过! 什么是 Python 字符串 字符串是包含一系列字符的对象.字符是长度为 1 的字符串.在 Python 中,单个字符也是字符串.但是比较有意思的是,Python 编程语言中是没有字符数据类

随机推荐