一、概念

 说起堆,我们就想起了土堆,把土堆起来,当我们要用土的时候,首先用到最上面的土。类似地,堆其实是一种优先队列,按照某种优先级将数字“堆”起来,每次取得时候从堆顶取。

 堆是一颗完全二叉树,其特点有如下几点:

 1.可以使用一维数组来表示。其中,第i个节点的父节点、子节点index如下:

  - parent(i) = (i - 1) / 2; (取整)

  - leftChild(i) = i * 2 + 1;

  - rightChild(i) = i * 2 + 2;

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

 注1:为什么可以用一维数组来表示堆,因为堆是一颗完全二叉树,可以通过下标计算获得其父子节点的下标

 注2:除了根节点外,其它节点的排序是未知的。(也就是说我们只知道最大or最小值是根节点的值,不知道其它节点的顺序)

二、上浮(shiftUp)和下沉(shiftDown)

1. 上浮(shiftUp)(以构建最小堆为例)

上浮操作就是,如果一个节点比它父节点小,则将它与父节点交换位置。这个节点在数组中的位置上升。

2. 下沉(shiftDown)

下沉操作就是,如果一个节点比它子节点大,那么它需要向下移动。称之为“下沉”。

三、堆的构建

给定一个一维数组[4,1,3,2,16,9,10,14,8,7],怎么把它构建成一个堆呢?使用自底向上的方法将数组转化为堆。

大体思路为:如果每一个节点都是一个堆(以这个节点为根),那么这个数组肯定是一个堆。自底向上的方法意思就是,自底向上依次调整每个节点,使得以该节点为根的树是一个堆。

(以最大堆为例)

  1. 首先将数组还原成一个完全二叉树

  2. 从倒数第一个非叶子节点开始(i = (n/2)-1,其中,n是数组的长度),依次对每个节点执行步骤3,将其调整成最大堆,直至第0个节点。

    注:这里要说一下为啥从第一个非叶子节点开始,因为叶节点没有孩子节点,可以把它看成只有一个元素的堆。
  3. 将以第i个节点为根节点的子树调整为最大堆。假设根节点A[i]的左右子树的根节点index分别为left[i]和right[i],其中,left[i]和right[i]都是最大堆。采用逐级下降的方法进行调整,具体步骤如下:

    (1) 从A[i]、A[left[i]]、A[right[i]]中找到最大的值,将其下标存到largest中。如果A[i]是最大的,那么以i为根节点的子树是最大堆;否则,交换A[i]和A[largest]的值。

    (2) 对下标为largest为根的子树重复(1)操作,直至largest为叶子节点。

时间复杂度:O(n)

 (此推到过程以后有时间了再说,可以先记一下)

如图所示:

  • 从 i = 4 开始遍历,此时,A[4] = 16,它是一个最大堆;
  • i = 3,A[3] = 2,与A[7]交换,因为i = 7是叶子节点,所以调整完毕。
  • i = 2,A[2] = 3,与A[6]交换,因为i = 6是叶子节点,所以调整完毕。此时,该树变成:

  • i = 1,A[1] = 1,与A[4]交换。因为i = 4不是叶子节点,所以要对以4为根的子树进行上述操作;此时,A[4] = 1,与A[9]交换,i = 9是叶子节点,调整完毕。如下图所示:

  • i = 0,A[0] = 4,与A[1]交换。因为i = 1不是叶子节点,对以1为根的子树进行上述操作;此时,A[1] = 4,与A[3]交换,i = 3不是叶子节点,对以3为根的子树重复进行操作;此时A[3] = 4,与A[8]交换,i = 8是叶子节点,调整完毕。最终得到最大堆:

四、堆的其它方法

1.插入

向数组最后插入一个数据,然后再向上调整。还以上述数组为例,插入一个11。

  • 在数组最后插入11

  • 比较该节点与其父节点的大小,11 > 7,交换两者,进行上浮。重复该步骤,11 < 14,此时满足最大堆性质,插入完毕。

时间复杂度:O(logn)

2.删除(指删除堆顶的数据)

交换堆顶和最后一个数据,删除最后一个数据,再对堆顶数据进行向下调整算法。

  • 交换堆顶和最后一个数据,并删除最后一个数据。

  • 堆根节点进行下沉操作:对比该节点和其左右子节点,将该节点与最大的节点进行交换。重复此操作,直至该节点为叶子节点。(因为这个节点原本就是叶子节点交换上去的,比上面所有层的节点都小,所以调整完毕后,这个节点一定仍是叶子节点)
    • 1与14交换
    • 1与8交换
    • 1与4交换

时间复杂度:O(logn)

五、堆排序

基本思路:

  • 将输入的数组建成最大堆。此时,堆顶元素就是最大值。
  • 交换堆顶元素和末尾元素。此时,末尾元素是最大值。
  • 将剩下 n-1 个元素重新构造成一个堆。重复执行上述步骤。

举个简单的例子:[14, 8, 10, 4]

  1. 将该数组构建成最大堆

  2. 交换堆顶元素和末尾元素。

  3. 忽略末尾元素,将剩下的元素利用下沉法重新构造为一个最大堆。

  4. 重复以上步骤,直至剩下的元素只剩下一个。最终得到如下结果,排序完毕:

时间复杂度:O(nlogn)

六、堆的应用

1. 优先队列(先填个坑,回头补上)

2. 求Top K

不管是面试还是啥,每次问到求Top K的问题,我们第一反应就是利用堆,但是怎么用呢?

Top K问题可抽象成两类,一类是针对静态数据集合,即数据是事先确定的;一类是针对动态数据集合,即数据动态地加入到集合中。

针对静态集合

维护一个大小为K的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。如果比堆顶元素大,则把堆顶元素删除,并把该元素插入堆中;反之则不做处理,继续遍历数组。数组遍历完毕后,堆中的数据就是前K大数据了。

时间复杂度:O(nlogK)

针对动态集合

其实思路与静态集合大体一致,只不过静态集合是遍历数组,将数组中的元素与堆顶比较然后然后进行一系列操作;而动态集合是每加入一个元素,将该元素与堆顶比较,然后进行操作。

时间复杂度:O(logK)

七、JavaScript实现堆类

js中并没有“堆”这个数据结构,只能手动实现,以下是源代码。

class MaxHeap {
constructor(heap) {
this.heap = heap;
this.heapSize = heap.length;
this.buildMaxHeap();
} // 构建最大堆
buildMaxHeap() {
for (let i = Math.floor(this.heapSize / 2) - 1; i >= 0; i--) {
this.maxHeapify(i);
}
} //将以i为根节点的子树调整为最大堆
maxHeapify(index) {
let left = 2 * index + 1;
let right = 2 * index + 2;
let largest = index;
if (left < this.heapSize && this.heap[left] > this.heap[largest]) largest = left;
if (right < this.heapSize && this.heap[right] > this.heap[largest]) largest = right;
if (largest !== index) {
this.swapNum(index, largest);
this.maxHeapify(largest);
}
} //交换i,j的值
swapNum(i, j) {
let temp = this.heap[i];
this.heap[i] = this.heap[j];
this.heap[j] = temp;
} //插入一个数
insert(num) {
this.heap.push(num);
this.heap.heapSize = this.heap.length;
let index = this.heap.heapSize - 1;
while (index != -1) {
index = this.shiftUp(index);
}
console.log(this.heap);
} //删除堆顶元素
pop() {
this.swapNum(0, this.heapSize - 1);
this.heap.pop();
this.heapSize = this.heap.length;
let index = 0;
while (1) {
let temp = this.shiftDown(index);
if (temp === index) break;
else index = temp;
}
} //堆排序
heapSort() {
while (this.heapSize > 1) {
this.swapNum(0, this.heapSize - 1);
this.heapSize -= 1;
let index = 0;
while (1) {
let temp = this.shiftDown(index);
if (temp === index) break;
else index = temp;
}
}
this.heapSize = this.heap.length;
} //上浮操作 - 将当前节点与父节点进行比较,如果该节点值大于父节点值,则进行交换。
shiftUp(index) {
let parent = Math.ceil(index / 2) - 1;
if (this.heap[index] > this.heap[parent] && parent >= 0) {
this.swapNum(index, parent);
return parent;
}
return -1;
} // 下沉操作 - 将当前节点与左右子节点进行比较,如果该节点值不是最大,则进行交换
shiftDown(index) {
let left = Math.floor(index * 2) + 1;
let right = left + 1;
let largest = index;
if (left < this.heapSize && this.heap[left] > this.heap[largest]) largest = left;
if (right < this.heapSize && this.heap[right] > this.heap[largest]) largest = right;
if (largest !== index) {
this.swapNum(index, largest);
}
return largest;
} }

八、参考文章:

  1. 《算法导论第3版》
  2. https://blog.csdn.net/qq_41754573/article/details/103371579
  3. https://www.jianshu.com/p/6b526aa481b1

五分钟带你读懂 堆 —— heap(内含JavaScript代码实现!!)的更多相关文章

  1. 五分钟带你读懂 TCP全连接队列(图文并茂)

    爱生活,爱编码,微信搜一搜[架构技术专栏]关注这个喜欢分享的地方. 本文 架构技术专栏 已收录,有各种视频.资料以及技术文章. 一.问题 今天有个小伙伴跑过来告诉我有个奇怪的问题需要协助下,问题确实也 ...

  2. 少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别

    1.引言 很多初涉网络编程的程序员,在研究Java NIO(即异步IO)和经典IO(也就是常说的阻塞式IO)的API时,很快就会发现一个问题:我什么时候应该使用经典IO,什么时候应该使用NIO? 在本 ...

  3. 五分钟让你读懂UML常见类图

    相信各位同学在阅读一些源码分析类文章或是设计应用架构时没少与UML类图打交道.实际上,UML类图中最常用到的元素五分钟就能掌握,经常看到UML类图但还不太熟悉的小伙伴赶紧来一起认识一下它吧:)   一 ...

  4. Python——五分钟带你弄懂迭代器与生成器,夯实代码能力

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是周一Python专题,给大家带来的是Python当中生成器和迭代器的使用. 我当初第一次学到迭代器和生成器的时候,并没有太在意,只是觉 ...

  5. 黑马程序员:3分钟带你读懂C/C++学习路线

    随着互联网及互联网+深入蓬勃的发展,经过40余年的时间洗礼,C/C++俨然已成为一门贵族语言,出色的性能使之成为高级语言中的性能王者.而在今天,它又扮演着什么样重要的角色呢?请往下看: 后端服务器,移 ...

  6. 十分钟带你读懂《增长黑客》zz

    背景 “If you are not growing, then you are dying. ”(如果企业不在增长,那么就是在衰亡!) 这句话适用于企业,也适用于个人.人生毕竟不像企业,是非成败,似 ...

  7. Python专题——五分钟带你了解map、reduce和filter

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python专题第6篇文章,给大家介绍的是Python当中三个非常神奇的方法:map.reduce和filter. 不知道大家看到ma ...

  8. 实战 | 一文带你读懂Nginx反向代理

    一个执着于技术的公众号 前言 在前面的章节中,我们已经学习了nginx基础知识: 给小白的 Nginx 10分钟入门指南 Nginx编译安装及常用命令 完全卸载nginx的详细步骤 Nginx 配置文 ...

  9. 一文带你读懂什么是vxlan网络

    一个执着于技术的公众号 一.背景 随着云计算.虚拟化相关技术的发展,传统网络无法满足大规模.灵活性要求高的云数据中心的要求,于是便有了overlay网络的概念.overlay网络中被广泛应用的就是vx ...

随机推荐

  1. 代码安全丨第二期:URL重定向(跳转)漏洞

    URL重定向: URL重定向(URLredirection)漏洞,又称跳转漏洞,指的是网络应用程序接受用户可控的输入作为到外部站点的链接,然后在重定向中使用该链接.该安全漏洞给网络钓鱼攻击提供了极大的 ...

  2. 【luogu P3803】【模板】多项式乘法(FFT)

    [模板]多项式乘法(FFT) 题目链接:luogu P3803 题目大意 给你两个多项式,要你求这两个多项式乘起来得到的多项式.(卷积) 思路 系数表示法 就是我们一般来表示一个多项式的方法: \(A ...

  3. 1.4.15- HTML标签之链接标签

    代码如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...

  4. 【ShardingSphere】ShardingSphere学习(三)-数据分片-分片

    分片键 分片算法 分片策略 SQL Hint 分片键 用于分片的数据库字段,是将数据库(表)水平拆分的关键字段.例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段. SQL中如果无分片字段, ...

  5. 【VsCode】 实用插件列表

    1.IntelliJ IDEA Keybindings   -IDEA风格的 快捷键,熟悉JetBrain全家桶的不要错过: 2.Vue VSCode Snippets    -写Vue时好用的快捷键 ...

  6. hdu4932 小贪心

    题意:      给了一些处在x轴上的点,要求我们用长度相等的线段覆盖所有点,线段和线段之间不能重叠,问线段最长可以使多长. 思路:       一开始一直在想二分,哎!感觉这个题目很容易就往二分上去 ...

  7. dependencyManagement 版本管理

    帮别人解决bug的时候碰到,随便记录一下. 使用<dependencyManagement>标签, 做版本管理的时候,需要父子项目关联,就需要子模块中利用<parent> 否则 ...

  8. windows的SEH异常处理以及顶层异常处理

    前言 windows的SEH结构化异常处理是基于线程的,传统的SEH结构化异常会基于堆栈形成一条包含异常回调函数地址的链(SEH链).而fs:[0](TEB的第一个字段)指向这条链的链头,当有异常发生 ...

  9. Zabbix 5.0 优化建议

    Blog:博客园 个人 在使用Zabbix过程中,正确的调整Zabbix系统,使之保持高性能是非常重要的,能够充分利用硬件资源,监控更多主机和性能指标. 硬件 关于zabbix server端硬件的建 ...

  10. 【BUAA_2020_软工】个人作业

    个人项目作业博客 1. 在文章开头给出教学班级和可克隆的 Github 项目地址(例子如下).(1') 项目 内容 北航2020软工 班级博客 作业要求 具体要求 项目GitHub地址 个人项目 教学 ...