原创文章出自公众号:「码农富哥」,欢迎转载和关注,如转载请注明出处!

堆基本概念

堆排序是一个很重要的排序算法,它是高效率的排序算法,复杂度是O(nlogn),堆排序不仅是面试进场考的重点,而且在很多实践中的算法会用到它,比如经典的TopK算法、小顶堆用于实现优先级队列。

堆排序是利用堆这种数据结构所设计的一种排序算法。堆实际上是一个完全二叉树结构。

问:那么什么是完全二叉树呢?

答:假设一个二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

我们知道堆是一个完全二叉树了,那么堆又分两种堆:大顶堆小顶堆

它们符合一个重要的性质:

  • 小顶堆满足: Key[i] <= key[2i+1] && Key[i] <= key[2i+2]
  • 大顶堆满足: Key[i] >= Key[2i+1] && key >= key[2i+2]

怎么理解呢,其实很简单,顾名思义,大顶堆最大的元素在跟节点,堆的性质决定了大顶堆中节点一定大于等于其子节点,反之,小顶堆的最小元素在根节点。我们来看看大顶堆和小顶堆的示意图:

堆排序基本思想及步骤

堆排序有以下几个核心的步骤:

  1. 将待排序的数组初始化为大顶堆,该过程即建堆。
  2. 将堆顶元素与最后一个元素进行交换,除去最后一个元素外可以组建为一个新的大顶堆。
  3. 由于第二部堆顶元素跟最后一个元素交换后,新建立的堆不是大顶堆,需要重新建立大顶堆。重复上面的处理流程,直到堆中仅剩下一个元素。

假设我们有一个待排序的数组 arr = [4, 6, 7, 2, 9, 8, 3, 5], 我们把这个数组构造成为一个二叉树,如下图:

问:此时我们需要把这个完全二叉树构造成一个大顶堆,怎么构造呢?

答:一个很好的方法是遍历二叉树的非叶子节点自下往上的构造大顶堆,针对每个非叶子节点,都跟它的左右子节点比较,把最大的值换到这个子树的父节点。

问:为什么要从非叶子节点开始,而不是从最后一个节点开始?

答:因为叶子节点下面没有子节点了,就没必要操作了。

问:为什么要从下往上而不是从上往下遍历非叶子节点?

答:我们从下面开始遍历调整每个节点成为它左右节点的最大值,那么一直往上的话,最后根节点一定是最大的值;但是如果我们从上往下,上面满足了大顶堆,下面不满足,调整后,上面可能又不满足了,所以从下往上是最好的方案。

那么我们构造的大顶堆的代码就很明显了:

# 构造大顶堆,从非叶子节点开始倒序遍历,因此是l//2 -1 就是最后一个非叶子节点
l = len(arr)
for i in range(l//2-1, -1, -1):
build_heap()
# 遍历针对每个非叶子节点构造大顶堆

看我们的例子,非叶子节点有2, 8, 6, 4, 我们从最后一个非叶子节点,也就是5开始遍历构造大顶堆,2 跟 5 比较,5比较大,所以把 arr[3]和arr[7]从数组中交换一下位置,那么就完成第一个非叶子节点的置换。下面的节点继续交换







此时9跟4交换后,4这个节点下面的树就不是不符合大顶堆了,所以要针对4这个节点跟它的左右节点再次比较,置换成较大的值,4跟左右子节点比较后,应该跟6交换位置。



那么至此,整个二叉树就是一个完完整整的大顶堆了,每个节点都不小于左右子节点。

此时我们把堆的跟节点,即数组最大值9跟数组最后一个元素2交换位置,那么9就是排好序的放在了数组最后一个位置

2到了跟节点后,新的堆不满足大顶堆,我们需要重复上面的步骤,重新构造大顶堆,然后把大顶堆根节点放到二叉树后面作为排好序的数组放好。就这样利用大顶堆一个一个的数字排好序。

值得注意的一个地方是,上面我们把9和2交换位置后,2处于二叉树根节点,2需要跟右子树8交换位置,交换完位置后,右子树需要重新递归调整大顶堆,但是左子树6这边,已经是满足大顶堆属性,因为不需要再操作。

我们再看看堆排序的一个直观的动图吧:

代码实现:

class Solution(object):
def heap_sort(self, nums):
i, l = 0, len(nums)
self.nums = nums
# 构造大顶堆,从非叶子节点开始倒序遍历,因此是l//2 -1 就是最后一个非叶子节点
for i in range(l//2-1, -1, -1):
self.build_heap(i, l-1)
# 上面的循环完成了大顶堆的构造,那么就开始把根节点跟末尾节点交换,然后重新调整大顶堆
for j in range(l-1, -1, -1):
nums[0], nums[j] = nums[j], nums[0]
self.build_heap(0, j-1) return nums def build_heap(self, i, l):
"""构建大顶堆"""
nums = self.nums
left, right = 2*i+1, 2*i+2 ## 左右子节点的下标
large_index = i
if left <= l and nums[i] < nums[left]:
large_index = left if right <= l and nums[left] < nums[right]:
large_index = right # 通过上面跟左右节点比较后,得出三个元素之间较大的下标,如果较大下表不是父节点的下标,说明交换后需要重新调整大顶堆
if large_index != i:
nums[i], nums[large_index] = nums[large_index], nums[i]
self.build_heap(large_index, l)

堆排序复杂度

时间复杂度, 包括两个方面:

  1. 初始化建堆过程时间:O(n)
  2. 更改堆元素后重建堆时间:O(nlogn),循环 n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn - logn ,所以复杂度是 O(nlogn)

时间复杂度:O(nlogn)

空间复杂度: 因为堆排序是就地排序,空间复杂度为常数:O(1)

堆排序的应用:TopK算法

面试中经常考的一个面试题就是,如果在海量数据中找出最大的100个数字,看到这个问题,可能大家首先会想到的是使用高效排序算法,比如快排,对这些数据排序,时间复杂度是O(nlogn),然后取出最大的100个数字。但是如果数据量很大,一个机器的内存不足以一次过读取这么多数据,就不能使用这个方法了。

不使用分布式机器计算,使用一个机器也能找出TopK的经典算法就是使用堆排序了,具体方法是:

维护一个大小为 K 的小顶堆,依次将数据放入堆中,当堆的大小满了的时候,只需要将堆顶元素与下一个数比较:

  • 如果小于堆顶元素,则直接忽略,比较下一个元素;
  • 如果大于堆顶元素,则将当前的堆顶元素抛弃,并将该元素插入堆中。遍历完全部数据,Top K 的元素也自然都在堆里面了。



整个操作中,遍历数组需要O(n)的时间复杂度,每次调整小顶堆的时间复杂度是O(logK),加起来就是 O(nlogK) 的复杂度,如果 K 远小于 n 的话, O(nlogK) 其实就接近于 O(n) 了,甚至会更快,因此也是十分高效的。

总结

堆排序有以下几个核心的步骤:

  1. 将待排序的数组初始化为大顶堆,该过程即建堆。
  2. 将堆顶元素与最后一个元素进行交换,除去最后一个元素外可以组建为一个新的大顶堆。
  3. 由于第二部堆顶元素跟最后一个元素交换后,新建立的堆不是大顶堆,需要重新建立大顶堆。重复上面的处理流程,直到堆中仅剩下一个元素。

最后

文章如果对你有收获,可以收藏转发,这也是对我写作的肯定!另外可以关注我公众号「码农富哥」 ,我会持续输出Python,服务端架构,计算机基础(MySQL, Linux,TCP/IP)的 原创 文章

Python 实现转堆排序算法原理及时间复杂度(多图解释)的更多相关文章

  1. Canny边缘检測算法原理及其VC实现具体解释(一)

    图象的边缘是指图象局部区域亮度变化显著的部分,该区域的灰度剖面一般能够看作是一个阶跃,既从一个灰度值在非常小的缓冲区域内急剧变化到还有一个灰度相差较大的灰度值.图象的边缘部分集中了图象的大部分信息,图 ...

  2. 第一篇:K-近邻分类算法原理分析与代码实现

    前言 本文介绍机器学习分类算法中的K-近邻算法并给出伪代码与Python代码实现. 算法原理 首先获取训练集中与目标对象距离最近的k个对象,然后再获取这k个对象的分类标签,求出其中出现频数最大的标签. ...

  3. 梯度迭代树(GBDT)算法原理及Spark MLlib调用实例(Scala/Java/python)

    梯度迭代树(GBDT)算法原理及Spark MLlib调用实例(Scala/Java/python) http://blog.csdn.net/liulingyuan6/article/details ...

  4. Python实现的选择排序算法原理与用法实例分析

    Python实现的选择排序算法原理与用法实例分析 这篇文章主要介绍了Python实现的选择排序算法,简单描述了选择排序的原理,并结合实例形式分析了Python实现与应用选择排序的具体操作技巧,需要的朋 ...

  5. 【机器学习】Apriori算法——原理及代码实现(Python版)

    Apriopri算法 Apriori算法在数据挖掘中应用较为广泛,常用来挖掘属性与结果之间的相关程度.对于这种寻找数据内部关联关系的做法,我们称之为:关联分析或者关联规则学习.而Apriori算法就是 ...

  6. PageRank算法原理与Python实现

    一.什么是pagerank PageRank的Page可是认为是网页,表示网页排名,也可以认为是Larry Page(google 产品经理),因为他是这个算法的发明者之一,还是google CEO( ...

  7. Python实现各种排序算法的代码示例总结

    Python实现各种排序算法的代码示例总结 作者:Donald Knuth 字体:[增加 减小] 类型:转载 时间:2015-12-11我要评论 这篇文章主要介绍了Python实现各种排序算法的代码示 ...

  8. GBDT算法原理深入解析

    GBDT算法原理深入解析 标签: 机器学习 集成学习 GBM GBDT XGBoost 梯度提升(Gradient boosting)是一种用于回归.分类和排序任务的机器学习技术,属于Boosting ...

  9. Javascript中的冒泡排序,插入排序,选择排序,快速排序,归并排序,堆排序 算法性能分析

    阿里面试中有一道题是这样的: 请用JavaScript语言实现 sort 排序函数,要求:sort([5, 100, 6, 3, -12]) // 返回 [-12, 3, 5, 6, 100],如果你 ...

随机推荐

  1. 定时器之Quart.net(2)

    第一步:Install-Package Quartz namespace ProjectEdb { class Program { static void Main(string[] args) { ...

  2. 引用类型(C# 参考)

    C# 中有两种类型:引用类型和值类型. 引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据. 对于引用类型,两种变量可引用同一对象:因此,对一个变量执行的操作会影响另一个变量所引用 ...

  3. vue+jest+vue-test-utils 单元测试

           jest是Facebook的一套开源的JavaScript测试框架,它集成了快照测试.断言.mock以及覆盖率报告等功能,很全面而且基本不需要太多的配置便可使用Vue-Test-Util ...

  4. Java多线程的创建(一)

    方法一:继承Thread类实现 1.创建一个类A,并继承Thread类 2.重写A的run()方法 3.创建A的实例对象b,即创建了线程对象 4.使用b调用start()方法:启动线程(会自动调用ru ...

  5. Spring Boot 入门(十二):报表导出,对比poi、jxl和esayExcel的效率

    本片博客是紧接着Spring Boot 入门(十一):集成 WebSocket, 实时显示系统日志写的 关于poi.jxl和esayExcel的介绍自行百度. jxl最多支持03版excel,所以单个 ...

  6. LeetCode 第15题-三数之和

    1. 题目 2.题目分析与思路 3.思路 1. 题目 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且 ...

  7. 真机调试报The executable was signed with invalid entitlements.错误

    真机运行时,提示The executable was signed with invalid entitlements.(The entitlements specified in your appl ...

  8. Go Web 编程之 数据库

    概述 数据库用来存储数据.只要不是玩具项目,每个项目都需要用到数据库.现在用的最多的还是 MySQL,PostgreSQL的使用也在快速增长中. 在 Web 开发中,数据库也是必须的.本文将介绍如何在 ...

  9. 聊一聊 InnoDB 引擎中的这些索引策略

    在上一篇中,我们简单的介绍了一下 InnoDB 引擎的索引类型,这一篇我们继续学习 InnoDB 的索引,聊一聊索引策略,更好的利用好索引,提升数据库的性能,主要聊一聊覆盖索引.最左前缀原则.索引下推 ...

  10. CentOS7 zabbix4.0搭建配置

    一.Zabbix-Server服务器端的安装 概述:10050是Agent的端口,Agent采用被动方式,Server主动连接Agent的10050端口:10051是Server的端口,Agent采用 ...