算法|最大堆、最小堆和堆排序的实现(JavaScript)

一些概念

  • 堆:特殊的完全二叉树,具有特定性质的完全二叉树。
  • 大根堆:父节点 > 子节点
  • 小根堆:父节点 < 子节点

二叉堆也属于完全二叉树,所以可以用数组表示。

  • 若下标从1开始,左节点为 2*i ,右节点为 2*i+1 ,父节点为 i//2
  • 若下标从1开始,左节点为 2*i+1 ,右节点为 2*i+1+2 ,父节点为 (i-1)//2
    image.png

最大堆

两个重要方法,插入元素和移出元素。

  • 插入元素:在堆尾插入元素,调用辅助方法,将该元素上浮到正确位置。
  • 移出元素:将堆尾元素删去并替换到堆首,将该元素下沉到正确位置。

解释:

  • 上浮:如果父节点更大,则替换,循环直至比父节点小。
  • 下沉:如果子节点中较大的那个更小,则替换,循环直至子节点都比自身小。

实现

class MaxHeap {constructor() {this.heap = []}isEmpty() {return this.heap.length === 0}size() {return this.heap.length}#getParentIndex(idx) {return Math.floor((idx-1)/2)}#getLeft(idx) {return idx * 2 + 1}#getRight(idx) {return idx * 2 + 2}// 插入insert(v) {this.heap.push(v)this.#swim(this.size()-1)}// 删除最大值deleteMax() {const max = this.heap[0]this.#swap(0, this.size() - 1) // 将根和最后一个元素交换this.heap.pop() // 防止对象游离this.#sink(0) // 下沉,恢复有序性return max}// 第i个是否小于第j个#compare(a, b) {return a < b}// 交换#swap(i, j) {[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]}// 上浮#swim(k) {let parent = this.#getParentIndex(k)while(k > 0 && this.#compare(this.heap[parent], this.heap[k])) {this.#swap(parent, k)k = parentparent = this.#getParentIndex(k)}}// 下沉#sink(k) {while (this.#getLeft(k) < this.size()) {let j = this.#getLeft(k)// j 指向子节点的较大值if (j+1 < this.size() && this.#compare(this.heap[j], this.heap[j+1])) j++// 如果子节点都小if (this.#compare(this.heap[j], this.heap[k])) breakthis.#swap(k, j)k = j}}
}

测试

const mh = new MaxHeap()
mh.insert(20)
mh.insert(80)
mh.insert(50)
mh.insert(40)
mh.insert(30)
mh.insert(40)
mh.insert(20)
mh.insert(10)
mh.insert(35)
mh.insert(15)
mh.insert(90)
console.log(mh.heap)
// [ <1 empty item>, 90, 80, 50, 35, 40, 40, 20, 10, 20, 15, 30 ]
mh.deleteMax()
mh.deleteMax()
mh.deleteMax()
console.log(mh.heap)
// [ <1 empty item>, 40, 35, 40, 20, 30, 15, 20, 10 ]

最小堆

与最小堆相比,仅是交换条件不同

实现

class MinHeap {constructor() {this.heap = []}isEmpty() {return this.heap.length === 0}size() {return this.heap.length}#getParentIndex(idx) {return Math.floor((idx-1)/2)}#getLeft(idx) {return idx * 2 + 1}#getRight(idx) {return idx * 2 + 2}// 插入insert(v) {this.heap.push(v)this.#swim(this.size()-1)}// 删除最大值deleteMin() {const max = this.heap[0]this.#swap(0, this.size() - 1) // 将根和最后一个元素交换this.heap.pop() // 防止对象游离this.#sink(0) // 下沉,恢复有序性return max}// 第i个是否小于第j个#compare(a, b) {return a > b}// 交换#swap(i, j) {[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]}// 上浮#swim(k) {let parent = this.#getParentIndex(k)while(k > 0 && this.#compare(this.heap[parent], this.heap[k])) {this.#swap(parent, k)k = parentparent = this.#getParentIndex(k)}}// 下沉#sink(k) {while (this.#getLeft(k) < this.size()) {let j = this.#getLeft(k)// j 指向子节点的较小值if (j+1 < this.size() && this.#compare(this.heap[j], this.heap[j+1])) j++// 如果子节点都大if (this.#compare(this.heap[j], this.heap[k])) breakthis.#swap(k, j)k = j}}
}

测试

const mh = new MinHeap()
mh.insert(20)
mh.insert(80)
mh.insert(50)
mh.insert(40)
mh.insert(30)
mh.insert(40)
mh.insert(20)
mh.insert(10)
mh.insert(35)
mh.insert(15)
mh.insert(90)
console.log(mh.heap)
// [10, 15, 20, 30, 20, 50, 40, 80, 35, 40, 90]
mh.deleteMin()
mh.deleteMin()
mh.deleteMin()
console.log(mh.heap)
// [20, 30, 40, 35, 40, 50, 90, 80]

堆(自定义比较函数)

默认为最大堆,根据元素的大小进行排序,可自定义排序规则,返回值为布尔值。

class Heap {constructor(compareFn) {this.heap = []this.compare = (typeof compareFn === 'function') ? compareFn : this.#defaultCompare}isEmpty() {return this.heap.length === 0}size() {return this.heap.length}#getParentIndex(idx) {return Math.floor((idx-1)/2)}#getLeft(idx) {return idx * 2 + 1}#getRight(idx) {return idx * 2 + 2}// 插入insert(v) {this.heap.push(v)this.#swim(this.size()-1)}// 删除最大值delete() {const max = this.heap[0]this.#swap(0, this.size() - 1) // 将根和最后一个元素交换this.heap.pop() // 防止对象游离this.#sink(0) // 下沉,恢复有序性return max}// 第i个是否小于第j个#defaultCompare(a, b) {return a < b}// 交换#swap(i, j) {[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]}// 上浮#swim(k) {let parent = this.#getParentIndex(k)while(k > 0 && this.compare(this.heap[parent], this.heap[k])) {this.#swap(parent, k)k = parentparent = this.#getParentIndex(k)}}// 下沉#sink(k) {while (this.#getLeft(k) < this.size()) {let j = this.#getLeft(k)// j 指向子节点的较大值if (j+1 < this.size() && this.compare(this.heap[j], this.heap[j+1])) j++// 如果子节点都小if (this.compare(this.heap[j], this.heap[k])) breakthis.#swap(k, j)k = j}}
}

测试

const mh = new Heap((a,b)=>a.val<b.val)
mh.insert({val: 20})
mh.insert({val: 45})
mh.insert({val: 56})
mh.insert({val: 12})
mh.insert({val: 93})
mh.insert({val: 34})
mh.insert({val: 12})
mh.insert({val: 84})
console.log(mh.heap)
// [
//   { val: 93 },
//   { val: 84 },
//   { val: 45 },
//   { val: 56 },
//   { val: 20 },
//   { val: 34 },
//   { val: 12 },
//   { val: 12 }
// ]
mh.delete()
mh.delete()
console.log(mh.heap)
// [
//   { val: 56 },
//   { val: 20 },
//   { val: 45 },
//   { val: 12 },
//   { val: 12 },
//   { val: 34 }
// ]

堆排序

(1)先原地创建一个最大堆,因为叶子节点没有子节点,因此只需要对非叶子节点从右向左进行下沉操作

(2)把堆首(堆的最大值)和堆尾替换位置,堆大小减一,保持非堆是递增的,保持数组最后一个元素是最大的,最后对堆首进行下沉操作(或者说把非堆重新堆化)。

(3)重复第二步直至清空堆。

注意:排序的数组第一个元素下标是 0 ,跟上面处理边界不一样。

实现

function heapSort (arr) {// arr = arr.slice(0) // 是否原地排序let N = arr.length - 1if (!arr instanceof Array) {return null}else if (arr instanceof Array && (N === 0 || N === -1) ) {return arr}function exch(i, j) {[arr[i], arr[j]] = [arr[j], arr[i]]}function less(i, j) {return arr[i] < arr[j]}function sink(k) {while (2 *k + 1 <= N) {let j = 2 * k + 1// j 指向子节点的较大值if (j+1 <= N && less(j, j+1)) {j++}// 如果子节点都小if (less(j, k)) breakexch(k, j)k = j}}// 构建堆for(let i = Math.floor(N/2); i >= 0; i--) {sink(i)}// 堆有序while (N > 0) {exch(0, N--)sink(0)}
}

另一个实现

function heapSort (arr) {// arr = arr.slice(0) // 是否原地排序let N = arr.lengthif (!arr instanceof Array) {return null}else if (arr instanceof Array && (N === 0 || N === -1) ) {return arr}function getParentIndex(idx) {return Math.floor((idx-1)/2)}function getLeft(idx) {return idx * 2 + 1}function getRight(idx) {return idx * 2 + 2}function swap(i, j) {[arr[i], arr[j]] = [arr[j], arr[i]]}function compare(i, j) {return i < j}function sink(k) {while (getLeft(k) < N) {let j = getLeft(k)// j 指向子节点的较大值if (j+1 < N && compare(arr[j], arr[j+1])) j++// 如果子节点都小if (compare(arr[j], arr[k])) breakswap(k, j)k = j}}// 构建堆for(let i = Math.floor(N/2); i >= 0; i--) {sink(i)}// 堆有序while (N > 1) {swap(0, --N)sink(0)}
}

测试

const arr1 = [15, 20, 30, 35, 20, 50, 40, 80, 10, 40, 90]
heapSort(arr1)
console.log(arr1)
// [10, 15, 20, 20, 30, 35, 40, 40, 50, 80, 90]
const arr2 = [62, 88, 58, 47, 35, 73, 51, 99, 37, 93];
heapSort(arr2)
console.log(arr2)
// [35, 37, 47, 51, 58, 62, 73, 88, 93, 99]

参考

  1. algs4
  2. 【JS手写最小堆(小顶堆)、最大堆(大顶堆)】:https://juejin.cn/post/7128369000001568798
  3. 【数据结构与算法(4)——优先队列和堆】:https://zhuanlan.zhihu.com/p/39615266
  4. 【最大堆最小堆及堆排序】:https://mingshan.fun/2019/05/14/heap/
  5. 【搞定JavaScript算法系列–堆排序】:https://juejin.cn/post/6844903830258188296

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/637443.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字谐振器设计

数字谐振器设计 电路里的谐振:当电路中激励的频率等于电路的固有频率时&#xff0c;电路电磁振荡的振幅也将达到峰值。 形式一 形式二 例子

Python程序设计 字典

教学案例十 字典 1. 判断出生地 sfz.txt文件中存储了地区编码和地区名称 身份证的前6位为地区编码&#xff0c;可以在sfz.txt文件中查询到地区编号对应的地区名称 编写程序&#xff0c;输入身份证号&#xff0c;查询并显示对应的地区名称 若该地区编码不在文件中&#xff0c;…

文本生成任务的评价方法BLEU 和 ROUGE

BLEU 是 2002 年提出的&#xff0c;而 ROUGE 是 2003 年提出的。这两种指标虽然存在着一些问题&#xff0c;但是仍然是比较主流的评价指标。 BLUE BLEU 的全称是 Bilingual evaluation understudy&#xff0c;BLEU 的分数取值范围是 0&#xff5e;1&#xff0c;分数越接近1&a…

【JavaWeb】Day51.Mybatis动态SQL

什么是动态SQL 在页面原型中&#xff0c;列表上方的条件是动态的&#xff0c;是可以不传递的&#xff0c;也可以只传递其中的1个或者2个或者全部。 而在我们刚才编写的SQL语句中&#xff0c;我们会看到&#xff0c;我们将三个条件直接写死了。 如果页面只传递了参数姓名name 字…

把idea的Java代码中的包打开,以层级的方式显示

我们在使用idea敲谢Java代码的时候&#xff0c;会注意到包显示在一起&#xff0c;这样对于有一些开发来说节省了空间&#xff0c;但是对于一些程序员来说看起来不舒服&#xff0c;而且不好操作。 针对这样的情况&#xff0c;我们需要取消一项勾选。 点击这三个... 取消这个按钮…

LeetCode 热题 100 题解:普通数组部分

文章目录 题目一&#xff1a;最大子数组和&#xff08;No. 53&#xff09;题解 题目二&#xff1a;合并区间&#xff08;No. 56&#xff09;题解 题目三&#xff1a;轮转数组&#xff08;No. 189&#xff09;题解 题目四&#xff1a;除自身以外数组的乘积&#xff08;No. 238&a…

深入剖析Spring框架:循环依赖的解决机制

你好&#xff0c;我是柳岸花开。 什么是循环依赖&#xff1f; 很简单&#xff0c;就是A对象依赖了B对象&#xff0c;B对象依赖了A对象。 在Spring中&#xff0c;一个对象并不是简单new出来了&#xff0c;而是会经过一系列的Bean的生命周期&#xff0c;就是因为Bean的生命周期所…

【高阶数据结构】并查集 -- 详解

一、并查集的原理 1、并查集的本质和概念 &#xff08;1&#xff09;本质 并查集的本质&#xff1a;森林。 &#xff08;2&#xff09;概念 在一些应用问题中&#xff0c;需要将 n 个不同的元素划分成一些不相交的集合。 开始时&#xff0c;每个元素自成一个单元素集合&…

ruoyi-vue前端的一些自定义插件介绍

文章目录 自定义列表$tab对象打开页签关闭页签刷新页签 $modal对象提供成功、警告和错误等反馈信息&#xff08;无需点击确认&#xff09;提供成功、警告和错误等提示信息&#xff08;类似于alert&#xff0c;需要点确认&#xff09;提供成功、警告和错误等提示信息&#xff08…

免费语音转文字:自建Whisper,贝锐花生壳3步远程访问

Whisper是OpenAI开发的自动语音识别系统&#xff08;语音转文字&#xff09;。 OpenAI称其英文语音辨识能力已达到人类水准&#xff0c;且支持其它98中语言的自动语音辨识&#xff0c;Whisper神经网络模型被训练来运行语音辨识与翻译任务。 此外&#xff0c;与其他需要联网运行…

【软考---系统架构设计师】软件架构

目录 1 一、软件架构的概念 二、软件架构风格 &#xff08;1&#xff09;数据流风格​​​​​​​ &#xff08;2&#xff09;调用/返回风格 &#xff08;3&#xff09;独立构件风格 &#xff08;4&#xff09;虚拟机风格 &#xff08;5&#xff09;仓库风格 三、架构…

《乱弹篇(30)厌战的杜诗》

时下地球村有一伙成天叫嚣着“打打杀杀”、鼓吹快快发动战争的狂人&#xff0c;他们视老百姓的生命如草芥&#xff0c;毫不珍惜。没有遭受过战火焚烧的人&#xff0c;也跟着成天吠叫“快开战吧”。然而中国唐朝大诗人却是个“厌战派”&#xff0c;他对战争的厌恶集中表现在诗《…