[路飞]_优先队列&手撕堆

379 阅读3分钟

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

之前的一篇文章已经介绍过了如何在力扣中使用优先队列。这篇主要讲一下优先队列实现的过程。

队列与优先队列

  • 对比 图片.png
  • 优先队列在逻辑上可以看成是一个堆,实际可以用数组来实现

堆是一种非线性结构,可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组但堆并不一定是完全二叉树

  • 按照堆的特点可以把堆分为大顶堆和小顶堆 图片.png
  • 大顶堆:每个结点的值都大于或等于其左右孩子结点的值 图片.png
  • 小顶堆:每个结点的值都小于或等于其左右孩子结点的值

堆的插入删除操作

堆的插入删除符合队列的性质,在堆尾插入元素,在堆首删除元素

  • 插入调整:在堆尾插入元素,向上调整

插入调整.gif

  • 删除调整:将堆首元素弹出,将堆尾元素放到堆首,然后向下调整

删除调整.gif

堆排序

  1. 将堆顶元素与堆尾元素交换
  2. 将此操作看做是堆顶元素弹出操作
  3. 按照头部弹出以后的策略调整堆
  4. 重复以上操作

堆排序.gif

代码实现堆

  • 实例化
    • arr传入一个数组
    • cmp函数用于比较元素,判断在调整的时候两个元素是否需要交换,比较的对象不一定是元素本身,也可以是对象中的属性,因此比较灵活,比如在实例化堆的时候可以重新定义 cmp = (a, b) => a.x < b.x,此时如果a.x < b.x说明需要交换元素
class Heap {
  constructor(arr = [], cmp = (a, b) => a < b) {
    this.arr = arr // 用数组实现堆
    this.cmp = (a, b) => cmp(a, b) //比较函数
  }

  // 返回堆的长度
  size() {
    return this.arr.length
  }

  // 返回堆顶元素
  top() {
    if (!this.size()) return null
    return this.arr[0]
  }
}
  • 实例化的同时调整堆
  constructor(arr = [], cmp = (a, b) => a < b) {
    this.arr = arr // 用数组实现堆
    this.cmp = (a, b) => cmp(a, b) //比较函数
    this.heapify() // 初始化调整成堆的性质
  }
  // 初始化调整
  heapify() {
    if (this.size() < 2) return
    for (let i = 1; i < this.size(); i++) {
      this.bubbleUp(i) // 遍历每一个元素向上调整
    }
  }
  • 插入删除元素
  // 插入元素
  push(val) {
    this.arr.push(val) // 堆尾插入
    this.bubbleUp(this.size() - 1) // 向上调整
  }
  // 弹出元素
  pop() {
    if (!this.size()) return null
    if (this.size() === 1) return this.arr.pop()
    var top = this.top()
    this.arr[0] = this.arr.pop() // 堆尾元素与堆首交换
    this.bubbleDown(0) // 向下调整
    return top
  }
  • 接下来是重头戏如何向上向下调整,宗旨就是找到需要交换的两个元素的索引值,然后进行交换
    • 向上调整需要比较当前元素父元素
    • 向下调整需要将当前元素分别与左右孩子进行比较
  // 向上调整
  bubbleUp(index) {
    while (index) {
      const parentIndex = Math.floor((index - 1) / 2) // 当前元素的父元素
      if (this.cmp(this.arr[index], this.arr[parentIndex])) { //符合条件就交换
        [this.arr[index], this.arr[parentIndex]] = [this.arr[parentIndex], this.arr[index]]
        index = parentIndex
      } else { 
        // 说明不需要交换已经在正确的位置跳出循环
        break
      }
    }
  }
  
  // 向下调整
  bubbleDown(index) {
    const lastIndex = this.size() - 1 // 堆尾元素
    while (index < lastIndex) { 
      let findIndex = index // 当前元素
      let leftIndex = index * 2 + 1 // 左孩子
      let rightIndex = index * 2 + 2 // 右孩子
      // 与左孩子比较
      if (leftIndex <= lastIndex && this.cmp(this.arr[leftIndex], this.arr[findIndex])) {
        findIndex = leftIndex
      }
      // 与右孩子比较
      if (rightIndex <= lastIndex && this.cmp(this.arr[rightIndex], this.arr[findIndex])) {
        findIndex = rightIndex
      }
      // 如果已经找到需要交换的索引值
      if (findIndex > index) {
        // 交换元素
        [this.arr[findIndex], this.arr[index]] = [this.arr[index], this.arr[findIndex]]
        index = findIndex
      } else { 
        // 说明不需要交换已经在正确的位置跳出循环
        break
      }
    }
  }

完整代码

class Heap {
  constructor(arr = [], cmp = (a, b) => a < b) {
    this.arr = arr // 用数组实现堆
    this.cmp = (a, b) => cmp(a, b) //比较函数
    this.heapify() // 初始化调整成堆的性质
  }

  // 堆的长度
  size() {
    return this.arr.length
  }

  // 返回堆顶元素
  top() {
    if (!this.size()) return null
    return this.arr[0]
  }

  // 初始化调整
  heapify() {
    if (this.size() < 2) return
    for (let i = 1; i < this.size(); i++) {
      this.bubbleUp(i) // 遍历每一个元素向上调整
    }
  }

  // 插入元素
  push(val) {
    this.arr.push(val) // 堆尾插入
    this.bubbleUp(this.size() - 1) // 向上调整
  }

  // 弹出元素
  pop() {
    if (!this.size()) return null
    if (this.size() === 1) return this.arr.pop()
    var top = this.top()
    this.arr[0] = this.arr.pop() // 堆尾元素与堆首交换
    this.bubbleDown(0) // 向下调整
    return top
  }

  // 向上调整
  bubbleUp(index) {
    while (index) {
      const parentIndex = Math.floor((index - 1) / 2) // 当前元素的父元素
      if (this.cmp(this.arr[index], this.arr[parentIndex])) { //符合条件就交换
        [this.arr[index], this.arr[parentIndex]] = [this.arr[parentIndex], this.arr[index]]
        index = parentIndex
      } else { 
        // 说明不需要交换已经在正确的位置跳出循环
        break
      }
    }
  }

  // 向下调整
  bubbleDown(index) {
    const lastIndex = this.size() - 1 // 堆尾元素
    while (index < lastIndex) { 
      let findIndex = index // 当前元素
      let leftIndex = index * 2 + 1 // 左孩子
      let rightIndex = index * 2 + 2 // 右孩子
      // 与左孩子比较
      if (leftIndex <= lastIndex && this.cmp(this.arr[leftIndex], this.arr[findIndex])) {
        findIndex = leftIndex
      }
      // 与右孩子比较
      if (rightIndex <= lastIndex && this.cmp(this.arr[rightIndex], this.arr[findIndex])) {
        findIndex = rightIndex
      }
      // 如果已经找到需要交换的索引值
      if (findIndex > index) {
        // 交换元素
        [this.arr[findIndex], this.arr[index]] = [this.arr[index], this.arr[findIndex]]
        index = findIndex
      } else { 
        // 说明不需要交换已经在正确的位置跳出循环
        break
      }
    }
  }
}

算法题应用

692. 前K个高频单词

/**
* @param {string[]} words
* @param {number} k
* @return {string[]}
*/
var topKFrequent = function (words, k) {
 var map = new Map()
 // 记录单词出现的次数
 words.forEach(item => {
   map.has(item) ? map.set(item, map.get(item) + 1) : map.set(item, 1)
 })
 // 维护一个K个元素的小顶堆,这K个元素就是出现频率最高的
 var mapArr = [...map]
 var heap = new Heap(mapArr, (a, b) => {
   if(a[1] > b[1]){
     return a[1] > b[1]
   }else if(a[1] === b[1]){
     return a[0] < b[0]
   }
 })
 
 var temp = []
 while (k) {
   temp.push(heap.pop())
   k--
 }

 var res = []
 temp.forEach(item => {
   res.push(item[0])
 })
 return res
};

--- end ---