[譯文]跳錶:一種平衡樹的機率性替代品

跳錶是一種能夠替代平衡樹的數據結構。跳錶追求的是機率性平衡,而不是嚴格平衡。所以,跟平衡二叉樹相比,跳錶的插入刪除操做要簡單得多,執行也更快。算法

二叉樹能夠用來實現字典和有序表等抽象數據結構。在元素隨機插入的場景,二叉樹能夠很好應對。然而,在有序插入的狀況下,二叉樹就退化了(鏈表),性能很是差。若是有辦法對待插入元素進行隨機排列,二叉樹大機率能夠運行良好。大部分狀況下,插入是在線進行的,所以隨機排列並不具備可行性。平衡樹在操做時對樹結構進行調整以知足平衡條件,所以得到理想性能。數據結構

跳錶是一種機率性可行的平衡二叉樹替代數據結構。跳錶經過一個隨機數生成器實現平衡。雖然跳錶最壞狀況下(worst-case)性能也不好,可是沒有任何輸入序列必然會致使最壞狀況發生(這點相似劃分元素(pivot point)隨機選定的快排)。跳錶極度不平衡發生的機率很是低(一個包含250個元素的字典,一次查找須要花3倍指望時間的機率小於百萬分之一)。跳錶平衡機率跟隨機插入的二叉樹差很少,好處是插入順序不要求隨機。dom

實現機率性平衡比嚴格控制平衡要簡單得多。對不少應用來講,跳錶用起來比平衡樹更天然,並且算法更簡單。跳錶算法簡單性意味着更容易實現,並且與平衡樹和自適應樹相比有常數倍數的性能提高。跳錶在空間上也比較高效。平均每一個元素只須要額外耗費個2指針(甚至能夠配置得更低),並不須要在每一個節點上都存與平衡和優先級相關的數據。函數

結構

Paste_Image.png

搜索一個鏈表時,咱們須要遍歷每一個節點(如圖 1a)。若是列表是有序的,偶數節點另存一個指向下一個偶數節點的指針(如圖 1b),咱們只須要檢查最多(n/2)+1個節點(n是鏈表規模)。若是序號爲4的倍數的節點都有一個往前跳4步的節點,那麼最多隻須要檢查(n/4)+2次。若是,序號爲2^i的節點有一個向前跳2^i步的指針,那麼則須要檢查log2 n次了!這種數據結構能夠用來作快速搜索,可是插入和刪除並無可行性。性能

k個前進指針的節點成爲k層節點。若是第2^i個節點有一個向前跳2^i步的指針,那麼每層節點數知足如下關係:第1層有50%的節點;第2層有25%的節點;第3層有12.5%的節點;以此類推。假設每層的比例仍是同樣,可是節點隨機選擇,會怎樣呢(圖 1e)?節點第i個前進指針不嚴格跳2^i步,而是能夠跳任意步。因爲不須要維持特殊條件,插入節點層數隨機生成,插入和刪除只須要作局部修改。極端狀況下,有些層次分佈會致使極差的性能,不過接下來咱們會看到這種狀況很是罕見。這種數據結構在鏈表的基礎上加上額外指針以跳過一些中間節點,所以命名爲跳錶指針

算法

這小節介紹用於搜索插入刪除的算法。搜索操做返回與給定鍵(key)關聯的值(value),鍵不存在時則失敗。插入操做將給定鍵關聯到新的值,若是鍵不存在則插入新的節點。刪除操做刪除給定鍵。另外,相似最小鍵下一鍵這類操做實現起來也很是簡單。code

每一個元素由一個節點表示,層次由節點在插入時隨機選定,與已有元素無關。層次爲i的節點擁有i個前進指針,下標分別是1i。節點不須要存儲層數。選定一個合適的常量MaxLevel,層數在這個範圍內。跳錶的層數時當前全部節點層數的最大值,或者當跳錶爲空是,層數爲1。用一個頭向量存儲從層次1MaxLevel的向前指針。指針高於當前跳錶層數的部分直接指向NILblog

初始化

約定NIL元素,其鍵比全部合法建都大(上限)。跳錶的任意層都以NIL結尾。新的跳錶初始化成層數只有1,而且全部表頭全部前進指針都指向NILtable

查找

查找某個元素時,須要逐層遍歷全部鍵不超過給定鍵的節點。若是當前層前進節點已經不符合條件了,往下一層開始遍歷。當遍歷進行到第1層時,下一個節點就是目標節點(如存在)。ast

Search(list, searchKey)
    x := list->header

    for i := list->level downto 1 do
        while x->forward[i]->key < searchKey do
            x = x->forward[i]

    x := x->forward[1]

    if x->key = searchKey
    then
        return x->value
    else
        return failure

插入/刪除

插入或者刪除節點,只需先執行搜索操做(圖 3),而後視狀況從新拼接。僞代碼以下所示:

Insert(list, searchKey, newValue)
    local update[1..MaxLevel]
    x := list-header

    for i := list->level downto 1 do
        while x->forward[i]->key < searchKey do
            x := x->forward[i]
        update[i] := x

    x := x->forward[i]

    if x->key = searchKey then
        x->value := newValue
    else
        lvl := randomLevel()
        if lvl > list->level then
            for i := list->level+1 to lvl do
                update[i] := list->header
            list->level = lvl
        x := makeNode(lvl, searchKey, value)
        for i := 1 to lvl do
            x->forward[i] = update[i]->forward[i]
            update[i]->forward[i] := x

Paste_Image.png

圖3展現了搜索過程。注意到,搜索的過程當中維護了一個名爲update的向量,在每次降層搜索時更新。搜索完成後,update恰好記錄了各層在操做位置(圖中環)左邊最近的節點:

元素 節點
update[1] 12
update[2] 9
update[3] 6
update[4] 6

若是插入時生成了一個比當前最大層更大的層數,則須要更新跳錶層數而且初始化update向量對應部分。

接下來,看看刪除操做的僞代碼:

Delete(list, searchKey)
    local update[1..MaxLevel]
    x := list-header

    for i := list->level downto 1 do
        while x->forward[i]->key < searchKey do
            x := x->forward[i]
        update[i] := x

    x := x->forward[i]

    if x->key < searchKey then
        for i := 1 to list->level do
            if update[i]->forward[i] != x then break
            update[i]->forward[i] = x->forward[i]

        free(x)

        while list->level > 1 and list->header->forward[list->level] = NIL do
            list->level := list->level - 1

在每次刪除時,須要檢查被刪除節點是不是最大層節點。若是是,須要對跳錶層數作對應調整。

隨機函數

接下來,需啊肯定一個隨機數生成函數,其機率分佈使得第i層中有50%的節點同時數據第i+1層。先拋開具體數值,咱們在討論一個分數p,對於有i層指針的節點中p部分,同時擁有i+1層指針。如下即是一個很是理想的隨機數生成函數,隨機層數生成與跳錶元素及規模無關:

randomLevel()
    lvl := 1
    while random() < p and lvl < MaxLevel do
        lvl := lvl + 1
    return lvl
相關文章
相關標籤/搜索