-
Notifications
You must be signed in to change notification settings - Fork 634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前端进阶算法9:看完这篇,再也不怕堆排序、Top K、中位数问题面试了 #60
Comments
对于我这个对二叉树、堆排序完全外行的人,遇到的一些问题,我参考了很多文章后发现的: |
@woaixiangbao 你的疑惑来源于你对完全二叉树的基本性质不熟悉。你可以尝试自己用一个数组来表示一下完全二叉树,然后找几个节点观察一下,就会发现完全二叉树的父子节点的位置是有规律的,至于这个规律是什么,可以记住,也可以不记,大不了用到的时候写个简单的二叉树推一下。 |
堆排序推荐看这个视频https://www.bilibili.com/video/BV1Eb41147dK?from=search&seid=16051892050619509521&spm_id_from=333.337.0.0 |
你把根节点用arr索引标记时标记索引为0,即你把arr[0]放到根节点,那么这个堆里根节点的索引为 i ,则根节点的左子节点的索引与根节点索引的关系就是=2i+1 ,当你把根节点用arr索引标记时标记索引为1,那么这个堆里根节点的索引为 i ,则根节点的左子节点的索引与根节点索引的关系就是=2i , 你自己画个堆的完全二叉树结构然后给它用索引标记一下看下过程就能很容易看出来根与左右子节点索引方面的对应关系的。一种情况就是你说的根节点填的是索引0, 上面层主给的解法中,它往根节点填的索引是1 。可以很容易看出来对应关系的,这个没有特别的科学依据,无厘头的,看出来的。 大根堆的基础知识网上搜了一个,如下文章: 它heapify 那个函数里为啥用 2i表示节点i的左子节点,2i+1表示节点i的右子节点。很明显 , i 指的是在数组里的索引,我们回想这个数组填满堆的顺序是从顶层往下层,每层从左往右依次填满,他们每个节点的值对应的在数组里的索引就是(0, 1, 2, 。。。或者就是1,2,3,...或者就是2,3,4,....。反正填满这个堆结构用到的这些arr[i],i 得是连续的,索引依次增加 1 )然后现在这个答案中堆顶部根它填充的是索引1,则根下面一层(即第二层)的索引是2和3,第三层的索引是4,5,6,7。此时依次按照索引递增然后从上层到下层,每层从左到右填下去,会发现,它里面任意拿出一个二叉结构(比如(1,2,3))的根节点索引是i,它左子节点的索引就是2i ,而它右子节点的索引就是2i+1 (就是比2*i 多 1,毕竟用数组里的值填充堆的过程是从上往下,从左往右挨着填充的。 )。 |
分隔线******* /**
} // 原地建堆,从后往前,自上而下式建大顶堆(顶元素是堆中最大的。)
} for(let i= k; i<arr.length; i++){ return heap; |
class Solution {
}; |
为什么加递归?我认为是:为了自顶向下都筛选一遍,如果不加递归那一句,代码就只比较了root和root的两个子节点。实际上,如果root值是最大或者最小的,它应该从该完全二叉树的顶部开始一次次向下交换位置,直到位置换到该完全二叉树的底部。 |
引言
堆是前端进阶必不可少的知识,也是面试的重难点,例如内存堆与垃圾回收、Top K 问题等,这篇文章将从基础开始梳理整个堆体系,按以下步骤来讲:
下面开始吧👇
一、堆
满足下面两个条件的就是堆:
如果堆上的任意节点都大于等于子节点值,则称为 大顶堆
如果堆上的任意节点都小于等于子节点值,则称为 小顶堆
也就是说,在大顶堆中,根节点是堆中最大的元素;
在小顶堆中,根节点是堆中最小的元素;
二、怎样创建一个大(小)顶堆
我们在上一节说过,完全二叉树适用于数组存储法( 前端进阶算法7:小白都可以看懂的树与二叉树 ),而堆又是一个完全二叉树,所以它可以直接使用数组存储法存储:
简单来说: 堆其实可以用一个数组表示,给定一个节点的下标
i
(i从1开始) ,那么它的父节点一定为A[i/2]
,左子节点为A[2i]
,右子节点为A[2i+1]
那么怎样去创建一个大顶堆(小顶堆)喃?
常用的方式有两种:
三、插入式建堆
插入节点:
代码实现:
时间复杂度: O(logn),为树的高度
四、原地建堆(堆化)
假设一组序列:
原地建堆的方法有两种:一种是承袭上面插入的思想,即从前往后、自下而上式堆化建堆;与之对应的另一种是,从后往前、自上往下式堆化建堆。其中
所以,自下而上式堆是调整节点与父节点(往上走),自上往下式堆化是调整节点与其左右子节点(往下走)。
1. 从前往后、自下而上式堆化建堆
这里以小顶堆为例,
代码实现:
测试成功
2. 从后往前、自上而下式堆化建堆
这里以小顶堆为例
注意:从后往前并不是从序列的最后一个元素开始,而是从最后一个非叶子节点开始,这是因为,叶子节点没有子节点,不需要自上而下式堆化。
最后一个子节点的父节点为
n/2
,所以从n/2
位置节点开始堆化:代码实现
测试成功
五、排序算法:堆排序
1. 原理
堆是一棵完全二叉树,它可以使用数组存储,并且大顶堆的最大值存储在根节点(i=1),所以我们可以每次取大顶堆的根结点与堆的最后一个节点交换,此时最大值放入了有效序列的最后一位,并且有效序列减1,有效堆依然保持完全二叉树的结构,然后堆化,成为新的大顶堆,重复此操作,知道有效堆的长度为 0,排序完成。
完整步骤为:
2. 动图演示
3. 代码实现
测试成功
4. 复杂度分析
**时间复杂度:**建堆过程的时间复杂度是
O(n)
,排序过程的时间复杂度是O(nlogn)
,整体时间复杂度是O(nlogn)
空间复杂度:
O(1)
六、内存堆与垃圾回收
前端面试高频考察点,瓶子君已经在 栈 章节中介绍过,点击前往 前端进阶算法5:全方位解读前端用到的栈结构(+leetcode刷题)
七、堆的经典应用: Top K 问题(常见于腾讯、字节等面试中)
这种问题我们该怎么处理喃?我们以从数组中取前 K 大的数据为例,可以按以下步骤来:
K
个数,构造一个小顶堆K+1
位开始遍历数组,每一个数据都和小顶堆的堆顶元素进行比较,如果小于堆顶元素,则不做任何处理,继续遍历下一元素;如果大于堆顶元素,则将这个元素替换掉堆顶元素,然后再堆化成一个小顶堆。遍历数组需要 O(N) 的时间复杂度,一次堆化需要 O(logK) 时间复杂度,所以利用堆求 Top K 问题的时间复杂度为 O(NlogK)。
利用堆求 Top K 问题的优势
也许很多人会认为,这种求 Top K 问题可以使用排序呀,没必要使用堆呀
其实是可以使用排序来做的,将数组进行排序(可以是最简单的快排),去前 K 个数就可以了,so easy
但当我们需要在一个动态数组中求 Top K 元素怎么办喃,动态数组可能会插入或删除元素,难道我们每次求 Top K 问题的时候都需要对数组进行重新排序吗?那每次的时间复杂度都为 O(NlogN)
这里就可以使用堆,我们可以维护一个 K 大小的小顶堆,当有数据被添加到数组中时,就将它与堆顶元素比较,如果比堆顶元素大,则将这个元素替换掉堆顶元素,然后再堆化成一个小顶堆;如果比堆顶元素小,则不做处理。这样,每次求 Top K 问题的时间复杂度仅为 O(logK)
八、堆的经典应用:中位数问题
除了 Top K 问题,堆还有一个经典的应用场景就是求中位数问题
如何利用堆来求解中位数问题喃?
这里需要维护两个堆:
Math.floor(n/2) + 1
个元素那么,中位数就为:
当数组为动态数组时,每当数组中插入一个元素时,都需要如何调整堆喃?
如果插入元素比大顶堆的堆顶要大,则将该元素插入到小顶堆中;如果要小,则插入到大顶堆中。
当出入完后后,如果大顶堆、小顶堆中元素的个数不满足我们已上的要求,我们就需要不断的将大顶堆的堆顶元素或小顶堆的堆顶元素移动到另一个堆中,直到满足要求
由于插入元素到堆、移动堆顶元素都需要堆化,所以,插入的时间复杂度为 O(logN) ,每次插入完成后求中位数仅仅需要返回堆顶元素即可,时间复杂度为 O(1)
中位数的变形:TP 99 问题
所以,针对 TP99 问题,我们同样也可以维护两个堆,一个大顶堆,一个小顶堆。大顶堆中保存前
99%
个数据,小顶堆中保存后1%
个数据。大顶堆堆顶的数据就是我们要找的 99% 响应时间。本小节参考极客时间的:数据结构与算法之美
九、总结
堆是一个完全二叉树,并且堆上的任意节点值都必须大于等于(大顶堆)或小于等于(小顶堆)其左右子节点值,推可以采用数组存储法存储,可以通过插入式建堆或原地建堆,堆的重要应用有:
JavaScript 的存储机制分为代码空间、栈空间以及堆空间,代码空间用于存放可执行代码,栈空间用于存放基本类型数据和引用类型地址,堆空间用于存放引用类型数据,当调用栈中执行完成一个执行上下文时,需要进行垃圾回收该上下文以及相关数据空间,存放在栈空间上的数据通过 ESP 指针来回收,存放在堆空间的数据通过副垃圾回收器(新生代)与主垃圾回收器(老生代)来回收。
十、leetcode刷题:最小的k个数
话不多说,来一道题目加深一下理解吧:
输入整数数组
arr
,找出其中最小的k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。示例 1:
示例 2:
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
题目详情已提交到 #59 ,欢迎解答,欢迎star
The text was updated successfully, but these errors were encountered: