你瞭解紅黑樹麼?告訴你一個不一樣的紅黑樹,說點有意思的吧!

一隻會鏟史的貓發表於2021-09-02

先看如下兩個問題:

問題1、紅黑樹的鍵值可以重複麼?
問題2、紅黑樹必須有鍵值麼?

關於紅黑樹的介紹網上非常多,紅黑樹的應用也非常廣泛。問一下度娘,她會告訴你各種各樣的實現方法,C和C++版本都有,linux核心使用的版本也有。程式碼都大同小異,就是插入或刪除時如何修正,如何搞平衡。很多文章圖文並茂、寫實而生動,當你在腦海裡試圖左旋一把,右旋一把搞平衡時,基本也到了精神崩潰的邊緣。

如何維護祖孫三代父、祖父、叔叔以及兄弟間的平衡,如何搞好家庭關係,是個頭疼的問題。如果把紅黑樹比作一個族譜的話,可能開始你是高祖,下個節點插進去後就變成了太宗,隨著族系的繁衍最後你可能變成個哀帝。開始A是B爸爸,過會B又變成A爸爸,甚至是爺爺,叔叔、兄弟,你說亂不亂,燒腦燒腦,氣人不氣人。
套用郭德綱在相聲中對於謙說的話:到了我們們這個年紀,誰是誰爸爸都無所謂了。臺上無大小,臺下立規矩,送給臺上的各種二叉樹。

這裡先介紹一個樸實無華的網站:視覺化的資料結構和演算法教學,非常不錯,裡面有經典資料結構的動態展現,可以將你從各種旋轉中解救出來。如下圖:

說了這麼多回到本文開頭的兩個問題:

問題1:紅黑樹的鍵值可以重複麼?

大部分人可能認為不可以重複,因為重複的鍵值會衝突或沒有現實意義。其實是可以重複的。為了表達對二叉樹的敬意,這裡連續插入多個2。使用上面安利的網站,建立了一個很2的紅黑樹。如下圖:

上面的紅黑樹鍵值都相等,非常不可思議,但它確實是棵紅黑樹。
那麼這顆很2的紅黑樹的現實意義是什麼,能應用到什麼地方?當然有,而且很廣泛,這個地方就是定時器,對於大部分伺服器程式,基本都要實現自己的定時器,從而完成一些特殊的重複性工作,比如nodejs的引擎libuv庫中的定時器,nginx中的定時器、以及redis的鍵值有效期判斷等。。。。

當管理多個定時器時就會存在鍵值相等的節點,也就是到期時間相等的節點。這時候如何判斷誰先執行呢?
下面是libuv定時器實現的部分關鍵程式碼:

// libuv定時器使用回撥函式來比較key的大小,這裡的key就是到期時間timeout
static int timer_less_than(const struct heap_node* ha,
                           const struct heap_node* hb) {
  const uv_timer_t* a;
  const uv_timer_t* b;

  a = container_of(ha, uv_timer_t, heap_node);
  b = container_of(hb, uv_timer_t, heap_node);

  if (a->timeout < b->timeout)
    return 1;
  if (b->timeout < a->timeout)
    return 0;

  /* Compare start_id when both have the same timeout. start_id is
   * allocated with loop->timer_counter in uv_timer_start().
   */
   // 如果兩者過期時間相同,則採用start_id來判斷誰先執行
   // 這個start_id是個自增變數,後加入堆中的定時器的start_id要大於早加入堆中的定時器
  return a->start_id < b->start_id;
}

// 定時器啟動並加入到堆中的函式
int uv_timer_start(uv_timer_t* handle,
                   uv_timer_cb cb,
                   uint64_t timeout,
                   uint64_t repeat) {
  uint64_t clamped_timeout;

  if (uv__is_closing(handle) || cb == NULL)
    return UV_EINVAL;

  if (uv__is_active(handle))
    uv_timer_stop(handle);

  clamped_timeout = handle->loop->time + timeout;
  if (clamped_timeout < timeout)
    clamped_timeout = (uint64_t) -1;

  handle->timer_cb = cb;
  handle->timeout = clamped_timeout;
  handle->repeat = repeat;
  /* start_id is the second index to be compared in timer_less_than() */
  handle->start_id = handle->loop->timer_counter++;

  // 將定時器插入堆中,並使用timer_less_than函式進行堆排序
  heap_insert(timer_heap(handle->loop),
              (struct heap_node*) &handle->heap_node,
              timer_less_than);
  uv__handle_start(handle);

  return 0;
}

libuv使用的是最小堆來儲存和管理多個定時器,在排序的過程中如果發現時間相等的節點(見上面函式 timer_less_than),則採用start_id來比較大小,這個start_id是個自增變數,後加入堆中的定時器的start_id要大於早加入堆中的定時器 。從而來判斷鍵值相等的到期事件誰先執行。

回到上面很2的紅黑樹,如果你仔細觀察這顆樹的建立過程就會發現,對於鍵值相同的節點是有時間順序的,插入晚的預設為大值,放在後面,也就是說紅黑樹自動實現了按時間軸儲存鍵值的功能。即使到期事件相等(鍵值Key相等),我們也可以根據其插入紅黑樹的時間順序來取出最小到期事件去執行。

nginx使用的就是紅黑樹的方式來儲存和管理多個定時器。這裡就不再介紹了,可以問度娘要原始碼分析。

問題2、紅黑樹必須有鍵值麼?

這棵樹也是可以建立的,只不過看上去比上面很2的樹還難理解。
上面libuv的定時器節點大小比較函式 timer_less_than已經告訴我們了,你是可以在比較節點的時候不依賴於key值,在你的插入節點時,通過回撥函式來告訴節點誰是“大”的誰是“小”的,這個大小不是數學意義上的大小,可能是業務上一個邏輯業務的大小。通過一系列多個指標而非單一key,來評估一個節點的在業務上而非數學上的前後順序。比如個人信用的評估,可能要根據多項指標(年齡、工齡、消費記錄等)來計算出一個所謂的“大小”值。

寫的太多了,趕緊上班就此打住,權當拋磚引玉吧。

感謝您的閱讀!

相關文章