堆分为大顶堆和小顶堆,对数组中topk的问题进行求解,以及堆排序,通过堆广的应用等
1.求数组的topk问题(堆解决)
- 求最大的topk,建立小顶堆( 每次和小顶堆比较,大于小顶堆替换堆顶元素,并调整堆 ,堆的大小始终k,是所有topk中要返回的数据)
- 求最小的topk,建立大顶堆
解决思路(求数组中的最小的前k个数)
方案1:直接遍历在排序去前k个(时间复杂度nlogn)
方案2:构建堆排序
- 把前
k个数构建一个大顶堆 - 从第
k个数开始,和大顶堆的最大值进行比较,若比最大值小,交换两个数的位置,重新构建大顶堆 - 一次遍历之后大顶堆里的数就是整个数据里最小的
k个数。
时间复杂度为
O(n)+O(nlogk) = O(nlogk)
这里只设计堆相关的实现
/**
* 求数组中topk
*/
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp
}
//插入元素向上调整
function moveUp(arr, index) {
let parent = parseInt((index - 1) / 2);
while (parent > -1 && arr[parent] < arr[index]) {
swap(arr, parent, index);
index = parent;
parent = parseInt((index - 1) / 2)
}
}
// 向下调整
function moveDown(arr, index, k) {
let left = 2 * index + 1;
//保证不会越界
while (left < k) {
//取左右孩子的最大值
let larget = (left + 1) < k && arr[left] < arr[left + 1] ? left + 1 : left;
//求父元素和子元素的最大值
larget = arr[index] > larget ? index : larget;
if (larget == index) {
break
}
swap(arr, index, larget);
index = larget;
left = 2 * index + 1
}
}
function topMink(arr, k) {
if (!arr || k > arr.length) return null;
for (let i = 0; i < arr.length; i++) {
if (i < k) {
//构建大顶堆
moveUp(arr, i);
} else {
//当大于arr[0],交换并向下调整
if (arr[0] < arr[i]) {
swap(arr, 0, i);
moveDown(arr, 0, k)
}
}
}
return arr.slice(0, k).reverse()
}
var arr = [5, 3, 1, 4, 7]
console.log(topMink(arr, 3));
扩展(nlogK)
当求解字符串中,出现频率最高的topk,依旧可以使用该方法
- 先整体遍历保存在map中(key:value),key表示的是字符,value表示出现的次数,
- 遍历map根据value的值构建小顶堆,(这里通过Object.entries(map)可以获取每一项,在通过item[0],item[1]分别获取键和值)
- 当前的value>小顶堆的元素,就覆盖小顶堆,调整小顶堆,首次遍历(i<k)直接进堆(注意堆的大小始终是k,存放的是当前topk)
- 最后返回堆中的所有的key就是词频最高的topk 这里主要是leetcode中通过reduce的解法
function topKFrequent(words, k) {
return Array.from(words.reduce((map, word) => map.set(word, (map.get(word) || 0) + 1), new Map()))
.sort((a, b) => b[1] - a[1] === 0 ? a[0].localeCompare(b[0]) : b[1] - a[1])
.slice(0, k)
.map(a => a[0])
}
2.堆排序(这里的堆从0开始计算)
-
堆:将数组看成是一个完全二叉树,堆分为大顶堆和小顶堆.
父子元素之间的关系
父元素:(i-1)/2
左孩子:2i+1<n
右孩子:2i-1<n,
这里的i是数组的下标值,当他们小于最后数组的长度表示子节点存在没有越界
-
堆结构非常重要
- 堆结构的插入heapInsert和调整heapify
- 堆结构的增大和减小
- 建立堆的过程,时间复杂度为O(N)
- 优先级队列结构就是堆结构
思路
- 通过遍历,边插入边调整堆
- 交换堆的最大值和最后一个元素,将调整范围-1,调整堆,
- 每次将最大值放在数组的末尾,直到调整范围变为0,此时数组中所有的元素都有序
复杂度
- 时间:O(nlogn)
- 空间:O(1)
- 不稳定
注意
- 堆在调整的过程中的时间复杂度就是logN,在不断的调整二叉树结构;插入N
- 通过堆就可以实现一个优先级队列
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//注意这里采用的是大顶堆
//向上调整,插入操作
function insert(arr, index) {
let parent = Math.floor((index - 1) / 2);
while (parent > -1 && arr[index] > arr[parent]) {
swap(arr, index, parent);
index = parent;
parent = Math.floor((index - 1) / 2);
}
}
//向下调整
function heapfy(arr, index, heapsize) {
let left = 2 * index + 1;
while (left < heapsize) {
let largest = (left + 1) < heapsize && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) break;
swap(arr, largest, index);
index = largest;
left = 2 * index + 1;
}
}
function heapSort(arr) {
if (!arr || arr.length < 1) return;
for (let i = 0; i < arr.length; i++) {
insert(arr, i)
}
let heapsize = arr.length;
swap(arr, 0, --heapsize);
while (heapsize) {
heapfy(arr, 0, heapsize);
swap(arr, 0, --heapsize)
}
}
let arr = [4, 3, 6, 7, 2, 1, 8]
heapSort(arr);
console.log(arr)
3.补充:通过堆实现优先级队列
-
这里通过堆来实现优先级队列(可以通过数组实现也可以通过链表实现)
-
堆在实现的过程中都是通过数组来实现
- 优先队列插入都是先插到最后的新结点位置,数组的话直接插到最后就行,不需要记录最后结点的位置。
- 因为完全二叉树的结构,我们可以通过当前结点的索引来计算父结点的索引位置
-
主要的应用场景
- 数据的topk
在调整过程的时间复杂度O(logn)树的高度
/**
* 在堆的基础上进行修改
*/
function PriorityQueue(type = 'max') {
this.queue = [];
this.type = type;//min||max
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp
}
//入队列,也就是向上调整
PriorityQueue.prototype.moveUp = function () {
let index = this.queue.length - 1;
let parent = parseInt((index - 1) / 2);
if (this.type == 'max') {
while (parent > -1 && this.queue[index] > this.queue[parent]) {
swap(this.queue, index, parent);
index = parent;
parent = parseInt((index - 1) / 2)
}
} else if (this.queue == 'min') {
while (parent > -1 && this.queue[index] < this.queue[parent]) {
swap(this.queue, index, parent);
index = parent;
parent = parseInt((index - 1) / 2)
}
}
}
//向下调整
PriorityQueue.prototype.moveDown = function () {
let index = 0;
let length = this.queue.length;
let left = 2 * index + 1;
if (this.type == 'max') {
while (left < length) {
let largest = (left + 1) < length && this.queue[left + 1] > this.queue[left] ? left + 1 : left;
largest = this.queue[index] > this.queue[largest] ? index : largest;
if (largest == index) break;
swap(this.queue, index, largest)
index = largest;
left = 2 * index + 1;
}
} else if (this.type == 'min') {
while (left < length) {
let min = (left + 1) < length && this.queue[left + 1] > this.queue[left] ? left : left + 1;
min = this.queue[min] > this.queue[index] ? index : min;
if (min == index) break;
swap(this.queue, min, index);
index = min;
left = 2 * index + 1
}
}
}
PriorityQueue.prototype.enqueue = function (data) {
this.queue.push(data);
this.moveUp();
}
PriorityQueue.prototype.dequeue = function () {
if (!this.queue.length) return null;
let res = this.queue.shift();
//用最后一个元素插入头部
this.queue.unshift(this.queue.pop())
//向下调整
this.moveDown();
return res
}
const p = new PriorityQueue(type = 'max');
p.enqueue(5);
p.enqueue(10);
console.log(p)
p.dequeue();
console.log(p)