# 定义

  • 堆是一种特殊的完全二叉树;

完全二叉树是每层节点都完全填满,在最后一层如果不是满的,则只缺少右边的若干节点。

avatar

  • 所有的节点都大于等于(最大堆)或小于等于(最小堆)他的子节点;

# js 中的堆

  • js 中通常用数组表示堆;

广度优先遍历:

avatar

  • 左侧子节点的位置是 2 * index + 1;
  • 右侧子节点的位置是 2 * index + 2;
  • 父节点位置是 (index - 1) / 2

# 堆的应用

  • 堆能高效、快速地找出最大值和最小值,时间复杂度为 o(1);

  • 找出第 k 个最大(小)元素;

首先构建一个最小堆,并将元素依次插入堆中;当对的容量超过 k,就删除堆顶;插入结束后,堆顶就是第 k 个最大元素;

# js 实现最小堆

主要是通过声明一个数组,然后实现插入、删除堆顶、获取堆顶、获取堆大小等方法。

# 插入

  • 将值插入堆的底部,即数组的尾部;
  • 然后上移:将这个值和他的父节点进行交换,直到父节点小于等于这个插入的值;
  • 大小为 k 的堆中插入元素的时间复杂度为 O(logk);
class MinHeap {
  constructor() {
    this.heap = []
  }

  getParentIndex(i) {
    // return Math.floor((i - 1) / 2)
    return (i - 1) >> 1
  }

  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)
    }
  }

  insert(value) {
    this.heap.push(value)
    // 上移操作-参数为要插入值的数组的下标
    this.shiftUp(this.heap.length - 1)
  }
}

# 删除堆顶

  • 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构,因为数组的元素都会往前移动);
  • 然后下移:将新堆顶和他的子节点进行交换,直到子节点大于等于这个新堆顶;
  • 大小为 k 的堆中删除堆顶的时间复杂度为 O(logk);
class MinHeap {
  constructor() {
    this.heap = []
  }

  // 获取左侧子节点
  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
  }


  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)
    }
  }

  pop() {
    this.heap[0] = this.heap.pop()
    // 进行下移操作-参数为从指定位置下移的下标
    this.shiftDown(0)
  }
}

# 获取堆顶和堆的大小

  • 获取堆顶:返回数组的头部
  • 获取堆的大小:返回数组的长度
class MinHeap {
  constructor() {
    this.heap = []
  }
  // 获取堆顶元素
  peek() {
    return this.heap[0]
  }

  // 获取堆大小
  size() {
    return this.heap.length
  }
}

整体代码:

class MinHeap {
  constructor() {
    this.heap = []
  }

  getParentIndex(i) {
    // return Math.floor((i - 1) / 2)
    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()
h.insert(3)
h.insert(2)
h.insert(1)

# 使用场景

# 数组中的第K个最大元素

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

https://leetcode-cn.com/problems/kth-largest-element-in-an-array/

  • 解题思路:使用堆来解决;
  • 解题步骤:首先构建一个最小堆,并依次把数组的值插入堆中;当堆的容量超过 k,就删除堆顶;插入结束后,堆顶就是 k 个最大元素;
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */

class MinHeap {
  constructor() {
    this.heap = []
  }

  getParentIndex(i) {
    // return Math.floor((i - 1) / 2)
    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)
        // 堆中元素超过 k
        if (h.size() > k) {
            // 移除堆顶元素
            h.pop();
        }
    });
    return h.peek()
};

# 前 K 个高频元素

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

https://leetcode-cn.com/problems/top-k-frequent-elements/

提示:

  • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
  • 你可以按任意顺序返回答案。

代码:


class MinHeap {
  constructor() {
    this.heap = []
  }

  getParentIndex(i) {
    // return Math.floor((i - 1) / 2)
    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[parentIndex].value > this.heap[index].value) {
      // 交换值
      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[leftIndex].value < this.heap[index].value) {
      // 交换位置
      this.swap(leftIndex, index)
      // 继续下移
      this.shiftDown(leftIndex)
    }

    if (this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value) {
      // 交换位置
      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
  }
}

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    const map = new Map();
    nums.forEach(n => {
        map.set(n, map.has(n) ? map.get(n) + 1 : 1);
    });
    const h = new MinHeap();
    map.forEach((value, key) => {
        h.insert({value, key});
        if (h.size() > k) {
            h.pop();
        }
    });
    return h.heap.map(a => a.key)
};

# 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

链接:https://leetcode-cn.com/problems/merge-k-sorted-lists

  • 解题思路:新链表的下一个节点一定是 k 个链表头中的最小节点;使用堆来解决;
  • 解题步骤:构建一个最小堆,并依次把链表头插入堆中;然后弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中;等堆元素全部弹出,合并工作就完成了。
class MinHeap {
  constructor() {
    this.heap = []
  }

  getParentIndex(i) {
    // return Math.floor((i - 1) / 2)
    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[parentIndex].val > this.heap[index].val) {
      // 交换值
      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[leftIndex].val < this.heap[index].val) {
      // 交换位置
      this.swap(leftIndex, index)
      // 继续下移
      this.shiftDown(leftIndex)
    }

    if (this.heap[rightIndex] && this.heap[rightIndex].val < this.heap[index].val) {
      // 交换位置
      this.swap(rightIndex, index)
      // 继续下移
      this.shiftDown(rightIndex)
    }
  }

  // 插入元素
  insert(value) {
    this.heap.push(value)
    // 上移操作-参数为要插入值的数组的下标
    this.shiftUp(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.shiftDown(0)
    return top
  }

  // 获取堆顶元素
  peek() {
    return this.heap[0]
  }

  // 获取堆大小
  size() {
    return this.heap.length
  }
}

/**
 * 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 res = new ListNode(0);
    let p = res;
    const h = new MinHeap();
    // 将链表中的头部插入到堆中
    lists.forEach(l => {
        if (l) h.insert(l)
    });
    while(h.size()) {
        // 获取所有链表头部
        const n = h.pop();
        p.next = n;
        p = p.next;
        if (n.next) h.insert(n.next);
    }
    return res.next;
};
更新: 12/2/2020, 7:45:29 PM