JavaScript中的[堆heap]数据结构
前言
javaScript 中数据结构都是由数组对象构建出来的,堆由数组构建【】,本质是完全二叉树结构
JS中的堆:
找到节点位置的,写插入删除查找替换等方法
一、节点位置和堆的用处
1、节点位置
- 左侧子节点的位置 : 2 * index + 1
- 右侧子节点的位置 : 2 * index + 2
- 父节点的位置 : (index -1) / 2 。 可以用位运算表示 (i - 1) >> 1
2、堆的运用
- 快速找最大最小值,时间复杂度O(1)
- 找第k个最大元素
二、最小堆类和方法
1.构建最小堆类
插入涉及移动父节点元素,getParentIndex(i) 方法获取父节点,upShift(index) 方法来移动当前节点, swap(i1,i2)方法来交换位置的值。
// 构建最小堆类
class MinHeap {
constructor() {
this.heap = []
}
// 交换
swap(i1, i2) {
let temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
// 获取父节点位置
getParentIndex(i) {
return (i - 1) >> 1
}
// 获取左子节点位置
getLeftIndex(i) {
return 2 * i + 1
}
// 获取右子节点位置
getRightIndex(i) {
return 2 * i + 2
}
// 上移
upShift(index) {
if(index == 0) return
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex] > this.heap[index]) {
this.swap(index, parentIndex)
this.upShift(parentIndex)
}
}
// 下移
downShift(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if(this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex, index)
this.downShift(leftIndex)
}
if(this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex, index)
this.downShift(rightIndex)
}
}
// 插入
insert(value) {
this.heap.push(value)
this.upShift(this.heap.length - 1)
}
// 删除堆顶
pop() {
this.heap[0] = this.heap.pop()
this.downShift(0)
}
}
2. 插入方法
根据最小堆的结构进行插入
时间复杂度为O(logk),
// 插入
insert(value) {
this.heap.push(value)
this.upShift(this.heap.length - 1)
}
测试插入方法
let minheap = new MinHeap()
minheap.insert(6)
minheap.insert(2)
minheap.insert(3)
minheap.insert(1)
console.log(minheap.heap);
结果
3. 删除堆顶
- 用数组尾部元素替换堆顶(直接删除堆顶会破环堆结构)
- 下移: 将新堆顶和它的子节点进行交换,直到子节点大于大于这新的堆顶
- 大小为k 的堆顶中删除堆顶的时间复杂度为O(logk),和插入一样
使用获取左右节点的方法 getLeftIndex(i) ,getRightIndex(i)和下移方法 downShift(index),交换方法swap(i1,i2)
// 删除堆顶
/** this.heap[0] 堆顶值
* this.heap.pop() 数组尾部元素的值
*/
pop() {
this.heap[0] = this.heap.pop()
this.downShift(0)
}
- 验证一下
let minheap = new MinHeap()
minheap.insert(6)
minheap.insert(2)
minheap.insert(3)
minheap.insert(1)
minheap.pop()
console.log(minheap.heap);
结果:
4. 获取堆顶和堆的大小vscode 调试
这两个比较简单,直接返回数组第一个元素和长度
// 获取堆顶
peek() {
return this.heap[0]
}
// 获取堆大小
size() {
return this.heap.length
}
- 可以在vs code 中使用node.js 的调试环境进行查看变量方法等,这里可以监听peek() 和size()
- F5启动调试
- 在调用堆栈栏目进行单步调试
- 在监视栏目设置断点监听
三. LeetCode 算法题中的堆
以下的算法题,在我们上面的堆类的基础上进行实现
1、215 数组中第k个最大元素
解题步骤
let findKLargest = function (nums, k) {
// 创建堆实例
const heap = new MinHeap()
for(let num of nums) {
// 将数组的值依次插入到堆里
heap.insert(num)
// 判断堆的容量是否超过k
if (heap.size() > k) {
// 如果超过,就删除堆顶
heap.pop()
}
}
// 返回堆顶
return heap.peek()
}
let arr1 = [3,2,1,5,6,4]
let arr2 = [3,2,2,1,2,4,5,5,6]
console.log(findKLargest(arr1, 2)); // 5
console.log(findKLargest(arr2, 4)); // 4
- 时间复杂度:O(n*logK)
- 空间复杂度: O(logK)
2、347.前K个高频元素
非堆结构的算法实现
k <= n
let topKFrequent = function (nums, k) {
const map = new Map();
for(let n of nums) {
map.set(n, map.has(n) ? map.get(n) + 1 : 1)
}
const list = Array.from(map).sort((a, b) => b[1] - a[1])
console.log(list.slice(0, k));
return list.slice(0, k).map(n => n[0])
}
let arr3 = [1,1,1,2,2,3]
console.log(topKFrequent(arr3, 2));// [1, 2]
console.log(topKFrequent([1], 1)); // [1]
- 时间复杂度:O(n*log n)
- 空间复杂度: O(log n)
堆结构的实现
……
下面的最小堆类符合传入对象时,对对象的value的比较的情况,
因此只在上移、下移的方法中的 if 判断时进行修改,
分别是判断父、左、右 节点是否存在,
因为要取里面的value,防止undefined的时候报错,
其余没有修改,为方便阅读就都啪的贴上了
if(this.heap[parentIndex] && this.heap[parentIndex].value > this.heap[index].value)
if(this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value)
if(this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value)
堆类和题目的算法和输出结果如下:
class MinHeap {
constructor() {
this.heap = []
}
// 交换
swap(i1, i2) {
let temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
// 获取父节点位置
getParentIndex(i) {
return (i - 1) >> 1
}
// 获取左子节点位置
getLeftIndex(i) {
return 2 * i + 1
}
// 获取右子节点位置
getRightIndex(i) {
return 2 * i + 2
}
// 上移
upShift(index) {
if(index == 0) return
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex]&& this.heap[parentIndex].value > this.heap[index].value) {
this.swap(index, parentIndex)
this.upShift(parentIndex)
}
}
// 下移
downShift(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if(this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value) {
this.swap(leftIndex, index)
this.downShift(leftIndex)
}
if(this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value) {
this.swap(rightIndex, index)
this.downShift(rightIndex)
}
}
// 插入
insert(value) {
this.heap.push(value)
this.upShift(this.heap.length - 1)
}
// 删除堆顶
pop() {
this.heap[0] = this.heap.pop()
this.downShift(0)
}
// 获取堆顶
peek() {
return this.heap[0]
}
// 获取堆大小
size() {
return this.heap.length
}
}
let topKFrequent = function (nums, k) {
const map = new Map();
for(let n of nums) {
map.set(n, map.has(n) ? map.get(n) + 1 : 1)
}
const heap = new MinHeap()
map.forEach((value, key) => {
heap.insert({value, key})
if(heap.size() > k) {
heap.pop()
}
})
return heap.heap.map(a => a.key)
}
let arr3 = [1,1,1,2,2,3]
console.log(topKFrequent(arr3, 2));// [2, 1]
console.log(topKFrequent([1], 1)); // [1]
注意: 这里题目对输出的顺序没有要求,和非堆结构相比发现了吗【1,2】 【2,1】
- 时间复杂度:O(n*logK)
- 空间复杂度: O(logK)
3、23.合并K个排序链表
解题思路
- 新链表的下一个节点一定是K个链表头重的最小节点
解题步骤
// 构建最小堆类
class MinHeap {
constructor() {
this.heap = []
}
// 交换
swap(i1, i2) {
let temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
// 获取父节点位置
getParentIndex(i) {
return (i - 1) >> 1
}
// 获取左子节点位置
getLeftIndex(i) {
return 2 * i + 1
}
// 获取右子节点位置
getRightIndex(i) {
return 2 * i + 2
}
// 上移
upShift(index) {
if(index == 0) return
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex]&& this.heap[parentIndex].value > this.heap[index].value) {
this.swap(index, parentIndex)
this.upShift(parentIndex)
}
}
// 下移
downShift(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if(this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value) {
this.swap(leftIndex, index)
this.downShift(leftIndex)
}
if(this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value) {
this.swap(rightIndex, index)
this.downShift(rightIndex)
}
}
// 插入
insert(value) {
this.heap.push(value)
this.upShift(this.heap.length - 1)
}
// 删除堆顶
pop() {
if(this.size() === 1) return this.heap.shift()
const top = this.heap[0]
this.heap[0] = this.heap.pop()
this.downShift(0)
return top
}
// 获取堆顶
peek() {
return this.heap[0]
}
// 获取堆大小
size() {
return this.heap.length
}
}
function ListNode(value) {
this.value = value
this.next = null
}
let mergeKlists = function (lists) {
const res = new ListNode(0)
let p = res
const heap = new MinHeap()
lists.forEach(list => {
if (list) {
heap.insert(list)
}
})
while (heap.size()){
const n = heap.pop()
p.next = n
p = p.next
if (n.next) {
heap.insert(n.next)
}
}
return res.next
}
let lists = [[1, 4, 5], [1, 3, 4], [2, 6]]
console.log(mergeKlists(lists));
这道题有点难啊
- 时间复杂度:O(n*logK)
- 空间复杂度: O(K)
6.文章结构框架md
@[TOC](<center>Title</center>)
<hr style=" border:solid; width:100px; height:1px;" color=#000000 size=1">
# 前言
<font color=#999AAA >**简述文章内容或者创作目的**</font>
<font face = '宋体' color = black size = 5>**完成目标:**
  文章想要达到什么样的目标
<hr style=" border:solid; width:100px; height:1px;" color=#000000 size=1">
# 一、
<font face = '宋体' color = black size = 5>
# 二、
## 1.
## 2.
## 3.
## 4.
## 5.
## 6.
# 最后
><font face="宋体" color = black size = 4>最后想和读者说的话
最后
堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。
堆在实际的应用中还是右出现的,调用堆栈这些,堆本质上是完全二叉树,堆在js中用数组表示【】,
万字文还真有点多。
释义
堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
堆中某个结点的值总是不大于或不小于其父结点的值;
堆总是一棵完全二叉树。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
堆是非线性数据结构,相当于一维数组,有两个直接后继。
堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。
(且)或者(), ()
若将和此次序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)
算法思想
不必将值一个个地插入堆中,通过交换形成堆。假设一个小根堆的左、右子树都已是堆,并且根的元素名为root ,其左右子结点为 left和right ,这种情况下,有两种可能:
(1)root <= left 并且root <= right,此时堆已完成;
(2) root >= left或者root >= right ,此时root 应与两个子女中值较小的一个交换,结果得到一个堆,除非 root仍然大于其新子女的一个或全部的两个。这种情况下,我们只需简单地继续这种将root “拉下来”的过程,直至到达某一个层使它小于它的子女,或者它成了叶结点。