七大排序算法的个人总结(三)
堆排序(Heap):
要讲堆排序之前先要来复习一下完全二叉树的知识。
定义:
对一棵具有n个结点的二叉树按层序编号,如果编号为i(0 <= i <= n)的结点与同样深度的满二叉树编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
如上面就是一棵完全二叉树。
我们主要会使用的的性质是父结点与子结点的关系:
标号为n的结点的左孩子为2 * n + 1(如果有的话),右孩子为2 * n + 2(如果有的话)。
由于完全二叉树的结点的编号是连接的,所以我们可以用一个数组来保存这种数据结构。结点之间的关系可以通过上面的公式进行计算得到。
那什么是堆呢?
堆是具有下列性质的完全二叉树:
每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(或大根堆);或者每个结点的值都小于或等于其左右孩子结点的值。称为小顶堆(小根堆)。
如图:就是一大根堆。将它转化为数组就是这样的:
{ 9,7,5,6,1,4,2,0,3 }
可以看到一个大概的情况是:第0个元素是最大的,前面的元素普遍比后面的大,但这不是绝对的比如例子中的1就跑到4前边去了。
建堆:
那接下来就是第一个问题了,怎么创建一个大根堆呢?也就是解决怎么将给定的一个数组调整成大根堆?
假如我们给定一个比较极端的例子{ 10,20,30,40,50,60,70,80 },加个0是为了方便不与结点的编号产生混淆。
对于这样的一个堆,我们应该怎么进行调整呢?
对于堆排序而言,一个比较直观的想法就是从下面开始,把值比较大的元素往上推。这样进行到根位置时,就可以得到一个一个最大的根了。
所以,我们应该从最后一个非叶子结点开始调整。
那么怎么确定哪一个是最后一个非叶子结点呢?
其实这完全是可以从完全二叉树的性质中得到的。还记得吗?
左孩子为2 * n + 1;
右孩子为2 * n + 2;
所以最后一个非叶子结点的编号为array.length / 2 – 1。array就是给定的数组。
所以我们第一个要调整的结点是编号为3的结点,拿它的值跟两个孩子的值做比较(它只有一个孩子)。显然,40和80这两个要交换位置了。
接下来就轮到编号为2的结点了,进行比较后显然是70比较大一点,也进行交换:
同样的道理,编号为1的结点也进行调节:
请注意,这个时候问题就来了。结点1是符合条件了,可以对于以结点3这根的这棵子树就不符合大根堆的要求了,所以我们要重新对编号为3的结点再做一次调整。得到:
我们以同样的方法对编号为0的结点也进行同样的调整。最后就可以得到第一个大根堆了。
这一个过程我们可以称为建堆。我们将数据展开成数组:
{ 80,50,70,40,10,60,30,20 }
不难发现这一个过程中,我们已经把很多值比较大的数字也放到了比较靠前的位置。这一点相当重要,也可以说是堆排序的精华所在。
得到了大根堆之后,我们是可以得到一个最大值了,接下来要做的,就是不断的移除这个堆顶值,与堆尾的值进行交换,堆的长度减小1,然后进行重新的调整。
显然,每次都是在堆顶删除,在堆顶开始调整。
之后就是一直重复这个过程直到只剩下一个元素时,就可以完成排序工作了。
相信只要跟着这个思路和这几张图,自己模拟几次还是很好理解的。
接下来看看代码是怎么实现的:
public static void sort(int[] array) {init(array);// 这个过程就是不断的从堆顶移除,调整for (int i = 1; i < array.length; i++) {int temp = array[0];int end = array.length - i;array[0] = array[end];array[end] = temp;adjust(array, 0, end);}}private static void init(int[] array) {for (int i = array.length / 2 - 1; i >= 0; i--) {adjust(array, i, array.length);}}private static void adjust(int[] array, int n, int size) {int temp = array[n]; // 先拿出数据int child = n * 2 + 1; // 这个是左孩子while (child < size) { // 这个保证还有左孩子// 如果右孩子也存在的话,并且右孩子的值比左孩子的大if (child + 1 < size && array[child + 1] > array[child]) {child++;}if (array[child] > temp) {array[n] = array[child];n = child; // n需要重新计算child = n * 2 + 1; // 重新计算左孩子} else {// 这种情况说明左右孩子的值都比父结点的值小break;}}array[n] = temp;}
堆排序的代码量比较多,主要的工作其实是在adjust上。
在adjust这个过程中有几个要注意的:
一个是要注意数组的边界,因为我们每次是把最大值放在最后,然后它就不能再参与调整了。
其次,是最后一个非叶子结点可能只有一个孩子,这也是需要注意的。
堆排序到底快在哪呢?
还是来看一个极端的例子:
{ 1,2,3,4,5,6,7 }
在建堆的时候第一次比较之后的结果应该是这样的:(7和3交换了位置)
{ 1,2,7,4,5,6,3 }
第二次调整之后是:
{ 1,5,7,4,2,6,3 }(5和2交换了位置)
然后是:
{ 7,5,1,4,2,6,3 }(7和1交换了位置,1的位置不对,需要再调整)
{ 7,5,6,4,2,1,3 }(6和1交换了位置)
可以看到,仅仅用了4次比较和4次交换就已经把数组给调整成“比较有序”了。
这个其实是由完全二叉树的性质决定的,因为子结点的编号和父结点的编号存在着两倍(粗略)的差距。
也就说父结点与子结点的数据进行一次交换移动的距离是比较大的(相对于步进)。这个与冒泡和直接插入的“步进”是有明显的区别的。可以说,堆排序的优势在于它具有高效的元素移动效率(这是个人总结,不严谨)。
其次,我们在调整堆的时候,可以发现有一半的数据是我们不用动到的。这就使比较次数大大地减少。这个就是很好地利用在建堆的时候保存下来的状态。还是那句话“让上一次的操作结果为下一次操作服务”。
最后回顾一下七个排序:
冒泡排序:好吧,它是中枪次数最多的,最大的优点应该是衬托其他算法的高效。
选择排序:我个人认为它是最符合人的思维习惯的,缺点在于比较次数太多了,但其实它在对少量数据,或者是对于只排序一部分(比如只选出前十名之类的),这种情况下,选择排序就很不错了,因为它可以“部分排序”。
直接插入排序:其实它还不算太差,在应对一些平时的使用时,性能还是可以的。直接插入排序是希尔排序的基础。
希尔排序:这个曾经把我纠结很久的算法,它的外表很难让人看出它的强大。它在几个比较高效的排序算法中代码是最少的,也很容易一次性写出。但理解有点困难。我觉得主要是那个步长序列太难让人一眼看出它到底做了些什么。个人觉得要理解希尔排序首先要弄清楚“基本有序”这个有什么用和希尔排序的前n-1个步长做的就是这些事。先让整个数组变得基本有序,基于一个事实,就是对于基本有序的数组而言,直接插入排序的效率是很高的。
归并排序:分治和递归的经典使用,胜就胜在元素的比较次数比较少(貌似说是最少的)。缺点是需要比较大的辅助空间,这个有时会成为限制条件(因为过大的空间消耗有时是不允许的)。
快速排序:如其名,虽存在一定的不稳定性,理论上在最差的情况下,快速排序会退化成选择排序,但可以通过一些手段来使这种情况发生的概率相当的小。
堆排序:个人觉得是最难一口气写出来的排序算法,特别是调整结点的算法每次都要写得小心翼翼(当然,可能是平时写得少)。但它确实是一个很优秀的排序算法,堆排序在元素的移动效率和比较次数上都是比较优秀的。操作系统中堆可是一个重要的数据结构。我记得当时第一次写出堆排序的感叹是“原来数组还可以这么用”。
最后让这几大高手进行一次PK吧,测试的数据是3000000个范围在0 ~ 30000000的随机数。
得到的结果大概是这样的:
差距并不算太大,可以看到,最快的还是Java类库提供的方法,它为什么能比快速排序还快呢?
因为它是综合了其他几个算法的特点,比如说在元素很少的时候,直接插入排序可能会快一点,数据量大一点的时候归并可能会快一点,当数据很大的时候,用快速排序可以把数组分成小部分。所以它不是一个人在战斗!
好了,至此,七个排序算法也算是复习了一次,还是那句话,本人菜鸟一个,对这几个算法理解有限,出错之处还请各位指出。
一点个人感受,算法这东西有时以为自己弄懂了,其实还差得远了,有时候看十次书不如自己写一次代码,写了十次代码不如跟别人讲一次。因为这个过程会遇到很多自己以前从没想过的事。这就是我写博客的初衷。
from: https://www.cnblogs.com/yjiyjige/p/3258849.html
七大排序算法的个人总结(三)相关推荐
- 排序算法之low B三人组
排序算法之low B三人组 排序low B三人组 列表排序:将无序列表变成有充列表 应用场景:各种榜单,各种表格,给二分法排序使用,给其他算法使用 输入无序列表,输出有序列表(升序或降序) 排序low ...
- 七大排序算法大汇总(上)
目录 一.[前言]排序的稳定性: 二.七大排序总览 三.插入排序 1.1直接插入排序 1.2直接插入排序优化版--折半插入排序: 2.希尔排序 四.选择排序 1.1选择排序 1.2进阶版选择排序 2. ...
- 七大排序算法的个人总结(二)
归并排序(Merge Sort): 归并排序是一个相当"稳定"的算法对于其它排序算法,比如希尔排序,快速排序和堆排序而言,这些算法有所谓的最好与最坏情况.而归并排序的时间复杂度是固 ...
- 七大排序算法的个人总结(一)
冒泡排序(Bubble Sort): 很多人听到排序第一个想到的应该就是冒泡排序了.也确实,冒泡排序的想法非常的简单:大的东西沉底,汽泡上升.基于这种思想,我们可以获得第一个版本的冒泡: public ...
- 排序算法(01)— 三种简单排序(冒泡、插入、选择)
一.概述 排序是数据处理中十分常见且核心的操作,虽说实际项目开发中很小几率会需要我们手动实现,毕竟每种语言的类库中都有n多种关于排序算法的实现.但是了解这些精妙的思想对我们还是大有裨益的. 1.1 排 ...
- 七大排序算法—图文详解(插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序,归并排序)
作者:渴望力量的土狗 博客主页:渴望力量的土狗的博客主页 专栏:数据结构与算法 工欲善其事必先利其器,给大家介绍一款超牛的斩获大厂offer利器--牛客网 点击免费注册和我一起刷题吧 目录 插入排序: ...
- 数据结构七大排序算法图解
系列文章整合 排序是计算机程序设计中一个非常重要的操作,它将一个数据元素(或记录)的任意序列重新排列成一个按关键字有序的序列,在有序的序列中查找元素的效率很高,但是无序序列只能逐一查找,因此,如何进行 ...
- 【排序算法】冒泡排序的三种方法
冒泡排序算法: 最优时间复杂度为o(n),即当且只当元素本身就是按照从小到大的顺序排列的(这里默认冒泡排序从小到大排列). 最差时间复杂度为o(n^2),当其中有任何一组数据不是按照从小到大排列. # ...
- 【好记性不如烂笔头】排序算法之归并排序(三)小和问题
归并排序(三)小和问题 前言 小和 思考 暴力 引申 思路 代码 注意 前言 上篇博客学习了归并排序,一种是遍历的方式实现,一种是迭代的方式实现,那么归并排序的思路只能用于排序嘛?也不是,这篇博客 ...
最新文章
- pip install skimage安装skimage库出错的解决办法
- Nancy简单实战之NancyMusicStore(二):打造首页
- Java关于Properties用法的总结(一)
- Hadoop部署方式-高可用集群部署(High Availability)
- 【HDU - 1847】Good Luck in CET-4 Everybody! (巴什博奕,PN图或sg函数)
- # Schedulerx正式登陆Ali-k8s应用目录
- CF938G Shortest Path Queries
- Django中datetime类型的相关操作(记录一下)
- linux man 后面的数字,man命令后面的数字
- Python 分析国庆热门旅游景点,告诉你哪些地方好玩、便宜、人又少!
- 微信小程序时间轴demo_微信小程序 - 时间轴(组件)
- 视频教程-思科入门CCNA初级网络工程师视频课程-网络技术
- PS快速修改颜色的小方法
- K.gather()
- H3C路由器多出口NQA+TRACK实现冗余
- IT十年人生过客-十二-痛并快乐着
- 错误的英语提示翻译 以及经常犯的无错误
- get请求 params参数传递以及获取
- 企业微信官方认证的好处是什么?
- 【排序专训】练习题 士兵站队(中位数应用) 解题报告
热门文章
- Java多线程编程模式实战指南(二):Immutable Object模式--转载
- iOS pod init 报错
- 【风控术语】数字金融欺诈行为名词表
- 【采用】【风险管理】(第一篇)风险管理核心指标
- 30年货币翻了300倍!如何能跑赢印钞机?
- 程序员如果也能像C罗一样自律和勤奋,必将成为大神!
- TensorFlow损失函数(loss function) 2017-08-14 11:32 125人阅读 评论(0) 收藏 举报 分类: 深度学习及TensorFlow实现(10) 版权声明:
- KNN与K-Means
- YEP共享平台释放宜人贷无限潜力
- Kaggle 数据挖掘比赛经验分享 (转载)