一、概念

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

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

 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. python读取excel数据为json格式(兼容xls\xlsx)

    做自动化时需要从excel读取数据: 本文实现将excel文件数据读取为json格式,方便自动化调用 读取xls文件 使用xlrd读取xls文件代码: import xlrd def read_xls ...

  2. spring boot 通过feign调用api接口

    目的:远程调用服务器api,直接上步骤: 1,添加maven依赖,这是必须的: <dependency> <groupId>org.springframework.cloud& ...

  3. 概A第二章测试

    以下判断题全是(√) 问题 1 得 10 分,满分 10 分                     问题 2 得 10 分,满分 10 分       0-1分布相当于一个特殊的二项分布b(1,p) ...

  4. 908. Smallest Range I

    Given an array A of integers, for each integer A[i] we may choose any x with -K <= x <= K, and ...

  5. 2-7 Java基础数据类型之字符型

    代码中输入如下部分: /* char的取值范围0-65535 */ public class DataType06 { public static void main(String[]args){ c ...

  6. 让vim显示空格,tab字符,及vim多行注释

    1.显示 TAB 键 文件中有 TAB 键的时候,你是看不见的.要把它显示出来: :set list 现在 TAB 键显示为 ^I,而 $显示在每行的结尾,以便你能找到可能会被你忽略的空白字符在哪里 ...

  7. Python socket(TCP阻塞模式)基础程式

    前置知识:Python基础语法,socket库 tips: 1. 默认HOST_IP:127.0.0.1 2. 默认HOST_PORT:7676 参考代码: 1. 客户端程式 #!/usr/bin/e ...

  8. hdu3987 最小割边数

    题意:      是让你求最小割之后问最小割的最少边数是多少,因为最小割不是唯一的,所以存在最小边数的问法.思路:      两个方法,一个是先一遍最大流,然后把割边全都改成流量1,其他的全都改成流量 ...

  9. hdu5012 水搜索

    题意:       给你一个正方体的初始状态和末状态,问你是否可以再6步之内转到这个状态,有四种转的方式,如果你面对的是正方向的正前方,那么转的方式就是 顺时针,逆时针,上,下. 思路:        ...

  10. Andrew Ng机器学习算法入门(四):阶梯下降算法

    梯度降级算法简介 之前如果需要求出最佳的线性回归模型,就需要求出代价函数的最小值.在上一篇文章中,求解的问题比较简单,只有一个简单的参数.梯度降级算法就可以用来求出代价函数最小值. 梯度降级算法的在维 ...