拜托,面试别再问我堆(排序)了!

何为堆?

堆是一种特殊的树,只要满足下面两个条件,它就是一个堆:

(1)堆是一颗完全二叉树;

(2)堆中某个节点的值总是不大于(或不小于)其父节点的值。

其中,我们把根节点最大的堆叫做大顶堆,根节点最小的堆叫做小顶堆。

堆详解

满二叉树

满二叉树是指所有层都达到最大节点数的二叉树。比如,下面这颗树:

完全二叉树

完全二叉树是指除了最后一层其它层都达到最大节点数,且最后一层节点都靠左排列。比如,下面这颗树:

可见,其实满二叉树是一种特殊的完全二叉树。

那么,使用什么结构存储完全二叉树最节省空间呢?

我们可以看见,完全二叉树的节点都是比较紧凑的,且只有最后一层是不满的,所以使用数组是最节省空间的,比如上面这颗完全二叉树我们可以这样存储。

我们下标为0的位置不存储元素,从下标为1的位置开始存储元素,每层依次从左往右放到数组里来存储。

为什么下标0的位置不存在元素呢?

这是因为这样存储我们可以很方便地找到父节点,比如,4的父节点即4/2=2,5的父节点即5/2=2。

堆也是一颗完全二叉树,但是它的元素必须满足每个节点的值都不大于(或不小于)其父节点的值。比如下面这个堆:

前面我们说过完全二叉树适合使用数组来存储,那上面这个堆应该怎么存储呢?

同样地,我们下标为0的位置不存在元素,最后就变成下面这样。

这时候我们要找8的父节点就拿8的位置下标5/2=2,也就是5这个节点的位置,这也是为了我们后面堆化。

插入元素

往堆中插入一个元素后,我们需要继续满足堆的两个特性,即:

(1)堆是一颗完全二叉树;

(2)堆中某个节点的值总是不大于(或不小于)其父节点的值。

为了满足条件(1),所以我们把元素插入到最后一层最后一个节点往后一位的位置,但是插入之后可能不再满足条件(2)了,所以这时候我们需要堆化。

比如,上面那个堆我们需要插入元素2,我们把它放在9后面,这时不满足条件(2)了,我们就需要堆化。(这是一个小顶堆)

将完全二叉树和数组对照着来看。

在完全二叉树中,插入的节点与它的父节点相比,如果比父节点小,就交换它们的位置,再往上和父节点相比,如果比父节点小,再交换位置,直到比父节点大为止。

在数组中,插入的节点与n/2位置的节点相比,如果比n/2位置的节点小,就交换它们的位置,再往前与n/4位置的节点相比,如果比n/4位置的节点小,再交换位置,直到比n/(2^x)位置的节点大为止。

这就是插入元素时进行的堆化,也叫自下而上的堆化。

从插入元素的过程,我们知道每次与n/(2^x)的位置进行比较,所以,插入元素的时间复杂度为O(log n)。

删除堆顶元素

我们知道,在小顶堆中堆顶存储的是最小的元素,这时候我们把它删除会怎样呢?

删除了堆顶元素后,要使得还满足堆的两个特性,首先,我们可以把最后一个元素移到根节点的位置,这时候就满足条件(1),之后就是使它满足条件(2),就需要堆化了。

将完全二叉树和数组对照着来看。

在完全二叉树中,把最后一个节点放到堆顶,然后与左右子节点中小的交换位置(因为是小顶堆),依次往下,直到其比左右子节点都小为止。

在数组中,把最后一个元素移到下标为1的位置,然后与下标为2和3的位置对比,发现8比2大,且2是2和3中间最小的,所以与2交换位置;然后再下标为4和5的位置对比,发现8比5大,且5是5和7中最小的,所以与5交换位置,没有左右子节点了,堆化结束。

这就是删除元素时进行的堆化,也叫自上而下的堆化。

从删除元素的过程,我们知道把最后一个元素拿到根节点后,每次与2n和(2n+1)位置的元素比较,取其小者,所以,删除元素的时间复杂度也为O(log n)。

建堆

假定给定一组乱序的数组,我们该怎么建堆呢?

如下图所示,我们模拟依次往堆中添加元素。

(1)插入6这个元素,只有一个,不需要比较;

(2)插入8这个元素,比6大,不需要交换;

(3)插入3这个元素,比下标3/2=1的位置上的元素6小,交换位置;

(4)插入2这个元素,比下标4/2=2的位置上的元素8小,交换位置,比下标2/2=1的位置上的元素3小,交换位置;

(5)...

(10)最后,全部插入完成,即完成了建堆的过程。

我们知道,完全二叉树的高度h=log n,且第h层有1个元素,第(h-1)层有2个元素,第(h-2)层有2^2个元素,...,第1层有2^(h-1)个元素。

其实,建堆的整个过程中一个节点的比较次数是与它的高度k成正比的,比如,上图中的1这个元素,它也是从最后一层依次比较了3次(高度h=4),才到达了现在的位置。

所以,我们可以得出第h层的元素有1个,它最多需要比较(h-1)次;第(h-1)层有2个元素,它们最多比较(h-2)次;第(h-2)层有2^2个元素,它们最多比较(h-3)次;...;第1层有2^(h-1)个元素,它们最多比较0次。

因而,总和就如下图:

所以,建堆的时间复杂度就是O(n)。

堆排序

我们知道,对于小顶堆,堆顶存储的元素就是最小的。

那么,我们删除堆顶元素,堆化,第二小的跑堆顶了,再删除,再堆化,...,这些删除的元素是不是正好有序的?

当然是的,所以堆排序的过程就很简单了。

我们直接把堆顶的元素与第n个元素交换位置,再把前(n-1)个元素堆化,再把堆顶元素与第(n-1)个元素交换位置,再把前(n-2)个元素堆化,..,,进行下去,最后,数组中的元素就整个变成倒序的了,也就排序完了。

我们知道删除一个元素的时间复杂度是O(log n),那么删除n个元素正好是:

log n + log(n-1) + log(n-2) + log 1

这个公式约等于nlog n,所以堆排序的时间复杂度为O(nlog n)。

而且,这样排序不需要占用额外的空间,只需要交换元素的需要一个临时变量,所以堆排序的空间复杂度为O(1)?。?

总结

(1)堆是一颗完全二叉树;

(2)小(大)顶堆中的每一个节点都不小于(不大于)它的父节点;

(3)堆的插入、删除元素的时间复杂度都是O(log n);

(4)建堆的时间复杂度是O(n);

(5)堆排序的时间复杂度是O(nlog n);

(6)堆排序的空间复杂度是O(1)?;?

彩蛋

堆都有哪些应用呢?

其实,堆除了堆排序以外,还有很多其它的用途,比如求中位数,99%位数,定时任务等。

比如,求中位数的大致思路,是分别建立一个大顶堆和一个小顶堆,然后往这两个堆中放元素,当其中一个堆的元素个数比另外一个多2时,就平衡一下,这样所有元素都放完之后,两个堆顶的元素之一(或之二)就是中位数。



欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

原文地址:https://www.cnblogs.com/tong-yuan/p/Heap.html

时间: 2024-10-21 02:14:41

拜托,面试别再问我堆(排序)了!的相关文章

【深度】扒开V8引擎的源码,我找到了你们想要的前端算法(下次面试官再问算法,用它怼回去!)

算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索`Array.prototype.sort`函数下的算法实现. 来,先把你用过的和听说过的排序算法都列出来: * 快速排序 * 冒泡排序 * 插入排序 * 归并排序 * 堆排序 * 希尔排序 * 选择排序 * 计数排序 * 桶排序 * 基数排序 * ... 答题环节到了, sort 函数使用的以上哪一种算法? 如果你在网上搜索过关于 sort 源码的文章,可能会告诉你数组长度小于10用插入排序,否则用快速排序. 开始我也是

面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ 目录 本篇文章主要包括以下内容: HashMap 的存储结构 常用变量说明,如加载因子等 HashMap 的四个构造函数 tableSizeFor()方法及作用 put()方法详解 hash()方法,以及避免哈希碰撞的原理 resize()扩容机制及原理 get()方法 为什么HashMap链表会

以后面试官再问你三次握手和四次挥手,直接把这一篇文章丢给他

三次握手和四次挥手是各个公司常见的考点,也具有一定的水平区分度,也被一些面试官作为热身题.很多小伙伴说这个问题刚开始回答的挺好,但是后面越回答越冒冷汗,最后就歇菜了. 见过比较典型的面试场景是这样的: 面试官:请介绍下三次握手 求职者:第一次握手就是客户端给服务器端发送一个报文,第二次就是服务器收到报文之后,会应答一个报文给客户端,第三次握手就是客户端收到报文后再给服务器发送一个报文,三次握手就成功了. 面试官:然后呢? 求职者:这就是三次握手的过程,很简单的. 面试官:...... (番外篇:

拜托,别再问我 QPS、TPS、PV、UV、GMV、IP、RPS 好吗?

关于 QPS.TPS.PV.UV.GMV.IP.RPS 这些词语,看起来好像挺专业.但实际上,我认为是这是每个程序员必懂的知识点了,你可以搞不懂它们怎么计算的,但是你最少要知道它们分别代表什么意思吧?! QPS QPS:全名 Queries Per Second,意思是“每秒查询率”,是一台服务器每秒能够响应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准. 简单的说,QPS = req/sec = 请求数/秒.它代表的是服务器的机器的性能最大吞吐能力. 在网上,我看到有

面试官,不要再问我“Java GC垃圾回收机制”了

Java GC垃圾回收几乎是面试必问的JVM问题之一,本篇文章带领大家了解Java GC的底层原理,图文并茂,突破学习及面试瓶颈. 楔子-JVM内存结构补充 在上篇<JVM之内存结构详解>中有些内容我们没有讲,本篇结合垃圾回收机制来一起学习.还记得JVM中堆的结构图吗? 图中展示了堆中三个区域:Eden.From Survivor.To Survivor.从图中可以也可以看到它们的大小比例,准确来说是:8:1:1.为什么要这样设计呢,本篇文章后续会给出解答,还是根据垃圾回收的具体情况来设计的.

爱上面试的凑弟弟--你再问我单例模式试试?

本系列博客以情景对话形式,用一个又一个的小故事或者编程实例来组织,对于实际开发尤其是面试中经常遇到的知识点进行深入探讨. 本书人物及背景:小豪: 23岁,武汉某双非本科不知名专业大学四年级学生,成绩一般,面临毕业,对后端开发.Java很感兴趣,正求职找工作.宇哥: 跟小豪通过租房认识,两人是室友,26岁,毕业后长期从事软件开发工作,是一个半吊子工程师,兴趣爱好是吹牛,不打草稿那种. 1.1 面试失败 小豪热爱编程,觉得写代码.做网站开发无敌酷炫雕炸天,在某站上看完了某马基础班和就业班的教学视频,

面试再问ThreadLocal,别说你不会!

ThreadLocal是什么  以前面试的时候问到ThreadLocal总是一脸懵逼,只知道有这个哥们,不了解他是用来做什么的,更不清楚他的原理了.表面上看他是和多线程,线程同步有关的一个工具类,但其实他与线程同步机制无关. 线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每个线程创建一个单独的变量副本,每个线程都可以改变自己的变量副本而不影响其它线程所对应的副本. 官方API上是这样介绍的:该类提供了线程局部(thread-local)变量.这些变量不同于它们的普通对应物,因

数组去重是面试中经常问到的问题

数组去重是面试中经常问到的问题 [html] view plain copy var arr=[1,3,4,52,4,5,4,8,7,6]; 第一种方法:使用ES5中的indexOf进行去重: [javascript] view plain copy function arr1(){ var n=[]; for(var i=0;i<arr.length;i++){ if(n.indexOf(arr[i])==-1){ n.push(arr[i]); } } return n; }//先定义一个空

[转帖]别再问“分库分表”了,再问就崩溃了!

别再问“分库分表”了,再问就崩溃了! https://www.cnblogs.com/butterfly100/p/9034281.html “ 在谈论数据库架构和数据库优化的时候,我们经常会听到分库分表,分库分表其实涉及到很多难题,今天我们来汇总一下数据库分库分表解决方案. 图片来自 Pexels 数据切分 关系型数据库本身比较容易成为系统瓶颈,单机存储容量.连接数.处理能力都有限. 当单表的数据量达到 1000W 或 100G 以后,由于查询维度较多,即使添加从库.优化索引,做很多操作时性能