算法随笔 — 树结构基础 — 堆与优先队列

堆与优先队列定义

在二叉树一文中讲到了完全二叉树,在这里复习一下完全二叉树的概念

完全二叉树是只允许最后一层右侧有空的二叉树,其有如下性质:

  1. 编号为 i 的节点(i=1开始),其左孩子节点编号为 2 ∗ i 2*i 2i,其右孩子编号为 2 ∗ i + 1 2*i+1 2i+1
    若 i 从0开始,则其左孩子节点编号为 2 ∗ i + 1 2*i+1 2i+1,其右孩子编号为 2 ∗ i + 2 2*i+2 2i+2
  2. 基于第一条性质,完全二叉树可以用数组来表示

完全二叉树示例1
就是通过完全二叉树实现的,通常将堆分为大顶堆和小顶堆

大顶堆

大顶堆
上图为一个简单的大顶堆,其有以下特点:

  1. 任意子顶堆的父节点的值比子节点的都大
  2. 堆只维护上下关系,也就是说次大值在第二层,第三大值可能在第二层或第三层,第四大值可能在2、3、4层,以此类推;换一个角度来说,只有父子间能比较大小,兄弟间比较不了

小顶堆

小顶堆
上图为一个简单的小顶堆,性质类比大顶堆

优先队列

回顾一下队列的性质,队列是一种先进先出的数据结构,最常用的是 push 和 pop 两个方法,而堆也有这两个方法。

不同的是,堆 pop() 出来的元素是整个堆元素中的最值,比如大顶堆依次把数据吐出来之后得到的是一个有序的降序序列,因此堆也被称作优先队列,依据此性质我们可以解决和有关维护最值的问题。


堆的实现方式

上文提到过,根据完全二叉树的性质我们可以将堆用数组的方式实现

  • pop()
    弹出数组首位元素,将原数组的最后一位进行数组头部的部位,然后进行下沉以维护堆的性质
  • push()
    在数组的最后一位推入元素,然后对数组的最后一位进行上浮以维护堆的性质

性质维护

  • 上浮
    找到当前节点的父节点并进行比较,以小顶堆举例,如果当前节点的值比父节点的值小,说明要进行交换(如果条件不符合,退出遍历)
  • 下沉
    找到当前节点的左右子节点,以小顶堆举例,先标记当前节点,然后和左子节点比较,如果左子节点的值比当前节点小则标记该左子节点,如果右子节点的值比标记点小则标记右子节点,最后交换标记节点和当前节点(如果左右子节点都不符合要求,则退出遍历)
class Heap {
   
  public heap: number[]
  private readonly kind: boolean
  constructor(arr: number[], kind: 'big' | 'small') {
   
    this.kind = kind === 'big'
    this.heap = arr

    this.init()
  }

  private init() {
   
    for (let i = 1; i < this.heap.length; i++) this.sortUp(i)
  }

  size() {
   
    return this.heap.length
  }

  peek() {
   
    return this.size() ? this.heap[0] : null
  }

  push(n: number) {
   
    this.heap.push(n)
    this.sortUp(this.heap.length - 1)
  }

  pop() {
   
    if (!this.heap.length) return null
    if (this.heap.length === 1) return this.heap.pop()
    const res = this.heap[0]
    this.heap[0] = this.heap.pop()
    this.sortDown(0)
    return res
  }

  private sortUp(index: number) {
   
    let parentIndex: number
    while (index > 0) {
   
      parentIndex = (index - 1) >> 1
      if (this.kind) {
   
        // 大顶堆
        if (this.heap[parentIndex] < this.heap[index]) this.swap(index, parentIndex)
        else break
      } else {
   
        // 小顶堆
        if (this.heap[parentIndex] > this.heap[index]) this.swap(index, parentIndex)
        else break
      }
      index = parentIndex
    }
  }

  private sortDown(index: number) {
   
    let leftIndex: number, rightIndex: number, target = index
    while (index < this.heap.length - 1) {
   
      leftIndex = (index << 1) + 1
      rightIndex = (index << 1) + 2
      if (this.kind) {
   
        // 大顶堆
        if (leftIndex < this.heap.length && this.heap[leftIndex] > this.heap[target]) target = leftIndex
        if (rightIndex < this.heap.length && this.heap[rightIndex] > this.heap[target]) target = rightIndex
      } else {
   
        // 小顶堆
        if (leftIndex < this.heap.length && this.heap[leftIndex] < this.heap[target]) target = leftIndex
        if (rightIndex < this.heap.length && this.heap[rightIndex] < this.heap[target]) target = rightIndex
      }
      if (target === index) break
      this.swap(target, index)
      index = target
    }
  }

  private swap(i: number, j: number) {
   
    [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]
  }
}

注意
获取父节点的时候代码使用了位运算,以下三种写法效果等同

parenIndex = (index - 1) >> 1
parenIndex = (index - 1) / 2 | 0
parenIndex = Math.floor((index - 1) / 2)

在初始化的时候,初始化操作是从头开始的,因为如果从尾开始,当遇到当前节点和父节点值相等时,就可能会使堆的性质被破坏
错误的初始化
以上代码实现的堆的节点只是一个数字,但是在解决问题的时候,我们通常会使用更加复杂的数据结构,具体见以下习题


习题

在解决以下习题的时候,每次需要用到的堆我都会手动写一遍而不是单纯的复制粘贴。

刚学堆的时候我感觉这是我目前学过最简单的数据结构了,但是实际编写代码的时候出现了不少错误,有很多细节也只有错了之后才发现自己还有那么多细节没有掌握,所以读者希望能掌握这种数据结构的话,建议多写多练

当然有的时候堆并不是题目最优的解决方式,但是本文的习题都会尽量以堆来解决,因为本文的目的是练习堆的使用

leetCode 剑指Offer 40 最小的k个数

看到“最”字,首先想到的就是堆维护最值的性质,所以这道题我们可以建一个小顶堆,前k个弹出的元素就是题目答案

class Heap {
   
  public heap: number[]
  private readonly kind: boolean
  constructor(arr: number[], kind: 'big' | 'small') {
   
    this.kind = kind === 'big'
    this.heap = arr
    
    this.init()
  }
  
  private init() {
   
    for (let i = 1; i < this.heap.length; i++) this.sortUp(i)
  }
  
  push(n: number) {
   
    this.heap.push(n)
    this.sortUp(this.heap.length - 1)
  }
  
  pop() {
   
    const res = this.heap.length ? this.heap[0] : null
    this.heap[0] = this.heap.pop()
    this.sortDown(0)
    return res
  }
  
  private sortUp(index: number) {
   
    let parentIndex: number
    while (index > 0) {
   
      parentIndex = (index - 1) >> 1
      if (this.kind) {
   
        // 大顶堆
        if (this.heap[parentIndex] < this.heap[index]) this.swap(index, parentIndex)
        else break
      } else {
   
        // 小顶堆
        if (this.heap[parentIndex] > this.heap[index]) this.swap(index, parentIndex)
        else break
      }
      index = parentIndex
    }
  }
  
  private sortDown(index: number) {
   
    let leftIndex: number, rightIndex: number, target = index
    while (index < this.heap.length - 1) {
   
      leftIndex = (index << 1) + 1
      rightIndex = (index << 1) + 2
      if (this.kind) {
   
        // 大顶堆
        if (leftIndex < this.heap.length && this.heap[leftIndex] > this.heap[target]) target = leftIndex
        if (rightIndex < this.heap.length && this.heap[rightIndex] > this.heap[target]) target = rightIndex
      } else {
   
        // 小顶堆
        if (leftIndex < this.heap.length && this.heap[leftIndex] < this.heap[target]) target = leftIndex
        if (rightIndex < this.heap.length && this.heap[rightIndex] < this.heap[target]) target = rightIndex
      }
      if (target === index) break
      this.swap(target, index)
      index = target
    }
  }
  
  private swap(i: number, j: number) {
   
    [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]]
  }
}

function getLeastNumbers(arr: number[], k: number): number[] {
   
  const smallHeap = new Heap(arr, 'small')
  const res: number[] = []
  for (let i = 0; i < k; i++) {
   
    res.push(smallHeap.pop())
  }
  return res
}

leetCode剑指offer40结果

leetCode 1046 最后一块石头的重量

leetCode1046题目
根据题目可知,我们需要一个大顶堆,每次取出两块石头进行操作,最后返回最后一块石头的重量,如果最后没有石头则返回0

class BigHeap {
   
  public data: number[]
  constructor(data: number[]) {
   
    this.data = data
    this.init()
  }
  
  init() {
   
    for (let i = 1; i < this.data.length; i++) this.softUp(i)
  }
  
  size() {
   
    return this.data.length
  }
  
  push(val: number) {
   
    this.data.push(val)
    this.softUp(this.data.length - 1)
  }
  
  pop() {
   
    if (!this.data.length) return null
    if (this.data.length === 1) return this.data.pop()
    const res = this.data[0]
    this.data[0] = this.data.pop()
    this.softDown(0)
    return res
  }
  
  softUp(index: number) {
   
    let parentIndex: number
    while (index) {
   
      parentIndex = (index - 1) >> 1
      if (this.data[parentIndex] < this.data[index]) {
   
        this.swap(index, parentIndex)
        index = parentIndex
      } else break
    }
  }
  
  softDown(index: number) {
   
    let leftIndex: number, rightIndex: number, target
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值