堆,是一种特殊的完全二叉树。 他要求他的子节点必须大于等于或小于等于父节点。且每个二叉树都必须填满,或者最后的右子树可以为空。同时,堆又分为最小堆和最大堆。下图就是典型的最大堆。
堆在js中的表示方式
在js中,我们通常使用数组去表示一个堆。 数组中的元素顺序,遵循广度优先遍历分别推入数组。于是,我们得出几个数组表示堆的规律。
- 左侧节点的位置是 2 * parentIndex + 1
- 右侧节点的位置是 2 * (parentIndex + 1)
- 父节点的位置是 (parentIndex - 1) / 2
构建最小堆类
因为js没有直接提供堆这个数据结构,所以我们可以使用一个构造函数去生成堆。我们需要去实现
- insert插入
- remove移除堆顶
- pop移除堆尾
- size获取堆的大小
insert实现
这里,我们要实现insert,可以先将元素插入到堆底,然后跟父节点进行不断的比较,同时交换元素,直到元素满足最小堆的条件,跳出递归。
class MinHeap{
constructor() {
this.heap = [];
}
getParentIndex(childIndex) {
return Math.floor((childIndex - 1) / 2);
}
swap(idx1, idx2) {
const temp = this.heap[idx1];
this.heap[idx1] = this.heap[idx2];
this.heap[idx2] = temp;
}
shiftUp(childIndex) {
if (childIndex === 0) { // 当元素处于堆顶时,已经不能再往上移动了
return ;
}
const parentIndex = this.getParentIndex(childIndex);// 获取父节点的index
const parent = this.heap[parentIndex];
const child = this.heap[childIndex];
if (parent > child) {
this.swap(childIndex, parentIndex);// 比较交换
this.shiftUp(parentIndex); // 递归交换,直到插入元素满足最小堆的条件
}
}
insert(item) {
this.heap.push(item); // 先在堆底推入一个元素
this.shiftUp(this.heap.length - 1); // 根据大小进行交换,使堆满足最小堆的条件
}
}
remove实现
这里要移除堆顶,如果直接移除堆顶元素,会导致堆结构混乱,所以,我们采取的思路是将堆底的元素移除,同时替换掉堆顶。然后根据父节点与子节点的大小关系去递归的调整父子节点的index。
class MinHeap{
constructor() {
this.heap = [];
}
swap(idx1, idx2) {
const temp = this.heap[idx1];
this.heap[idx1] = this.heap[idx2];
this.heap[idx2] = temp;
}
getLeftChildIndex(parentIndex) {
return 2 * parentIndex + 1;
}
getRightChildIndex() {
return 2 * (parentIndex + 1);
}
shiftDown(parentIndex) {
if (parentIndex === this.heap.length) {return;} // 等于堆底时已经无法下移
const parent = this.heap[parentIndex];
const leftChildIndex = this.getLeftChildIndex(parentIndex);
const rightChildIndex = this.getRightChildIndex(parentIndex);
const leftChild = this.heap[leftChildIndex];
const rightChild = this.heap[rightChildIndex];
if (leftChild < parent) {
this.swap(parentIndex, leftChildIndex);
this.shiftDown(leftChildIndex);
}
if (rightChild < parent) {
this.swap(parentIndex, rightChildIndex);
this.shiftDown(rightChildIndex);
}
}
remove() {
this.heap[0] = this.heap.pop();
this.shiftDown(0);
}
}
pop移除堆尾
这里可以直接移除堆尾即可。
class MinHeap{
constructor() {
this.heap = [];
}
pop() {
return this.heap.pop();
}
}
size 获取堆的大小
class MinHeap{
constructor() {
this.heap = [];
}
get size() {
return this.heap.length;
}
}
堆的应用
堆能高效的找出最大值和最小值。因为只要堆构建完成,最大值或者最小值就是堆顶。或者找出数组中第k个大的元素。
215. 数组中的第K个最大元素
解题思路:
通常,这种第k个最大元素我们都可以考虑使用堆这个数据结构去完成。因为第k个最大元素或者最小元素。实际上就是我们构建一个堆。 然后不断的删除堆顶。当堆大小大于k时我们就删除,直至堆大小等于k。这时,堆顶元素就是第k个大的元素。
class MinHeap {
constructor() {
this.heap = [];
}
get size() {
return this.heap.length;
}
getParentIndex(index) {
return Math.floor((index - 1) / 2);
}
swap(index1, index2) {
const temp = this.heap[index1];
this.heap[index1] = this.heap[index2];
this.heap[index2] = temp;
}
shiftUp(index) {
// 拿到父节点
if (index === 0) {
return;
}
const parentIndex = this.getParentIndex(index);
if (this.heap[parentIndex] > this.heap[index]) {
this.swap(index, parentIndex);
this.shiftUp(parentIndex);
}
}
insert(item) {
this.heap.push(item);
this.shiftUp(this.heap.length - 1);
}
/**
* @description 获取左侧子节点的index
*/
getLeftIndex(parentIndex) {
return 2 * parentIndex + 1;
}
getRightIndex(parentIndex) {
return 2 * (parentIndex + 1);
}
shiftDown(index) {
if (index === this.heap.length) {
// 已经到堆底了,没有向下交换的意义了
return;
}
const leftIndex = this.getLeftIndex(index);
const rightIndex = this.getRightIndex(index);
if (this.heap[index] > this.heap[leftIndex]) {
this.swap(index, leftIndex);
this.shiftDown(leftIndex);
}
if (this.heap[index] > this.heap[rightIndex]) {
this.swap(index, rightIndex);
this.shiftDown(rightIndex);
}
}
/**
* @description 移除堆顶元素
*/
remove() {
this.heap[0] = this.heap.pop(); // 移除尾部并用尾部元素替换堆顶元素。
this.shiftDown(0);
}
}
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
const heap = new MinHeap();
nums.forEach(item => {
heap.insert(item);
if (heap.size > k) {
heap.remove();
}
})
return heap.heap[0];
};
当然,我们也可以走走捷径 - - 。
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
return nums.sort((prev, curr) => curr - prev)[k - 1];
};
347. 前 K 个高频元素
代码思路:
- 用字典记录每个元素的出现次数。
- 将记录的字典值逐个推入到一个最小堆中, 堆的比较数据使用字典记录的出现次数。
- 不断移除堆顶元素,使得最小堆的长度等于k。这时剩下的就是前k个大小的了。
代码实现:
class MinHeap {
constructor() {
this.heap = [];
}
getParentIndex(index) {
return Math.floor((index - 1) / 2);
}
swap(index1, index2) {
const temp = this.heap[index1];
this.heap[index1] = this.heap[index2];
this.heap[index2] = temp;
}
shiftUp(index) {
// 拿到父节点
if (index === 0) {
return;
}
const parentIndex = this.getParentIndex(index);
if (this.heap[parentIndex].value > this.heap[index].value) {
this.swap(index, parentIndex);
this.shiftUp(parentIndex);
}
}
insert(item) {
this.heap.push(item);
this.shiftUp(this.heap.length - 1);
}
/**
* @description 获取左侧子节点的index
*/
getLeftIndex(parentIndex) {
return 2 * parentIndex + 1;
}
getRightIndex(parentIndex) {
return 2 * (parentIndex + 1);
}
shiftDown(index) {
if (index === this.heap.length) {
// 已经到堆底了,没有向下交换的意义了
return;
}
const leftIndex = this.getLeftIndex(index);
const rightIndex = this.getRightIndex(index);
if (this.heap[index].value > this.heap[leftIndex].value) {
this.swap(index, leftIndex);
this.shiftDown(leftIndex);
}
if (this.heap[index].value > this.heap[rightIndex].value) {
this.swap(index, rightIndex);
this.shiftDown(rightIndex);
}
}
get size() {
return this.heap.length;
}
/**
* @description 移除堆顶元素
*/
remove() {
this.heap[0] = this.heap.pop(); // 移除尾部并用尾部元素替换堆顶元素。
this.shiftDown(0);
}
}
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function (nums, k) {
const m = new Map();
nums.forEach((item) => {
m.set(item, m.has(item) ? m.get(item) + 1 : 1);
});
const h = new MinHeap();
m.forEach((value, key) => {
h.insert({ value, key });
if (h.size > k) {
h.remove();
}
});
return h.heap.map(item => item.key);
};