阅读本文你需具有知识点node
- 二叉查找树 2.准备纸和笔(本身动手画一画,这样方能真的理解)
libuv将最小二叉堆的算法应用到了timer上,咱们看一下timer的使用:git
uv_timer_t timer_handle;
r = uv_timer_init(loop, &timer_handle);
// 每10秒钟调用定时器回调一次
r = uv_timer_start(&timer_handle, timer_cb, 10 * 1000, 10 * 1000);
复制代码
当咱们每调用一次uv_timer_start
的时候,libuv都会往最小二叉堆中插入一条定时器信息,以下:github
int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat) {
... ...
heap_insert(timer_heap(handle->loop),
(struct heap_node*) &handle->heap_node,
timer_less_than);
... ...
}
复制代码
当调用uv_timer_stop
的时候,libuv都会删除一条定时器信息:算法
int uv_timer_stop(uv_timer_t* handle) {
if (!uv__is_active(handle))
return 0;
heap_remove(timer_heap(handle->loop),
(struct heap_node*) &handle->heap_node,
timer_less_than);
uv__handle_stop(handle);
return 0;
}
复制代码
为何用最小二叉堆呢? 由于它永远把最小值放在了根节点,而这里的最小值就是定时器最早到时间点的那一组,因此为了查询效率,采用了这么一种算法:api
void uv__run_timers(uv_loop_t* loop) {
... ...
for (;;) {
heap_node = heap_min(timer_heap(loop));
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout > loop->time)
break;
... ...
}
}
复制代码
libuv的最小二叉堆的实现源码在这里:heap-inl.h数组
接下去,咱们开始从libuv的源码中学习最小二叉堆的知识,为了让你们不至于那么陌生,将C语言实现版本转换为Js版本,咱们会一遍讲解理论,一边代码实现。数据结构
首先咱们得知道二叉堆的定义:二叉堆是一棵彻底二叉树,且任意一个结点的键值老是小于或等于其子结点的键值。less
那么什么是彻底二叉树(complete binary tree)呢?咱们先来看一下关于树的数据结构都有哪些?oop
定义是:学习
对于一个树高为
h
的二叉树,若是其第0层至第h-1
层的节点都满。若是最下面一层节点不满,则全部的节点在左边的连续排列,空位都在右边。这样的二叉树就是一棵彻底二叉树。 以下图所示:
正由于彻底二叉树的独特性质,所以其数据可使用数组来存储,而不须要使用特有的对象去连接左节点和右节点。由于其左右节点的位置和其父节点位置有这样的一个计算关系:
k表示父节点的索引位置
left = 2 * k + 1
right = 2 * k + 2
复制代码
知道了彻底二叉树,那么二叉堆的这种神奇的数据结构就是多了一个硬性条件:任意一个结点的键值老是小于(大于)或等于其子结点的键值。 由于其存储结构不是使用左右节点互相连接的形式,而是使用简单的数组,因此称之为”堆“,可是基于彻底二叉树,所以又带上了”二叉“两字。
那么有了上面的特征,当咱们插入或者删除某个值的时候,为了保持二叉堆的特性,因而又出现了一些二叉堆稳定的调整算法(也叫堆化),具体在下面讲解。
搞懂二叉堆的插入和删除操做,咱们先得掌握两个基本操做:一个是从顶向下调整堆(bubble down),一个自底向上调整堆(bubble up),两者的调整分别用于二叉堆的删除和插入。
这个操做其实就是根据父节点的位置,往下寻找符合条件的子节点,不断地交换直到找到节点大于父节点,示意图以下:
实现代码以下:
// 当前节点i的堆化过程
max_heapify(i) {
const leftIndex = 2 * i + 1 // 左节点
const rightIndex = 2 * i + 2 // 右节点
let maxIndex = i // 当前节点i
// 若是有子节点数据大于本节点那么就进行交换
if (leftIndex < this.heapSize && this.list[leftIndex] > this.list[maxIndex]) {
maxIndex = leftIndex
}
if (rightIndex < this.heapSize && this.list[rightIndex] > this.list[maxIndex]) {
maxIndex = rightIndex
}
if (i !== maxIndex) {
swap(this.list, maxIndex, i) // maxIndex子节点与当前节点位置交换
// 自顶向下调整
this.max_heapify(maxIndex) // 自顶向下递归依次对子节点建堆
}
}
复制代码
这种调整是当插入一个新值的时候,为了保证二叉堆的特性,须要从该新插入的子节点中一步步与父节点判断,不断交换位置,直到整个二叉堆知足特性。示意图以下:
这里有一个核心问题:倒数第一个分支节点的序号是多少呢?
代码实现以下:
//建堆
build() {
let i = Math.floor(this.heapSize / 2) - 1
while (i >= 0) {
// 自底向上调整, 从倒数第一个分支节点开始,自底向上调整,直到全部的节点堆化完毕
this.max_heapify(i--)
}
}
复制代码
有了上面的两种操做,插入的删除的实现就瓜熟蒂落了。只须要这么调用上面的两个操做:
//增长一个元素
insert(item) {
this.list.push(item);
this.heapSize++
this.build();
}
复制代码
这里的删除都是删除根节点,而后再把最后一个节点的数拿到根节点,以后再自上而下调整整个二叉堆。
//提取最大堆第一个节点并恢复堆为最大堆
extract() {
if (this.heapSize === 0) return null
const item = this.list[0]
swap(this.list, 0, this.heapSize - 1)
this.heapSize--
this.max_heapify(0)
return item
}
复制代码
完整代码展现以下:
/** * 数组元素交换 * @param {*} A * @param {*} i * @param {*} j */
function swap(A, i, j) {
const t = A[i]
A[i] = A[j]
A[j] = t
}
/** * 最大堆 */
class MaxHeap {
constructor(data) {
this.list = [...data]
for (let i = 0; i < data.length; i++) {
this.list[i] = data[i]
}
this.heapSize = data.length
this.build()
}
//建堆
build() {
let i = Math.floor(this.heapSize / 2) - 1
while (i >= 0) {
// 自底向上调整, 每一个节点一个循环
this.max_heapify(i--)
}
}
//最大[堆化]
max_heapify(i) {
const leftIndex = 2 * i + 1
const rightIndex = 2 * i + 2
let maxIndex = i
if (leftIndex < this.heapSize && this.list[leftIndex] > this.list[maxIndex]) {
maxIndex = leftIndex
}
if (rightIndex < this.heapSize && this.list[rightIndex] > this.list[maxIndex]) {
maxIndex = rightIndex
}
if (i !== maxIndex) {
swap(this.list, maxIndex, i)
// 自顶向下调整
this.max_heapify(maxIndex)
}
}
//提取最大堆第一个节点并恢复堆为最大堆
extract() {
if (this.heapSize === 0) return null
const item = this.list[0]
swap(this.list, 0, this.heapSize - 1)
this.heapSize--
this.max_heapify(0)
return item
}
//增长一个元素
insert(item) {
this.list.push(item);
this.heapSize++
this.build();
}
print() {
return JSON.stringify(this.list)
}
size() {
return this.list.length;
}
// 堆排序
sort() {
const result = []
let i = this.heapSize
while ( i > 0) {
result.push(heap.extract())
i--
}
return result
}
}
const array = [12, 15, 2, 4, 3, 8, 7, 6, 5]
const heap = new MaxHeap(array)
console.log('最大二叉堆是:', heap.print())
heap.insert(9)
console.log('插入9以后的最大二叉堆是:', heap.print())
heap.extract()
console.log('删除根节点以后的最大二叉堆是:', heap.print())
console.log('二叉堆进行堆排序结果:', heap.sort())
console.log(heap.print())
复制代码
参考 本文主要是参考从libuv源码中学习最小二叉堆,本身实现了一遍用于记录下