本篇文章可以收获的知识:
- 什么是
堆
堆
的使用场景- 使用JavaScript构建
堆
- 三道LeetCode算法题巩固
堆
- 如何分析
时间复杂度
和空间复杂度
什么是堆
堆
是一种特殊的完全二叉树,所有的节点都大于等于(最大堆)或者小于等于(最小堆)它的子节点,JavaScript通常使用数组表示堆,左侧子节点的位置是2 * index + 1
,右侧子节点的位置是2 * index + 2
,父节点位置是(index - 1)/ 2
。
堆的使用场景
- 堆能高效、快速地找出最大值和最小值,时间复杂度是O(1)。
- 找出第K个最大(小)元素思路
- 构建一个推,并将元素依次插入堆中
- 当堆的容量超过K,就删除堆顶
- 插入结束后,堆顶就是第K个最大元素
使用JavaScript构建堆
// 最小堆
// 构建堆的思路
// 在类里声明一个数组,用来装元素
// 主要方法:插入、删除堆顶、获取堆顶、获取堆大小
class MinHead {
constructor() {
this.heap = []
}
/**
* 获取左节点索引
* @param {number} index
*/
getLeftIndex(index) {
return index * 2 + 1
}
/**
* 获取右节点索引
* @param {number} index
*/
getRightIndex(index) {
return index * 2 + 2
}
/**
* 获取堆顶
*/
peek() {
return this.heap[0]
}
/**
* 获取堆大小
*/
size() {
return this.heap.length
}
/**
* 交换元素
* @param {number} i1
* @param {number} i2
*/
swap(i1, i2) {
const tmp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = tmp
}
/**
* 获取父节点索引
* @param {number} index
*/
getParentIndex(index) {
return (index - 1) >> 1
}
/**
* 将元素往上移
* @param {number} index
*/
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)
}
}
/**
* 插入方法
* @param {number} target
*/
insert(target) {
// 将值插入到堆的底部,即数组的结尾
// 然后上移:将这个值与父节点进行交换,直到父节点小于等于插入的这个值
// 大小为K的堆中插入元素的时间复杂度是O(logK)
this.heap.push(target)
this.shiftUp(this.heap.length - 1)
}
/**
* 下移元素
* @param {number} index
*/
shiftDown(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if (this.heap[index] > this.heap[leftIndex]) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if (this.heap[index] > this.heap[rightIndex]) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
/**
* 删除堆顶
*/
pop() {
// 使用数组的尾元素替换堆顶(直接删除堆顶会破坏堆的结构)
// 然后下移操作:将新堆顶和他的子节点进行交换,直到新堆顶小于等于子节点
// 大小为K的堆下移操作的时间复杂度是O(logK)
this.heap[0] = this.heap.pop()
this.shiftDown(0)
}
}
LeetCode算法题:题号215,数组中的第K个最大元素
解题思路:
- 看到第K个最大元素,考虑使用最小堆
解题步骤:
- 构建一个最小堆,并依次把数组的值插入到堆中
- 当堆的容量大于K时,就删除堆顶
- 插入结束后,返回堆顶,堆顶就是第K个最大元素
代码实现:
class MinHead {
constructor() {
this.heap = []
}
getLeftIndex(index) {
return index * 2 + 1
}
getRightIndex(index) {
return index * 2 + 2
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
swap(i1, i2) {
const tmp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = tmp
}
getParentIndex(index) {
return (index - 1) >> 1
}
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)
}
}
insert(target) {
this.heap.push(target)
this.shiftUp(this.heap.length - 1)
}
shiftDown(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if (this.heap[index] > this.heap[leftIndex]) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if (this.heap[index] > this.heap[rightIndex]) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
pop() {
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 MinHead()
for(let i = 0; i < nums.length; i ++) {
heap.insert(nums[i])
if (heap.size() === k + 1) {
heap.pop()
}
}
return heap.peek()
};
由于代码中使用了循环,并且在堆中插入或者删除堆顶的时候的有个递归,所以时间复杂度时O(n * logK),n时数组的长度,K是堆的大小。
由于代码中使用了堆,堆的空间复杂度是O(n),n是堆的大小。
LeetCode算法题:题号347,前K个高频元素
解题思路:
- 前K个高频元素,使用最小堆解决问题
解题步骤
- 构建一个最小堆,并依次把处理过的数据的值插入到堆中
- 当堆的容量大于K时,删除堆顶
- 遍历结束后返回处理后的堆
代码实现:
class MinHead {
constructor() {
this.heap = []
}
getLeftIndex(index) {
return index * 2 + 1
}
getRightIndex(index) {
return index * 2 + 2
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
swap(i1, i2) {
const tmp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = tmp
}
getParentIndex(index) {
return (index - 1) >> 1
}
shiftUp(index) {
if (index === 0) return
const parentIndex = this.getParentIndex(index)
if (
this.heap[parentIndex] &&
this.heap[parentIndex][1] > this.heap[index][1]
) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
insert(target) {
this.heap.push(target)
this.shiftUp(this.heap.length - 1)
}
shiftDown(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if (this.heap[leftIndex] && this.heap[index][1] > this.heap[leftIndex][1]) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if (
this.heap[rightIndex] &&
this.heap[index][1] > this.heap[rightIndex][1]
) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
pop() {
this.heap[0] = this.heap.pop()
this.shiftDown(0)
}
}
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
const heap = new MinHead()
const map = new Map()
for (let i = 0; i < nums.length; i++) {
const c = nums[i]
map.set(c, map.get(c) ? map.get(c) + 1 : 1)
}
map.forEach((value, key) => {
heap.insert([key, value])
if (heap.size() === k + 1) {
heap.pop()
}
})
return heap.heap.map((i) => i[0])
}
由于代码中使用了遍历了数组,所以时间复杂度是O(n),n是数组的长度,在遍历的时候操作堆,在上移元素或者下移元素的操作中的时间复杂度是O(logK),K是堆的大小,所以时间复杂度是O(n * logK)。
由于代码中使用了堆,所以空间复杂度是O(K),K是堆的大小。
LeetCode算法题:题号23,合并k个排序链表
解题思路;
- 排序可以使用堆数据结构,由于这道题目是链表,所以在取出堆顶的时候,如果next不为空,则还需要将next插入的堆中
解题步骤:
- 构建一个最小堆,并依次把链头插入到堆中
- 弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入到堆中
- 等堆元素全部弹出,合并工作就完成了
代码实现:
class MinHead {
constructor() {
this.heap = []
}
getLeftIndex(index) {
return index * 2 + 1
}
getRightIndex(index) {
return index * 2 + 2
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
swap(i1, i2) {
const tmp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = tmp
}
getParentIndex(index) {
return (index - 1) >> 1
}
shiftUp(index) {
if (index === 0) return
const parentIndex = this.getParentIndex(index)
if (
this.heap[parentIndex] &&
this.heap[parentIndex].val > this.heap[index].val
) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
insert(target) {
this.heap.push(target)
this.shiftUp(this.heap.length - 1)
}
shiftDown(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if (
this.heap[leftIndex] &&
this.heap[index].val > this.heap[leftIndex].val
) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if (
this.heap[rightIndex] &&
this.heap[index].val > this.heap[rightIndex].val
) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
pop() {
const t = this.heap.pop()
if (this.size() === 0) {
return
}
this.heap[0] = t
this.shiftDown(0)
}
}
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode[]} lists
* @return {ListNode}
*/
var mergeKLists = function(lists) {
const heap = new MinHead()
for (let i = 0; i < lists.length; i++) {
lists[i] && heap.insert(lists[i])
}
const ln = new ListNode(0)
let p = ln
while (heap.size() > 0) {
const t = heap.peek()
p.next = t
p = p.next
heap.pop()
if (t.next) {
heap.insert(t.next)
}
}
return ln.next
}
由于代码中使用了while循环,循环的次数是所有链表的长度,时间复杂度是O(n),在循环的过程在堆在进行上移或者下移的操作,时间复杂度是O(logK),K是K个链表,所以最终时间复杂度是O(n * logK)。
由于代码中使用了堆数据结构,所以空间复杂度是O(n),n是K个链表。
最后,希望读者可以通过本篇文章对堆
有一定的认识和觉悟。。。。。。