⑩ 数据结构之“堆”

一、理论

1. 堆简介

  • 堆是一种特殊的 完全二叉树
  • 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点

1.1 js中的堆

  • js中通常用数组表示堆
  • 左侧子节点的位置是 2*index+1
  • 右侧子节点的位置是 2*index+2
  • 父节点位置是(index-1)/2

1.2 应用

  • 堆能高效、快速找出最大值和最小值,时间复杂度O(1)
  • 找出第k个最大(小)问题

2. js实现最小堆类

2.1 实现步骤

  • 在类里声明一个数组,用来装元素
  • 主要方法:插入、删除堆顶、获取堆顶、获取堆大小
class MinHeap {
  constructor() {
    this.heap = [];
  }
}
插入
  • 将值插入堆的底部,即数组尾部
  • 然后上移:将该值与父节点交换,知道父节点小于等于该值
  • 大小为k的堆中插入元素的时间复杂度为O(logk)
class MinHeap {
  constructor() {
    this.heap = [];
  }
  swap(i1, i2) {
    const temp = this.heap[i1]
    this.heap[i1] = this.heap[i2]
    this.heap[i2] = temp
  }
  getParentIndex(i) {
    // return Math.floor((i-1)/2);
    return (i - 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(value) {
    this.heap.push(value);
    this.shiftUp(this.heap.length-1);
  }
}
删除堆顶
  • 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
  • 然后下移:将新堆顶和它的子节点进行交换,直到子节点大于等于新堆顶
  • 大小为k的堆中删除堆顶的时间复杂度为O(logk)
class MinHeap {
  constructor() {
    this.heap = [];
  }
  swap(i1, i2) {
    const temp = this.heap[i1]
    this.heap[i1] = this.heap[i2]
    this.heap[i2] = temp
  }
  getLeftIndex(i) {
    return i * 2 + 1
  }
  getRightIndex(i) {
    return i * 2 + 2
  }
  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
  }
}

二、刷题

1. 数组中的第k个最大元素(215)

1.1 题目描述

  • 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素
  • 请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素

1.2 解题思路

  • 看到 第k个最大元素 --> 考虑选择使用最小堆

1.3 解题步骤

  • 构建一个最小堆,并依次把数组的值插入堆中
  • 当堆的容量超过k,删除堆顶
  • 插入结束后,堆顶就是第k个最大元素
function findKthLargest(nums, k) {
  const h = new MinHeap()
  nums.forEach(n => {
    h.insert(n)
    if(h.size() > k) {
      h.pop()
    }
  })
  return h.peek()
}

1.4 时间复杂度 && 空间复杂度

  • 时间复杂度:O(nlogk)
  • 空间复杂度:O(k)

2. 前k个高频元素(347)

2.1 题目描述

  • 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素
  • 可以按 任意顺序 返回答案
直觉解法
function topKFrequent(nums, k) {
  const map = new Map()
  nums.forEach(n => {
    map.set(n, map.has(n) ? map.get(n)+1 : 1)
  })
  const list = [...map].sort((a, b) => b[1] - a[1])
  return list.slice(0, k).map(n => n[0])
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(nlogn)

2.2 解题思路

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

2.3 解题步骤

function topKFrequent(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((val, key) => {
    h.insert({ val, key })
    if(h.size() > k)  {
      h.pop()
    }
  })
  return h.heap.map(a => a.key)
}

2.4 时间复杂度 && 空间复杂度

  • 时间复杂度:O(nlogk)
  • 空间复杂度:O(n)

3. 合并k个排序链表(23)

3.1 题目描述

  • 给你一个链表数组,每个链表都已经按升序排列
  • 请你将所有链表合并到一个升序链表中,返回合并后的链表

3.2 解题思路

输入: lists = [ 1 -> 4 -> 5, 1 -> 3 -> 4, 2 -> 6 ]
输出:[1,1,2,3,4,4,5,6]

  • 新链表的下一个节点一定是k个链表头中的最小节点 -> 考虑选择使用最小堆

3.3 解题步骤

  • 构建最小堆,并依次把链表头插入堆中
  • 弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中
  • 等堆元素全部弹出,合并工作就完成了
function mergeKLists(lists) {
  const res = new ListNode(0)
  const h = new MinHeap()
  let p = res
  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
}

3.4 时间复杂度 && 空间复杂度

  • 时间复杂度:O(nlogk)
  • 空间复杂度:O(k)

三、总结 -- 技术要点

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

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

  • js通常用数组表示堆

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

  • 找出第k个最大(小)问题

posted on 2022-01-26 17:44  pleaseAnswer  阅读(102)  评论(0编辑  收藏  举报