热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JS数据结构与算法之《堆》

概念堆的底层实际上是一棵完全二叉树,可以用数组实现。二叉树的一种,满足以下条件:任意节点大于或小于它的所有子节点(大根堆、

概念

堆的底层实际上是一棵完全二叉树,可以用数组实现。

二叉树的一种,满足以下条件:


  1. 任意节点大于或小于它的所有子节点(大根堆、小根堆)

  2. 总是一完全树,即除了最底层,其它层的节点都被元素填满

将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆

 

JS数据结构与算法之《堆》 - 文章图片

 

将数组第一个元素置空,为了方便计算。这样我们就可以从下标1开始,下标变量为i,那么:


  • 左子节点位置是 2*i

  • 右子节点位置是 2*i+1

  • 父节点位置是 Math.floor(i/2)

堆(以大顶堆为例)相关的操作主要有:


  1. 大顶堆调整(Max-Heapify),将堆的末端子节点做调整,使得子节点永远小于父节点;

  2. 创建大顶堆(Build-Max-Heap),将堆中所有数据调整位置,使其成为大顶堆;

  3. 堆排序(Heap-Sort),移除在堆顶的根节点,并做大顶堆调整的迭代运算。


大顶堆调整

/**
* 从index开始检查并保持大顶堆
* @param arr 待检查数组
* @param i 检查的起始下标
* @param size 堆大小
*/
function maxHeapify(arr, i, size) {
let left = 2 * i
let right = left + 1
//左右孩子中较大的一个
let maxlr = -1
//无左右孩子节点
if (left > size && right > size) {
return
}
//只有左孩子节点
if (left <= size && right > size) {
maxlr = left
}
//只有右孩子节点
if (right <= size && left > size) {
maxlr = right
}
//同时有左右孩子节点
if (left <= size && right <= size) {
maxlr = arr[left] }
if (arr[i] swap(arr, i, maxlr)
maxHeapify(arr, maxlr, size)
}
}
function swap(arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}

非递归写法:

/**
* 从i开始检查并保持大顶堆
* @param arr 待排数组
* @param i 检查的起始下标
* @param size 堆大小
*/
function maxHeapify2(arr, i, size) {
let left, right, maxlr = -1
while (i left = 2 * i
right = left + 1
//无左右孩子节点
if (left > size && right > size) {
break
}
//只有左孩子节点
if (left <= size && right > size) {
maxlr = left
}
//只有右孩子节点
if (right <= size && left > size) {
maxlr = right
}
//同时有左右孩子节点
if (left <= size && right <= size) {
maxlr = arr[left] }
if (arr[i] swap(arr, maxlr, i)
i = maxlr
}
}
}
function swap(arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}

创建大顶堆

创建大顶堆(Build-Max-Heap)的作用是,将一个数组改造成一个大顶堆,会自下而上地调用 Max-Heapify 来改造数组。

function buildMaxHeap(arr) {
if (!Array.isArray(arr)) return []
//将null插到数组第一个位置上
arr.unshift(null)
let lastParentIndex = Math.floor((arr.length - 1) / 2)
for (let i = lastParentIndex; i > 0; i--) {
maxHeapify(arr, i, arr.length - 1)
}
arr.shift()
}

堆排序

堆排序(Heap-Sort)是堆排序的接口算法,其先要调用创建大顶堆(Build-Max-Heap)将数组改造为大顶堆; 然后进入迭代,迭代中先将堆顶与堆底元素交换,并将堆长度缩短,继而重新调用大顶堆调整(Max-Heapify)保持大顶堆性质。

因为堆顶元素必然是堆中最大的元素,所以每一次操作之后,堆中存在的最大元素会被分离出堆,重复 n-1 次,数组排序完成。

function heapSort(arr) {
arr[0] !== null && arr.unshift(null)
for (let j = arr.length - 1; j > 0; j--) {
//将堆顶与堆底元素交换,分离出最大的元素
swap(arr, 1, j)
//重新调整大顶堆
maxHeapify(arr, 1, j - 1)
}
arr.shift()
}

我们来测试一下大顶堆的构建与排序:

var arr = [5, 2, 8, 3, 1, 6, 9]
buildMaxHeap(arr)
console.log(arr)
heapSort(arr)
console.log(arr)
//[9,3,8,2,1,6,5]
//[1,2,3,5,6,8,9]

复杂度

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。

其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。

所以堆排序时间复杂度一般认为就是O(nlogn)级。

添加元素

将元素添加到数组末尾,然后进行大顶堆调整

function maxHeapPush(arr = [], elem) {
arr.push(elem)
arr[0] !== null && arr.unshift(null)
let lastParentIndex = Math.floor((arr.length - 1) / 2)
for (let i = lastParentIndex; i > 0; i--) {
maxHeapify(arr, i, arr.length - 1)
}
arr.shift()
}

弹出元素

每次只能弹出最值,即根节点,如果把根元素直接删除的话, 整个堆就毁了, 所以我们思考着使用内部的某一个元素先顶替根节点的位置,这个元素显而易见的是最后一个元素,因为最后一个元素的移动不会使得树的结构改变。 交换后,进行大顶堆调整。

function maxHeapPop(arr = []) {
arr[0] !== null && arr.unshift(null)
swap(arr, 1, arr.length - 1)
const top = arr.pop()
let lastParentIndex = Math.floor((arr.length - 1) / 2)
for (let i = lastParentIndex; i > 0; i--) {
maxHeapify(arr, i, arr.length - 1)
}
arr.shift()
return top
}

测试一下:

var arr = [5, 2, 8, 3, 1, 6, 9]
buildMaxHeap(arr)
console.log(arr)
maxHeapPush(arr, 10)
console.log(arr)
maxHeapPop(arr)
console.log(arr)
//[10, 9, 8, 3, 1, 6, 5, 2]
//[9, 3, 8, 2, 1, 6, 5]

封装一下,完整的代码如下:

function Heap(type = 'max') {
this.type = type
this.arr = []
}
Heap.prototype.build = function() {
this.arr.unshift(null)
let lastParentIndex = Math.floor((this.arr.length - 1) / 2)
for (let i = lastParentIndex; i > 0; i--) {
this.heapify(i, this.arr.length - 1)
}
this.arr.shift()
}
Heap.prototype.heapify = function(i, size) {
let left = 2 * i
let right = left + 1
//左右孩子中较大或较小的一个
let lr = -1
//无左右孩子节点
if (left > size && right > size) {
return
}
//只有左孩子节点
if (left <= size && right > size) {
lr = left
}
//只有右孩子节点
if (right <= size && left > size) {
lr = right
}
//同时有左右孩子节点
if (left <= size && right <= size) {
lr = this.type === 'max' ? (this.arr[left] this.arr[right] ? right : left)
}
if ((this.type === 'max' && this.arr[i] this.arr[lr])) {
this.swap(i, lr)
this.heapify(lr, size)
}
}
Heap.prototype.sort = function() {
this.arr[0] !== null && this.arr.unshift(null)
for (let j = this.arr.length - 1; j > 0; j--) {
this.swap(1, j)
this.heapify(1, j - 1)
}
this.arr.shift()
}
Heap.prototype.add = function(elem) {
this.arr.push(elem)
this.arr[0] !== null && this.arr.unshift(null)
let lastParentIndex = Math.floor((this.arr.length - 1) / 2)
for (let i = lastParentIndex; i > 0; i--) {
this.heapify(i, this.arr.length - 1)
}
this.arr.shift()
}
Heap.prototype.pop = function() {
this.arr[0] !== null && this.arr.unshift(null)
this.swap(1, this.arr.length - 1)
const top = this.arr.pop()
let lastParentIndex = Math.floor((this.arr.length - 1) / 2)
for (let i = lastParentIndex; i > 0; i--) {
this.heapify(i, this.arr.length - 1)
}
this.arr.shift()
return top
}
Heap.prototype.swap = function(i, j) {
let temp = this.arr[i]
this.arr[i] = this.arr[j]
this.arr[j] = temp
}
var heap = new Heap()
heap.add(5)
heap.add(2)
heap.add(8)
heap.add(3)
heap.add(1)
heap.add(6)
heap.add(9)
console.log(heap.arr)
//heap.build()
//console.log(heap.arr)
heap.add(10)
console.log(heap.arr)
heap.pop()
console.log(heap.arr)
heap.sort()
console.log(heap.arr)

数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。

如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

步骤:


  1. 维护一个大顶堆,一个小顶堆,数据总数:



  • 小顶堆里的值全大于大顶堆里的;

  • 2个堆个数的差值小于等于1



  1. 当插入数字后数据总数为奇数时:使小顶堆个数比大顶堆多1;当插入数字后数据总数为偶数时,使大顶堆个数跟小顶堆个数一样。

  2. 当总数字个数为奇数时,中位数就是小顶堆堆头;当总数字个数为偶数时,中位数数就是2个堆堆顶平均数。

const maxHeap = new Heap('max');
const minHeap = new Heap('min');
let count = 0;
function Insert(num) {
count++;
if (count % 2 === 1) {
maxHeap.add(num);
minHeap.add(maxHeap.pop());
} else {
minHeap.add(num);
maxHeap.add(minHeap.pop());
}
}
function GetMedian() {
if (count % 2 === 1) {
return minHeap.value[0];
} else {
return (minHeap.value[0] + maxHeap.value[0]) / 2
}
}

最小的k个数

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。

步骤:


  1. 把前k个数构建一个大顶堆

  2. 从第k个数开始,和大顶堆的最大值进行比较,若比最大值小,交换两个数的位置,重新构建大顶堆

  3. 一次遍历之后大顶堆里的数就是整个数据里最小的k个数

function getLeastNumbers(arr, k) {
if (k > arr.length) {
return []
}
arr.unshift(null)
buildMaxHeap(arr, k + 1)
for (let i = k + 1; i if (arr[i] [arr[i], arr[1]] = [arr[1], arr[i]]
let lastParentIndex = Math.floor(k / 2)
for (let j = lastParentIndex; j > 0; j--) {
maxHeapify(arr, j, k)
}
}
}
return arr.slice(1, k + 1)
}
function buildMaxHeap(arr, size) {
let lastParentIndex = Math.floor(size / 2)
for (let i = lastParentIndex; i > 0; i--) {
maxHeapify(arr, i, size)
}
}
function maxHeapify(arr, i, size) {
let left = 2 * i
let right = left + 1
//左右孩子中较大的一个
let maxlr = -1
//无左右孩子节点
if (left > size && right > size) {
return
}
//只有左孩子节点
if (left <= size && right > size) {
maxlr = left
}
//只有右孩子节点
if (right <= size && left > size) {
maxlr = right
}
//同时有左右孩子节点
if (left <= size && right <= size) {
maxlr = arr[left] }
if (arr[i] [arr[i], arr[maxlr]] = [arr[maxlr], arr[i]]
maxHeapify(arr, maxlr, size)
}
}
var arr = [4, 5, 1, 6, 2, 7, 3, 8]
var result = getLeastNumbers(arr, 4)
console.log(result)
//[4,3,1,2]


推荐阅读
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 在编写业务代码时,常常会遇到复杂的业务逻辑导致代码冗长混乱的情况。为了解决这个问题,可以利用中间件模式来简化代码逻辑。中间件模式可以帮助我们更好地设计架构和代码,提高代码质量。本文介绍了中间件模式的基本概念和用法。 ... [详细]
  • 本文介绍了在wepy中运用小顺序页面受权的计划,包含了用户点击作废后的从新受权计划。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • 本文总结了在开发中使用gulp时的一些技巧,包括如何使用gulp.dest自动创建目录、如何使用gulp.src复制具名路径的文件以及保留文件夹路径的方法等。同时介绍了使用base选项和通配符来保留文件夹路径的技巧,并提到了解决带文件夹的复制问题的方法,即使用gulp-flatten插件。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • Day2列表、字典、集合操作详解
    本文详细介绍了列表、字典、集合的操作方法,包括定义列表、访问列表元素、字符串操作、字典操作、集合操作、文件操作、字符编码与转码等内容。内容详实,适合初学者参考。 ... [详细]
author-avatar
bj韩式尕伙
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有