[Happy DSA] 將已排序的元素序列快速的插入到stl set中

已知一個從小到大已排序的元素序列,如何插入到stl set中最快。

1. stl set內部結構

我們知道stl set內部是用紅黑樹來實現的。紅黑樹是一種平衡二叉查找樹,它有以下4個用來平衡的條件:

  • 每個節點要麼是紅色,要麼是黑色
  • 根節點爲黑色
  • 紅節點的子節點一定是黑色
  • 任一個節點至頁節點的任何路徑上,黑節點的個數相等

所以當插入一個新的非根節點時,它一定要是紅色,是爲了不破壞條件4。所以最難滿足的就是條件3。爲了滿足這個條件3,沒插入一個紅色節點後,都需要進行樹的平衡調整。
回到題目中,要是插入速度最快,就得儘量減少RB樹的平衡調整次數或花銷。

RB樹條件有兩種條件方式,一種就是簡單的將父輩節點的顏色進行調整,譬如紅色變成黑色,黑色變成紅色;另一種就是旋轉並調整顏色,有時不止一次旋轉。簡單的顏色調整是必須的,減少不了。因爲默認都是以紅色來插入一個新節點,不可能所有的節點都是紅色。因此,我們要做的就是儘量減少旋轉的次數,甚至根本不要讓它進行旋轉來調整。

2. 按照什麼順序插入

假如排序的數組元素序列是: 0, 1, 2, 3, 4, 5, 6, 7, 8。共9個元素。
如果瞭解二叉查找樹的特性的話,一個比較穩定的樹,或者說是一個比較方便以後查找元素的二叉樹,應該滿足左右子樹元素個數儘量對稱的佈局。RB樹既然是一個平衡二叉查找樹,那麼它也應該滿足這樣的優勢,可以讓之後的查找比較迅速(不然也不會採用RB樹作爲set的內部實現了)。我們期望的樹的佈局應該是下面這樣的:


既然不要RB樹內部進行旋轉調整,那麼樹的根節點應該最先插入,那就是節點4。節點4是排序序列的最中間的元素。爲了保持樹的左右平衡,我們能想到的就是在左子樹中插入一個樹之後,右子樹也對應的插入一個節點。注意要保證在每插入一個節點後,只允許更改相關父輩節點的顏色,不能進行旋轉調整。同時我們要注意到,左右子樹插入的情況類似於原樹的插入情況,因爲節點4被選出首先插入RB樹,會將排序序列分成2個相等的左右排序子序列。因此我們想當然的期待着節點4左邊的全部節點插入到左子樹中,右邊的全部節點插入到右子樹中。依照這樣的思路,選出4插入之後,選出左邊子序列的中值,然後選出右邊子序列的中值,依次插入。分別是第n/2 => (n/4 => 3*4/n)。如下圖所示:

這裏容易有一個誤區,就是遞歸的插入中間元素,譬如插入4之後,再遞歸插入左半部分的中間元素,之後是右半部分的中間。其實這樣是不對的。插入4之後,插入紅色節點2,之後是紅色節點0/1,這時就得進行右旋轉調整了,因爲0/1的父節點是紅色節點。所以我們必須對稱的插入左右中值元素,以保持RB樹的平衡。

3. 算法驗證及實現

上面插入的順序到底是不是我們的一廂情願呢?我們來驗證一下,還是以前面9個數字序列:0, 1, 2, 3, 4, 5, 6, 7, 8。根據我們前面推導的插入序列,它們應該按照這樣的方式插入:4, 2, 6, 1, 3, 5, 7, 0, 8
a) 插入節點4,標記爲黑,因爲它是根節點。


b) 然後是節點2,和節點6。插入紅色節點2後,它的父節點是黑色節點4,因此不需要任何顏色調整。插入節點6時,父節點4是黑色,跟節點2一樣,不需要調整顏色。

c) 接着是節點1
紅色節點1作爲節點2的左子節點插入,由於父節點2是紅色,而且叔叔節點也是紅色。根據RB樹的調整規則,我們只需要將父輩節點2和節點6反轉爲黑色,將祖父節點4反轉爲紅色。但是最後又得將根節點4標記爲黑色(滿足規則2)。

d) 節點3,5,7
有了前面1的插入,導致現在節點2和節點6都是黑色,節點3,5,7將被直接插入,不做任何調整。

e) 節點0,8的插入
紅色節點0作爲節點1的左子節點插入。由於節點1和節點3都爲紅色,類似於當時節點1的插入,將節點1和節點3反轉爲黑色,將節點2反轉爲紅色。節點8類似於節點1。

高亮標記的那個樹就是RB樹內部的結構。

從節點4到節點8的插入,期間只有顏色調整,沒有任何旋轉調整。因此滿足我們之前的需求。

下面就是序列插入時,RB樹內部結構變化圖:



一個基於BFS的Python代碼的實現(借用隊列):

#insert all elements from l (type is list) to s (type is set)
def insert_set(l,s):
   sorted_l = sorted(l)
   import Queue
   q = Queue.deque()
   begin, end = 0, len(l)
   q.append((begin,end))
   while len(q):
      (begin, end) = q.popleft()
      if begin >= end:
          continue
      mid = (begin + end) /2
      print mid
      s.add(l[mid])
      q.append((begin,mid))
      q.append((mid+1,end))

if __name__ == '__main__':
    l = [0,1,2,3,4,5,6,7,8]
    s = set([])
    insert_set(l,s)



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章