閱讀本文你需具有知識點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源碼中學習最小二叉堆,本身實現了一遍用於記錄下