從libuv源碼中學習二叉堆

閱讀本文你需具有知識點node

  1. 二叉查找樹 2.準備紙和筆(本身動手畫一畫,這樣方能真的理解)

1.libuv中如何使用最小二叉堆?

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

2.一、徹底二叉樹

定義是:學習

對於一個樹高爲h的二叉樹,若是其第0層至第h-1層的節點都滿。若是最下面一層節點不滿,則全部的節點在左邊的連續排列,空位都在右邊。這樣的二叉樹就是一棵徹底二叉樹。 以下圖所示:

正由於徹底二叉樹的獨特性質,所以其數據可使用數組來存儲,而不須要使用特有的對象去連接左節點和右節點。由於其左右節點的位置和其父節點位置有這樣的一個計算關係:

k表示父節點的索引位置

left = 2 * k + 1
right = 2 * k + 2
複製代碼

2.二、最小(大)二叉堆

知道了徹底二叉樹,那麼二叉堆的這種神奇的數據結構就是多了一個硬性條件:任意一個結點的鍵值老是小於(大於)或等於其子結點的鍵值。 由於其存儲結構不是使用左右節點互相連接的形式,而是使用簡單的數組,因此稱之爲」堆「,可是基於徹底二叉樹,所以又帶上了」二叉「兩字。

那麼有了上面的特徵,當咱們插入或者刪除某個值的時候,爲了保持二叉堆的特性,因而又出現了一些二叉堆穩定的調整算法(也叫堆化),具體在下面講解。

三、二叉堆的基本操做

搞懂二叉堆的插入和刪除操做,咱們先得掌握兩個基本操做:一個是從頂向下調整堆(bubble down),一個自底向上調整堆(bubble up),兩者的調整分別用於二叉堆的刪除和插入。

3.一、自頂向下調整(堆化)

這個操做其實就是根據父節點的位置,往下尋找符合條件的子節點,不斷地交換直到找到節點大於父節點,示意圖以下:

實現代碼以下:

// 當前節點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) // 自頂向下遞歸依次對子節點建堆
      }
  }

複製代碼

3.二、自底向上調整(建堆)

這種調整是當插入一個新值的時候,爲了保證二叉堆的特性,須要從該新插入的子節點中一步步與父節點判斷,不斷交換位置,直到整個二叉堆知足特性。示意圖以下:

這裏有一個核心問題:倒數第一個分支節點的序號是多少呢?

代碼實現以下:

//建堆
  build() {
    let i = Math.floor(this.heapSize / 2) - 1
    while (i >= 0) {
        // 自底向上調整, 從倒數第一個分支節點開始,自底向上調整,直到全部的節點堆化完畢
        this.max_heapify(i--)
    }
  }
複製代碼

四、插入和刪除

有了上面的兩種操做,插入的刪除的實現就瓜熟蒂落了。只須要這麼調用上面的兩個操做:

4.1 插入操做

//增長一個元素
insert(item) {
    this.list.push(item);
    this.heapSize++
    this.build();
}
複製代碼

4.2 刪除操做

這裏的刪除都是刪除根節點,而後再把最後一個節點的數拿到根節點,以後再自上而下調整整個二叉堆。

//提取最大堆第一個節點並恢復堆爲最大堆
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源碼中學習最小二叉堆,本身實現了一遍用於記錄下

相關文章
相關標籤/搜索