从libuv源码中学习最小二叉堆
阅读本文你需具备知识点:
- 二叉查找树(js版本的简单实现可以参考二叉查找树的简单学习)
- 准备纸和笔(自己动手画一画,这样方能真的理解)
1、libuv如何使用最小二叉堆?
libuv将最小二叉堆的算法应用到了timer上,我们先回顾一下timer
的使用:
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都会往最小二叉堆中插入一条定时器信息,如下:
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;
}
为什么用最小二叉堆呢?因为它永远把最小值放在了根节点,而这里的最小值就是定时器最先到时间点的那一组,所以为了查询效率,采用了这么一种算法:
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版本,我们会一遍讲解理论,一边代码实现。
2、二叉堆的基本概念
首先我们得知道二叉堆的定义:二叉堆是一棵完全二叉树,且任意一个结点的键值总是小于或等于其子结点的键值。
那么什么是完全二叉树(complete binary tree)呢?我们先来看一下关于树的数据结构都有哪些?
2.1、完全二叉树
定义是:
对于一个树高为h的二叉树,如果其第0层至第h-1层的节点都满。如果最下面一层节点不满,则所有的节点在左边的连续排列,空位都在右边。这样的二叉树就是一棵完全二叉树。
如下图所示:
正因为完全二叉树的独特性质,因此其数据可以使用数组来存储,而不需要使用特有的对象去链接左节点和右节点。因为其左右节点的位置和其父节点位置有这样的一个计算关系:
k表示父节点的索引位置
left = 2 * k + 1
right = 2 * k + 2
2.2、最小(大)二叉堆
知道了完全二叉树,那么二叉堆的这种神奇的数据结构就是多了一个硬性条件:任意一个结点的键值总是小于(大于)或等于其子结点的键值。因为其存储结构不是使用左右节点互相链接的形式,而是使用简单的数组,所以称之为”堆“,但是基于完全二叉树,因此又带上了”二叉“两字。
那么有了上面的特征,当我们插入或者删除某个值的时候,为了保持二叉堆的特性,于是又出现了一些二叉堆稳定的调整算法,具体在下面讲解。
3、二叉堆的基本操作
搞懂二叉堆的插入和删除操作,我们先得掌握两个基本操作:一个是从顶向下调整堆(bubble down),一个自底向上调整堆(bubble up),二者的调整分别用于二叉堆的删除和插入。
3.1、自顶向下调整
这个操作其实就是根据父节点的位置,往下寻找符合条件的子节点,不断地交换直到找到节点大于父节点,示意图如下:
实现代码如下:
bubbleDown(array, parentIndex, length) {
// 可以看出这种向下调整的操作是以父节点找子节点的行为
let childIndex = parentIndex * 2 + 1
// parentIndex的值会和子节点、孙子节点等节点进行比较,直到找到属于自己的位置
let temp = array[parentIndex]
while (childIndex < length) {
if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
childIndex += 1
}
// 子节点都比父节点大,此时的位置便是父节点的最终位置
if (temp <= array[childIndex]) {
break
}
array[parentIndex] = array[childIndex]
// 将替换的节点作为父节点,继续遍历下面的子节点
parentIndex = childIndex
childIndex = childIndex * 2 + 1
}
array[parentIndex] = temp
}
3.2、自底向上调整
这种调整是当插入一个新值的时候,为了保证二叉堆的特性,需要从该新插入的子节点中一步步与父节点判断,不断交换位置,直到整个二叉堆满足特性。示意图如下:
代码实现如下:
bubbleUp(array) {
// 可以看出这种向上调整的操作是以子节点找父节点的行为
let childIndex = array.length - 1
let parentIndex = Math.floor((childIndex - 1) / 2)
let temp = array[childIndex]
// 查找父节点大于子节点的话,继续往上
while(childIndex > 0 && array[parentIndex] > temp) {
array[childIndex] = array[parentIndex]
childIndex = parentIndex
parentIndex = Math.floor((parentIndex - 1) / 2)
}
array[childIndex] = temp
}
4、插入和删除
有了上面的两种操作,插入的删除的实现就顺理成章了。只需要这么调用上面的两个操作:
插入操作
insert(newNode) {
// 插入节点操作,放在最后面,然后进行自下而上的操作
this.heap.push(newNode)
this.length++
this.bubbleUp(this.heap)
}
删除操作
这里的删除都是删除根节点,然后再把最后一个节点的数拿到根节点,之后再自上而下调整整个二叉堆。
remove() {
// 二叉堆的删除操作指的是删除根节点,之后自稳定
if (this.length === 1) {
return this.heap[0]
}
const lastEle = this.heap.pop()
const top = this.heap[0]
this.heap[0] = lastEle
this.length--
this.bubbleDown(this.heap, 0, this.length)
return top
}
完整代码展示如下:
const array = [8, 10, 4342, 2, 34, 18, 11, 77, 16, 66]
class MinBinaryHeap {
constructor(array) {
this.heap = [...array]
this.length = this.heap.length
this.buildHeap()
}
buildHeap() {
for(let i = Math.floor(this.length / 2) - 1; i >= 0; i -= 1) {
this.bubbleDown(this.heap, i, this.length - 1)
}
}
bubbleDown(array, parentIndex, length) {
// 可以看出这种向下调整的操作是以父节点找子节点的行为
let childIndex = parentIndex * 2 + 1
// parentIndex的值会和子节点、孙子节点等节点进行比较,直到找到属于自己的位置
let temp = array[parentIndex]
while (childIndex < length) {
if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
childIndex += 1
}
// 找到了有一个节点比父节点大,此时的位置便是父节点的最终位置
if (temp <= array[childIndex]) {
break
}
array[parentIndex] = array[childIndex]
// 将替换的节点作为父节点,继续遍历下面的子节点
parentIndex = childIndex
childIndex = childIndex * 2 + 1
}
array[parentIndex] = temp
}
bubbleUp(array) {
// 可以看出这种向上调整的操作是以子节点找父节点的行为
let childIndex = array.length - 1
let parentIndex = Math.floor((childIndex - 1) / 2)
let temp = array[childIndex]
while(childIndex > 0 && array[parentIndex] > temp) {
array[childIndex] = array[parentIndex]
childIndex = parentIndex
parentIndex = Math.floor((parentIndex - 1) / 2)
}
array[childIndex] = temp
}
insert(newNode) {
// 插入节点操作,放在最后面,然后进行自下而上的操作
this.heap.push(newNode)
this.length++
this.bubbleUp(this.heap)
}
print() {
return this.heap
}
remove() {
// 二叉堆的删除操作指的是删除根节点,之后自稳定
if (this.length === 1) {
return this.heap[0]
}
const lastEle = this.heap.pop()
const top = this.heap[0]
this.heap[0] = lastEle
this.length--
this.bubbleDown(this.heap, 0, this.length)
return top
}
sort() {
const result = []
let i = this.heap.length
while (i > 0) {
result.push(this.remove())
i -= 1
}
return result
}
}
const heap = new MinBinaryHeap(array)
console.log('最小二叉堆是:', heap.print())
heap.insert(5)
console.log('插入5之后的最小二叉堆是:', heap.print())
heap.remove()
console.log('删除根节点之后的最小二叉堆是:', heap.print())
console.log('二叉堆进行堆排序结果:', heap.sort())
console.log(heap.print())
参考
公众号关注一波~
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 从libuv源码中学习最小二叉堆 的内容有疑问,请在下面的评论系统中留言,谢谢。