JS/TS数据结构---堆
作者:互联网
1.什么是堆?
- 堆是一种特殊的完全二叉树
- 所有的节点都 大于等于 或 小于等于 它的子节点,最大堆的根节点大于等于它的子节点,最小堆的根节点小于等于它的子节点
- JS中常用数组表示堆
- 完全二叉树:二叉树除开最后一层,其他层结点数都达到最大,最后一层的所有结点都集中在左边(左边结点排列满的情况下,右边才能缺失结点)。
- 大顶堆:根结点为最大值,每个结点的值大于或等于其孩子结点的值。
- 小顶堆:根结点为最小值,每个结点的值小于或等于其孩子结点的值。
- 堆的存储:堆由数组来实现,相当于对二叉树做层序遍历。如下图:
2.常用操作
- 获取父节点
- 获取左节点
- 获取右节点
- 上移节点
- 下移节点
- 插入节点
- 删除节点
- 获取堆顶
- 获取堆的大小
- 寻找第K个最大元素
3.定义堆类
以定义一个最小堆为例
class MinHeap{
constructor() {
this.heap = [];
}
// 获取父节点
getParentIndex(i) {
return (i-1) >> 1;
}
// 获取左节点
getLeftIndex(i) {
return i * 2 + 1;
}
// 获取右节点
getRightIndex(i) {
return i * 2 + 2;
}
// 交换值
swap(i1,i2) {
const temp = this.heap[i1];
this.heap[i1] = this.heap[i2];
this.heap[i2] = temp;
}
// 上移节点
shiftUp(index) {
//到达堆顶就不用上移了
if(index == 0) {return;}
const parentIndex = this.getParentIndex(index);
// 最小堆要求父节点是最小的
if(this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex,index);
// 交换过后尝试继续上移
this.shiftUp(parentIndex);
}
}
//下移节点
shiftDown(index) {
const leftIndex = this.getLeftIndex(index);
const rightIndex = this.getRightIndex(index);
if(this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex,index);
this.shiftDown(leftIndex);
}
if(this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex,index);
this.shiftDown(rightIndex);
}
}
//插入节点
insert(value) {
this.heap.push(value);
this.shiftUp(this.heap.length - 1);
}
//删除节点
pop() {
// 把堆顶元素替换为堆的最后一个元素
this.heap[0] = this.heap.pop();
this.shiftDown(0);
}
// 获取堆顶
peek() {
return this.heap[0];
}
// 获取堆的大小
size() {
return this.heap.length;
}
}
const h = new MinHeap();
实现堆排序
LeetCode题精选
接下来使用堆这个数据结构来刷LeetCode有关堆的题目,巩固提升对堆的了解。
[215] 数组中的第K个最大元素(中等)
题目要求:
解题思路:
看到第k个最大值或者最小值,可以考虑直接构建堆
- 构建一个最小堆,并依次把数组的值插入堆中
- 当堆的容量超过k,就更换堆顶
- 插入结束后,堆顶就是第K个最大元素
编写代码:
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
// 构建一个最小堆
class MinHeap{
constructor() {
this.heap = [];
}
// 获取父节点
getParentIndex(i) {
return (i-1) >> 1;
}
// 获取左节点
getLeftIndex(i) {
return i * 2 + 1;
}
// 获取右节点
getRightIndex(i) {
return i * 2 + 2;
}
// 交换值
swap(i1,i2) {
const temp = this.heap[i1];
this.heap[i1] = this.heap[i2];
this.heap[i2] = temp;
}
// 上移节点
shiftUp(index) {
//到达堆顶就不用上移了
if(index == 0) {return;}
const parentIndex = this.getParentIndex(index);
// 最小堆要求父节点是最小的
if(this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex,index);
// 交换过后尝试继续上移
this.shiftUp(parentIndex);
}
}
//下移节点
shiftDown(index) {
const leftIndex = this.getLeftIndex(index);
const rightIndex = this.getRightIndex(index);
if(this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex,index);
this.shiftDown(leftIndex);
}
if(this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex,index);
this.shiftDown(rightIndex);
}
}
//插入节点
insert(value) {
this.heap.push(value);
this.shiftUp(this.heap.length - 1);
}
//删除节点
pop() {
// 把堆顶元素替换为堆的最后一个元素
this.heap[0] = this.heap.pop();
this.shiftDown(0);
}
// 获取堆顶
peek() {
return this.heap[0];
}
// 获取堆的大小
size() {
return this.heap.length;
}
}
var findKthLargest = function(nums, k) {
const h = new MinHeap();
nums.forEach(n => {
h.insert(n);
if(h.size() > k) {
h.pop();
}
});
return h.peek();
};
复杂度分析
- 时间复杂度:O(n) ,n为数组nums长度
- 空间复杂度:O(1) 堆的长度最长为k
[887] 鸡蛋掉落
你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?
示例 1:
输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。
示例 2:
输入:K = 2, N = 6
输出:3
示例 3:
输入:K = 3, N = 14
输出:4
提示:
1 <= K <= 100, 1 <= N <= 10000
你面前有一栋从1到N共N层的楼,不限制鸡蛋个数,现在确定这栋楼存在楼层0 <= F <= N,在这层楼将鸡蛋扔下去,鸡蛋恰好没摔碎(高于F的楼层都会碎,低于F的楼层都不会碎)。现在问你,最坏情况下,你至少要扔几次鸡蛋,才能确定这个楼层F呢?
PS:F 可以为 0,比如说鸡蛋在 1 层都能摔碎,那么 F = 0。
用最简单的二分法解
var eggDrop = function(n) {
if (n <= 0) return 0;
let count = 0;
let temp = n;
while (temp > 1) {
temp = temp >> 1;
count++;
}
return count;
};
你面前有一栋从1到N共N层的楼,限制鸡蛋个数为2,现在确定这栋楼存在楼层0 <= F <= N,在这层楼将鸡蛋扔下去,鸡蛋恰好没摔碎(高于F的楼层都会碎,低于F的楼层都不会碎)。现在问你,最坏情况下,你至少要扔几次鸡蛋,才能确定这个楼层F呢?
这时题目的变化使得不适用二分法求解了,本质上是一道动态规划求极值的问题。经典的"最坏情况下代价最小"问题。
由于只有两个鸡蛋,那么子问题可以分解为:总楼层为n,第一个鸡蛋随便从一个楼层i扔下去,碎了与没碎两种情况,
- 如果碎了,那么只能从第一层开始一直遍历到当前楼层i之前,共需 i - 1次
- 如果没碎,那么i层之前的楼层就一定不会碎,因此子问题转化为测试[i+1, n] 层的最小测试次数,即dp[n-i]
以上两种情况取较大值再加上本次测试使用的1次机会,即为本次实验的最小次数
那么有:dp[n] = 1 + max(i - 1, dp[n - i])
这是不是已经是该题的解呢,其实不是,如果我们能选择任意小于n的楼层进行上述求解,有可能取得的值要比上述结果更小,因此我们应当对所有小于总楼层n的楼层遍历取所有结果的最小值作为本次子问题的解。
即:dp[n] = min(1 + max(i - 1, dp[j - i])) (j遍历 1~n)
var eggDrop2 = function(n) {
if (n <= 0) return 0;
const dp = new Array(n).fill(Number.MAX_SAFE_INTEGER);
dp[0] = 1;
for (let j = 1; j < n; j++) {
for (let i = 1; i <= j; i++) {
const temp = 1 + Math.max(i - 1, dp[j - i]);
dp[j] = Math.min(temp, dp[j]);
}
}
return dp[n - 1];
};
你面前有一栋从1到N共N层的楼,限制鸡蛋个数为K,现在确定这栋楼存在楼层0 <= F <= N,在这层楼将鸡蛋扔下去,鸡蛋恰好没摔碎(高于F的楼层都会碎,低于F的楼层都不会碎)。现在问你,最坏情况下,你至少要扔几次鸡蛋,才能确定这个楼层F呢?
现在这道题变成了更为复杂的DP问题,由于多加入了一个参数,显然需要使用二维数组解题。
在上面已经推导出 k=2 时的状态转移方程了,又已知 i-1 这个值是 k=1 时的特例,那接下来的状态转移方程便也不难推导了。
还是依照上面的思路,每次在第i层扔鸡蛋的结果共有两种情况:
- 如果碎了,那么可使用的鸡蛋数减少一个,剩下需要测试的层数为1~i层,因此子问题转化为dp[k-1][i-1]
- 如果没碎,那么鸡蛋数不变,并且i层之前的楼层就一定不会碎,需要测试i+1~n层,因此子问题转化为dp[k][n-i]
那么有:dp[k][n] = 1 + max(dp[k-1][i-1], dp[k][n - i])
还是如上题所说,我们应当对所有小于总楼层n的楼层遍历取所有结果的最小值作为本次子问题的解。
即 dp[k][n] = min(1 + max(dp[k-1][i-1], dp[k][j - i])) (j遍历 1~n)
var superEggDrop = function(K, N) {
if (N < 1) return 0;
if (N === 1) return 1;
if (K === 1) return N;
const dp = new Array(K + 1).fill(0).map(x => new Array(N + 1).fill(Number.MAX_SAFE_INTEGER));
// K=0,1特例
for (let i = 0; i <= N; i++) {
dp[0][i] = 0;
dp[1][i] = i;
}
// N=0,1特例
for (let i = 1; i <= K; i++) {
dp[i][0] = 0;
dp[i][1] = 1;
}
for (let k = 2; k <= K; k++) {
for (let j = 1; j <= N; j++) {
for (let i = 1; i <= j; i++) {
const temp = 1 + Math.max(dp[k - 1][i - 1], dp[k][j - i]);
dp[k][j] = Math.min(temp, dp[k][j]);
}
}
}
console.log(dp);
return dp[K][N];
};
算法思想实现大致如此,时间复杂度为O(KN^2),空间复杂度为O(KN)。但是很遗憾,这是一道hard难度的题目,因此直接这样提交会TLE,我们需要想办法优化这个算法。
通过观察 dp[k-1][i-1] 与 dp[k][n-i] 这两种情况,我们可以发现前者随着i单调递增,后者随i单调递减,那么求这两者间的最大值可以转化为:存在一个值i,使得当二者相等或者差值最小,此时得到最优解。
同时我们观察二维数组还可以发现,随着j的增加,此时最优决策点也是单调递增的,那么对于每一次遍历k时,我们可以使用一个变量i,记录当前层数为j时的最优决策点,当j增加时,将i更新即可。这样我们就能将时间复杂度降到O(KN),总算能够AC了。
var superEggDrop = function(K, N) {
if (N < 1) return 0;
if (N === 1) return 1;
if (K === 1) return N;
const dp = new Array(K + 1).fill(0).map(x => new Array(N + 1).fill(Number.MAX_SAFE_INTEGER));
// K=0,1特例
for (let i = 0; i <= N; i++) {
dp[0][i] = 0;
dp[1][i] = i;
}
// N=0,1特例
for (let i = 1; i <= K; i++) {
dp[i][0] = 0;
dp[i][1] = 1;
}
for (let k = 2; k <= K; k++) {
let i = 1;
for (let j = 1; j <= N; j++) {
while (i < j && dp[k - 1][i - 1] < dp[k][j - i]) ++i;
const temp = 1 + Math.max(dp[k - 1][i - 1], dp[k][j - i]);
dp[k][j] = Math.min(temp, dp[k][j]);
}
}
return dp[K][N];
};
1
标签:index,return,TS,JS,---,楼层,heap,节点,dp 来源: https://www.cnblogs.com/guibi/p/16419976.html