排序算法 —— 堆排序

引言

此文基于《经典数据结构——堆的实现》中堆结构,实现一个以堆处理排序的算法。

一、算法思想

基于堆结构的堆排序的算法思想非常简单,循环获取大根堆中的最大值(0位置的根节点)放到堆的末尾,直到将堆拿空

由于一个现成的大根堆可以实现 O(1) 时间复杂度的最大值返回,因此堆排序的主要时间消耗就是在 heapInsert 或 heapify 这类维护大根堆结构的过程上。

二、代码演示

首先将数组从0开始,模拟逐个放入的过程,循环 heapInsert 建堆

然后以整个数组为堆,模拟循环取出 0 位置(最大值)的操作,循环 heapify。

小提示,取出的最大值你可以放在原数组中(即堆尾位置,由于拿出元素会导致堆缩小,数组末尾会有空余位置),也可以新创建一个同长数组放入,这对于排序本身并无影响,只不过是会增加额外的空间复杂度。

    public static void heapSort(int[] arr) {if (arr == null || arr.length < 2)return;// 整体变为大根堆for (int i = 0; i < arr.length; i++) {heapInsert(arr, i);}// 以整个数组作为堆大小,假设此堆已满int heapSize = arr.length;swap(arr, 0, --heapSize);while (heapSize > 0) {swap(arr, 0, --heapSize);heapify(arr, 0, heapSize);}}

模拟入堆的 heapInsert 、和模拟出堆的 heapify:

    // heapInsertprivate static void heapInsert(int[] arr, int index) {int father = (index - 1) / 2;while (arr[index] > arr[father]) {swap(arr, index, father);index = father;father = (index - 1) / 2;}}// heapifyprivate static void heapify(int[] arr, int index, int heapSize) {int leftIdx = index * 2 + 1;while (leftIdx < heapSize) {int largest = leftIdx + 1 < heapSize && arr[leftIdx] < arr[leftIdx + 1] ? leftIdx + 1 : leftIdx;largest = arr[index] < arr[largest] ? largest : index;if (index == largest)break;else {swap(arr, index, largest);index = largest;leftIdx = index * 2 + 1;}}}// 交换数组元素private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}

完整代码及对数器:

public class HeapSort {public static void heapSort(int[] arr) {if (arr == null || arr.length < 2)return;// 整体变为大根堆for (int i = 0; i < arr.length; i++) {heapInsert(arr, i);}// 以整个数组作为堆大小,假设此堆已满int heapSize = arr.length;swap(arr, 0, --heapSize);while (heapSize > 0) {swap(arr, 0, --heapSize);heapify(arr, 0, heapSize);}}private static void heapInsert(int[] arr, int index) {int father = (index - 1) / 2;while (arr[index] > arr[father]) {swap(arr, index, father);index = father;father = (index - 1) / 2;}}/*** 结合了两个方向的入堆方式** @param arr* @param index* @param heapSize*/private static void heapifyNew(int[] arr, int index, int heapSize) {if (index == 0) {// 向下int left = index * 2 + 1;while (left < heapSize) {int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;largest = arr[index] < arr[largest] ? largest : index;if (largest == index) break;else {swap(arr, index, largest);index = largest;left = index * 2 + 1;}}} else if (index == heapSize - 1) {// 向上int father = (index - 1) / 2;while (arr[index] > arr[father]) {swap(arr, index, father);index = father;father = (index - 1) / 2;}}}private static void heapify(int[] arr, int index, int heapSize) {int leftIdx = index * 2 + 1;while (leftIdx < heapSize) {int largest = leftIdx + 1 < heapSize && arr[leftIdx] < arr[leftIdx + 1] ? leftIdx + 1 : leftIdx;largest = arr[index] < arr[largest] ? largest : index;if (index == largest)break;else {swap(arr, index, largest);index = largest;leftIdx = index * 2 + 1;}}}private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}// for testpublic static void comparator(int[] arr) {Arrays.sort(arr);}// for testpublic static int[] generateRandomArray(int maxSize, int maxValue) {int[] arr = new int[(int) ((maxSize + 1) * Math.random())];for (int i = 0; i < arr.length; i++) {arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());}return arr;}// for testpublic static int[] copyArray(int[] arr) {if (arr == null) {return null;}int[] res = new int[arr.length];for (int i = 0; i < arr.length; i++) {res[i] = arr[i];}return res;}// for testpublic static boolean isEqual(int[] arr1, int[] arr2) {if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {return false;}if (arr1 == null && arr2 == null) {return true;}if (arr1.length != arr2.length) {return false;}for (int i = 0; i < arr1.length; i++) {if (arr1[i] != arr2[i]) {return false;}}return true;}// for testpublic static void printArray(int[] arr) {if (arr == null) {return;}for (int i = 0; i < arr.length; i++) {System.out.print(arr[i] + " ");}System.out.println();}// for testpublic static void main(String[] args) {int testTime = 500000;int maxSize = 100;int maxValue = 100;boolean succeed = true;for (int i = 0; i < testTime; i++) {int[] arr1 = generateRandomArray(maxSize, maxValue);int[] arr2 = copyArray(arr1);heapSort(arr1);comparator(arr2);if (!isEqual(arr1, arr2)) {succeed = false;break;}}System.out.println(succeed ? "Nice!" : "Fucking fucked!");int[] arr = generateRandomArray(maxSize, maxValue);printArray(arr);heapSort(arr);printArray(arr);}
}

三、时间复杂度

结论:堆排序的时间复杂度是O(N * logN)。

简单说明一下各个步骤的大体时间复杂度,详细推导不做讨论。

堆排序突破不了这个复杂度,为什么?这是因为第二步取值调整无法改变O(N* logN),同时,基于比较的排序方法也没有比 O(N * logN) 更好的排序了。

首先,heapInsert 的时间复杂度是 O(logN) ,这个不难理解,因为是二叉树,每次向上比较和交换的次数只与堆的层高有关,而层高又约等于 logN ,因此调整一次的复杂度就是 O(logN)。

而建堆的过程是循环 heapInsert,因此建堆的时间复杂度就是 O(N * logN)。

同样,heapipfy 的时间复杂度也是 O(logN),每次下沉也只与层高有关。而循环下沉同样也是 O(N * logN)。

因此,除去一些常数时间复杂度和倍数项,最终可知堆排序的时间复杂度是 O(N * logN)。

扩展--建堆的两种方式

上面的代码以模拟入堆的方式建堆,循环 heapInsert ,时间复杂度是 O(N * logN)。

但是如果使用反向建堆 ,从数组最后一个元素开始,循环 heapify,那么时间复杂度会降 O(N)。

// O(N)
for (int i = arr.length - 1; i >= 0; i--) {heapify(arr, i, arr.length);
}

首先不考虑复杂度,但看这种建堆方式,就要比 heapInsert 更优,因为 heapify 是指定 i 位置向下沉,由于最后一层元素更多,而这些元素不需要向下沉,因此可以减少很多不必要的操作。那么每一层从下往上越来越少,向下沉的元素也会越来越少。

再来看时间复杂度。

我们从最后一个元素开始,执行 heapify,由于heapify是向下比较向下沉,因此叶子节点只看一眼自身就直接返回了,而堆的叶子节点数量大概是 N/2 数量级,因此,时间消耗公式可以是:

T(N) = (N / 2) * 1 + (N/4) * 2 + (N/8) * 3 + (N/16) * 4 ... 

这个算式如何求解?可以使用数学上常用的 扩倍相减:

2 * T(N) = N + (N / 2) * 2 + (N / 4) * 3 + (N / 8) * 4 ...

最后两式错位相减,得到 T(N) = N + N/2 + N/4 + N/8 + N/16.... 等比数列求和,当 N 无限大时,收敛于 O(N)。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558658.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

经典数据结构——前缀树

引言 前缀树——trie /ˈtraɪ//树&#xff0c;也叫作“单词查找树”、“字典树”。 它属于多叉树结构&#xff0c;典型应用场景是统计、保存大量的字符串&#xff0c;经常被搜索引擎系统用于文本词频统计。它的优点是利用字符串的公共前缀来减少查找时间&#xff0c;最大限度…

排序算法 —— 计数排序

引言 计数排序是桶排序思想的一种具体实现&#xff0c;针对一些具有特殊限制的样本数据&#xff0c;如公司员工年龄&#xff0c;那么样本数据本身就一定在0~200之间&#xff0c;针对这样的数据&#xff0c;使用从0到200 的桶数组&#xff0c;桶的位置已经是有序的&#xff0c;…

Java多线程 —— 线程状态迁移

引言 线程状态迁移&#xff0c;又常被称作线程的生命周期&#xff0c;指的是线程从创建到终结需要经历哪些状态&#xff0c;什么情况下会出现哪些状态。 线程的状态直接关系着并发编程的各种问题&#xff0c;本文就线程的状态迁移做一初步探讨&#xff0c;并总结在何种情况下…

Java中的Unsafe

Java和C语言的一个重要区别就是Java中我们无法直接操作一块内存区域&#xff0c;不能像C中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C手动管理内存的能力。 Unsafe类&#xff0c;全限定名是sun.misc.Unsafe&#xff0c;从名字中我们可以看出来这个类对…

arm中断保护和恢复_浅谈ARM处理器的七种异常处理

昨天的文章&#xff0c;我们谈了ARM处理器的七种运行模式&#xff0c;分别是&#xff1a;用户模式User(usr)&#xff0c;系统模式System(sys)&#xff0c;快速中断模式(fiq)&#xff0c;管理模式Supervisor(svc)&#xff0c;外部中断模式(irq)&#xff0c;数据访问中止模式Abor…

Queue —— JUC 的豪华队列组件

目录引言一、Queue 的继承关系1.1 Queue 定义基础操作1.2 AbstractQueue 为子类减负1.3 BlockingQueue 阻塞式Queue1.4 Deque 两头进出二、Queue 的重要实现三、BlockingQueue 的实现原理四、Queue 在生产者消费者模式中的应用五、Queue 在线程池中的应用六、ConcurrentLinkedQ…

daad转换器实验数据_箔芯片电阻在高温应用A/D转换器中的应用

工业/应用领域高温&#xff1a;地震数据采集系统、石油勘探监测、高精度检测仪产品采用&#xff1a;V5X5 Bulk Metal (R) Foil芯片电阻案例介绍TX424是一个完整的4通道24位模数转换器&#xff0c;采用40脚封装。该设计采用最先进设计方案&#xff0c;两个双通道24位调节器和一个…

excel分段排序_学会这个神操作,报表填报不再五花八门,效率远超Excel

在报表工作人员的的日常工作中&#xff0c;常常要面临统计混乱的终端用户输入的问题。由于无法准确限制用户的输入内容&#xff0c;所以在最终进行数据统计时&#xff0c;常常会出现数据不合法的情况。为此需要花费大量的人力和时间核对校验数据。举个简单的例子&#xff0c;某…

IDEA——必备插件指南

目录一、Free-Mybatis-Plugin二、Lombok三、jclasslib Bytecode Viewer一、Free-Mybatis-Plugin 二、Lombok 三、jclasslib Bytecode Viewer 学习 class 文件的必备插件。 使用简单&#xff0c;安装后可以在菜单 View 中看到 show bytecode with jclasslib&#xff1a; 效果…

jitter 如何优化网络_如何做好关键词优化网络?

越来越多的传统企业开始建立自己的网站&#xff0c;进而不断的推广自己的产品。为了能够让自己的企业网站出现在搜索引擎的首页&#xff0c;现在最常用的手段就是竞价排名和关键词优化网络。往往很多企业会选择关键词优化网络这种方式来推广自己的网站&#xff0c;对于新手seoe…

python学生名片系统_Python入门教程完整版400集(懂中文就能学会)快来带走

如何入门Python&#xff1f;权威Python大型400集视频&#xff0c;学了Python可以做什么&#xff1f;小编今天给大家分享一套高老师的python400集视频教程&#xff0c;里面包含入门进阶&#xff0c;源码&#xff0c;实战项目等等&#xff0c;&#xff0c;不管你是正在学习中&…

JVM——详解类加载过程

导航一、过程概述二、Loading2.1 类加载器2.2 双亲委派机制2.3 类在内存中的结构三、Linking四、Initializing一、过程概述 java 源文件编译后会生成一个 .class文件存储在硬盘上。 在程序运行时&#xff0c;会将用到的类文件加载到 JVM 内存中。从磁盘到内存的过程总共分为三…

下载 Java 学习的权威文档

JVMS 和 JLS 文档的下载 快速直达&#xff1a; https://docs.oracle.com/javase/8/ --> Java Language and Virtual Machine Specifications jvm specification 和 java language specification 是Java 学习的两个最权威的文档。如果你用的是 Java 8&#xff0c;就可以去下载…

iso图像测试卡_4700万像素 五轴防抖 徕卡正式发布SL2无反相机

出自蜂鸟网-器材频道&#xff0c;原文链接&#xff1a;https://m.fengniao.com/document/5358989.html徕卡于今日正式发布SL2相机&#xff0c;搭载4700万像素CMOS感光元件、通过感光元件移位实现光学图像稳定的五轴防抖技术、全新徕卡物距探测式自动对焦技术以及576万像素分辨率…

JVM——对象的创建与内存布局

导航一、对象的创建过程二、对象的内存布局2.1 内存布局2.2 计算对象的内存大小三、对象的定位3.1 句柄池3.2 直接指针四、对象的分配过程一、对象的创建过程 对象&#xff0c;又叫实例&#xff0c;是 OOP 的最常用角色。 如何创建一个对象&#xff1f;一般都是使用 new 关键…

JVM垃圾收集器——G1

导航引言一、G1 介绍1.1 适用场景1.2 设计初衷1.3 关注焦点1.4 工作模式1.5 堆的逻辑结构1.6 主要收集目标1.7 停顿预测模型1.8 拷贝和压缩1.9 与 CMS 和 Parallel 收集器的比较1.10 固定停顿目标二、堆的逻辑分区2.1 region2.2 CSet2.3 RSet2.4 Card Table三、G1 的工作原理3.…

的mvc_简述PHP网站开发的MVC模式

为了提高开发时候的代码重用和开发速度&#xff0c;php使用了mvc的模式&#xff0c;主要是对代码的功能进行了分类&#xff0c;M&#xff1a;model主要是对数据库进行操作&#xff0c;v&#xff1a;view主要是前端html文件操作&#xff0c;c&#xff1a;controller主要是编写基…

CAP 原则与 BASE 理论

导航引言一、CAP 原则1.1 Consistency 一致性1.2 Available 可用性1.3 Partition tolerance 分区容错性1.4 CAP 的矛盾1.5 CAP 的组合场景二、BASE 理论2.1 基本可用2.2 软状态2.3 最终一致性2.3.1 因果一致性2.3.2 读自身所写2.3.3 会话一致性2.3.4 单调读一致性2.3.5 单调写一…

java 教室借用管理系统_[内附完整源码和文档] 基于JAVA语言的学生选课信息管理系统...

摘 要本系统运用Java面向对象的方法设计而成。近年来&#xff0c;学生选课系统越来越在高校学生群体中得到普及&#xff0c;其所承担的功能也变得越来越丰富&#xff0c;所起到的作用也变得越来越重要&#xff0c;在被学校学生重视的同时&#xff0c;也意味着它的功能要更加完善…

jMeter 模拟 web 高并发请求

导航一、jmeter 简介与下载二、接口压测设置三、实战演示一、jmeter 简介与下载 Apache JMeter是Apache组织开发的基于Java的压力测试工具。 最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。JMeter 可以用于对服务器、网络或对象模拟巨大的负载&#xff0c…