堆与优先队列定义
在二叉树一文中讲到了完全二叉树,在这里复习一下完全二叉树的概念
完全二叉树是只允许最后一层右侧有空的二叉树,其有如下性质:
- 编号为 i 的节点(i=1开始),其左孩子节点编号为 2 ∗ i 2*i 2∗i,其右孩子编号为 2 ∗ i + 1 2*i+1 2∗i+1
若 i 从0开始,则其左孩子节点编号为 2 ∗ i + 1 2*i+1 2∗i+1,其右孩子编号为 2 ∗ i + 2 2*i+2 2∗i+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 1046 最后一块石头的重量
根据题目可知,我们需要一个大顶堆,每次取出两块石头进行操作,最后返回最后一块石头的重量,如果最后没有石头则返回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