面试官:说说你对堆的理解?如何实现?应用场景?

开发 测试
堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是Olog(n),插入数据和删除堆顶元素的主要逻辑就是堆化。

[[426251]]

本文转载自微信公众号「JS每日一题」,作者灰灰。转载本文请联系JS每日一题公众号。

一、是什么

堆(Heap)是计算机科学中一类特殊的数据结构的统称

堆通常是一个可以被看做一棵完全二叉树的数组对象,如下图:

总是满足下列性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值
  • 堆总是一棵完全二叉树

堆又可以分成最大堆和最小堆:

  • 最大堆:每个根结点,都有根结点的值大于两个孩子结点的值
  • 最小堆:每个根结点,都有根结点的值小于孩子结点的值

二、操作

堆的元素存储方式,按照完全二叉树的顺序存储方式存储在一个一维数组中,如下图:

用一维数组存储则如下:

  1. [0, 1, 2, 3, 4, 5, 6, 7, 8] 

根据完全二叉树的特性,可以得到如下特性:

  • 数组零坐标代码的是堆顶元素
  • 一个节点的父亲节点的坐标等于其坐标除以2整数部分
  • 一个节点的左节点等于其本身节点坐标 * 2 + 1
  • 一个节点的右节点等于其本身节点坐标 * 2 + 2

根据上述堆的特性,下面构建最小堆的构造函数和对应的属性方法:

  1. class MinHeap { 
  2.   constructor() { 
  3.     // 存储堆元素 
  4.     this.heap = [] 
  5.   } 
  6.   // 获取父元素坐标 
  7.   getParentIndex(i) { 
  8.     return (i - 1) >> 1 
  9.   } 
  10.    
  11.   // 获取左节点元素坐标 
  12.   getLeftIndex(i) { 
  13.     return i * 2 + 1 
  14.   } 
  15.    
  16.  // 获取右节点元素坐标 
  17.   getRightIndex(i) { 
  18.     return i * 2 + 2 
  19.   } 
  20.    
  21.   // 交换元素 
  22.   swap(i1, i2) { 
  23.     const temp = this.heap[i1] 
  24.     this.heap[i1] = this.heap[i2] 
  25.     this.heap[i2] = temp 
  26.   } 
  27.    
  28.   // 查看堆顶元素 
  29.   peek() { 
  30.     return this.heap[0] 
  31.   } 
  32.    
  33.   // 获取堆元素的大小 
  34.   size() { 
  35.     return this.heap.length 
  36.   } 

涉及到堆的操作有:

  • 插入
  • 删除

插入

将值插入堆的底部,即数组的尾部,当插入一个新的元素之后,堆的结构就会被破坏,因此需要堆中一个元素做上移操作

将这个值和它父节点进行交换,直到父节点小于等于这个插入的值,大小为k的堆中插入元素的时间复杂度为O(logk)

如下图所示,22节点是新插入的元素,然后进行上移操作:

相关代码如下:

  1. // 插入元素 
  2. insert(value) { 
  3.   this.heap.push(value) 
  4.   this.shifUp(this.heap.length - 1) 
  5.  
  6. // 上移操作 
  7. shiftUp(index) { 
  8.   if (index === 0) { return } 
  9.   const parentIndex = this.getParentIndex(index
  10.   if(this.heap[parentIndex] > this.heap[index]){ 
  11.     this.swap(parentIndex, index
  12.     this.shiftUp(parentIndex) 
  13.   } 

删除

常见操作是用数组尾部元素替换堆顶,这里不直接删除堆顶,因为所有的元素会向前移动一位,会破坏了堆的结构

然后进行下移操作,将新的堆顶和它的子节点进行交换,直到子节点大于等于这个新的堆顶,删除堆顶的时间复杂度为O(logk)

整体如下图操作:

相关代码如下:

  1. // 删除元素 
  2. pop() { 
  3.   this.heap[0] = this.heap.pop() 
  4.   this.shiftDown(0) 
  5.  
  6. // 下移操作 
  7. shiftDown(index) { 
  8.   const leftIndex = this.getLeftIndex(index
  9.   const rightIndex = this.getRightIndex(index
  10.   if (this.heap[leftIndex] < this.heap[index]){ 
  11.     this.swap(leftIndex, index
  12.     this.shiftDown(leftIndex) 
  13.   } 
  14.   if (this.heap[rightIndex] < this.heap[index]){ 
  15.     this.swap(rightIndex, index
  16.     this.shiftDown(rightIndex) 
  17.   } 

时间复杂度

关于堆的插入和删除时间复杂度都是Olog(n),原因在于包含n个节点的完全二叉树,树的高度不会超过log2n

堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是Olog(n),插入数据和删除堆顶元素的主要逻辑就是堆化

三、总结

堆是一个完全二叉树

堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值

对于每个节点的值都大于等于子树中每个节点值的堆,叫作“大顶堆”

对于每个节点的值都小于等于子树中每个节点值的堆,叫作“小顶堆”

根据堆的特性,我们可以使用堆来进行排序操作,也可以使用其来求第几大或者第几小的值

参考文献

https://baike.baidu.com/item/%E5%A0%86/20606834

 

https://xlbpowder.cn/2021/02/26/%E5%A0%86%E5%92%8C%E5%A0%86%E6%8E%92%E5%BA%8F/

 

责任编辑:武晓燕 来源: JS每日一题
相关推荐

2021-09-29 07:24:20

场景数据

2021-09-16 07:52:18

算法应用场景

2021-10-09 10:25:41

排序应用场景

2021-10-08 09:59:32

冒泡排序场景

2021-10-13 18:01:33

快速排序场景

2021-10-11 09:38:41

开源

2021-10-12 07:15:02

归并排序场景

2021-11-10 07:47:49

组合模式场景

2021-08-16 08:33:26

git

2021-11-03 14:10:28

工厂模式场景

2021-11-09 08:51:13

模式命令面试

2021-11-05 07:47:56

代理模式对象

2021-10-14 07:55:20

二分查找面试

2021-09-06 10:51:27

TypeScriptJavaScript

2021-11-11 16:37:05

模板模式方法

2021-11-22 23:50:59

责任链模式场景

2021-09-08 07:49:34

TypeScript 泛型场景

2021-09-10 06:50:03

TypeScript装饰器应用

2021-11-04 06:58:32

策略模式面试

2021-05-31 10:35:34

TCPWebSocket协议
点赞
收藏

51CTO技术栈公众号