詞典(一) 跳轉表(Skip table)


詞典,顧名思義,就是通過關鍵碼來查詢的結構。二叉搜索樹也可以作為詞典,不過各種BST,如AVL樹、B-樹、紅黑樹、伸展樹,結構和操作比較復雜,而且理論上插入和刪除都需要O(logn)的復雜度。

在詞典中,key和value的地位相同,支持新的循值訪問(call by value)的方式。因為詞典的訪問不再強調關鍵碼的大小次序,因此不屬於CBA式算法的范疇,因而算法的復雜度可以突破CBA算法的界限。循值訪問要求在詞典的內部,數據對象的數值和物理地址建立某種關聯。當然,算法時間復雜度的降低,意味着空間復雜度的上升。介紹兩種典型的詞典,跳轉表(skiptable)和哈希表(hashtable),通過他們的操作復雜度,可以清晰地看到這一點。

詞典

首先,根據詞典需要的功能,定義一個詞典模板類。詞典需要支持的操作,主要是查詢get(),插入put(),刪除remove()。

1 template<typename K, typename V> struct Dictionary
2 {
3     virtual int size() const = 0;
4     virtual bool put(K, V) = 0;
5     virtual V* get(K k) = 0;
6     virtual bool remove(K k) = 0;
7 };

跳轉表

跳轉表的初衷在於,相對於二叉樹更加直觀簡便。它是一種基於鏈表的結構,不同之處在於,節點需要包含上下左右四個方向的指針,查詢和動態操作僅需要O(logn)的時間。跳轉表的總體結構如下圖所示。可以看到,需要用兩個鏈表來構成跳轉表結構,其中,每一個水平鏈表稱為一層(level),縱向鏈表的規模稱為層高,從S0-Sh,元素的數量遞減,最底層的S0包含有表中所有的數據項;同時,同一個數據項可能在幾層都出現,沿縱向組成塔(tower),從而也需要給每一個節點定義上下兩個指針。

可以自然地想到,這種結構會浪費一定的空間,因為有許多不必要的重復詞條,但這也正是跳轉表結構效率的來源,空間換時間。如果每個詞條都有很多重復,不僅接近於鏈表O(n)的效率,更是沒有必要的浪費。因此約定,在Sk中出現的節點,也出現在Sk+1中的概率為1/2,也就是說,總體上,每一層節點只有它下一層節點數量的的一半。

 

為了滿足四個方向都有指針的需求,需要對鏈表進行拓展,水平和豎直方向都可以定義后繼和前驅的,稱為四聯表,下面是四聯表的實現,總體與鏈表思路一致,不過因為跳轉表的插入規則,只定義了after-above插入的方式。

 1 #include"Entry.h"
 2 #define QlistNodePosi(T) QuadlistNode<T>*
 3 template<typename T> struct QuadlistNode
 4 {
 5     T entry;
 6     QlistNodePosi(T) pred; QlistNodePosi(T) succ;
 7     QlistNodePosi(T) above; QlistNodePosi(T) below;
 8     QuadlistNode(T e = T(), QlistNodePosi(T) p = NULL, QlistNodePosi(T) s = NULL,
 9         QlistNodePosi(T) a = NULL, QlistNodePosi(T) b = NULL)
10         :entry(e), pred(p), succ(s), above(a), below(b) {}
11     QlistNodePosi(T) insertAsSuccAbove(T const& e, QlistNodePosi(T) b = NULL);
12 };
13 #include"QuadlistNode.h"
14 template<typename T> class Quadlist
15 {
16 private:
17     int _size;
18     QlistNodePosi(T) header; QlistNodePosi(T) trailer;
19 protected:
20     void init();
21     int clear();
22 public:
23     Quadlist() { init(); }
24     ~Quadlist() { clear(); delete header; delete trailer; }
25     int size() const { return _size; }
26     bool empty() const{ return _size <= 0; }
27     QlistNodePosi(T) first() const { return header->succ; }
28     QlistNodePosi(T) last() const { return trailer->pred; }
29     bool valid(QlistNodePosi(T) p)
30     {
31         return p && (p != header) && (p != trailer);
32     }
33     T remove(QlistNodePosi(T) p);
34     QlistNodePosi(T) insertAfterAbove(T const& e, QlistNodePosi(T) p, QlistNodePosi(T) b = NULL);
35 };
36 template<typename T> void Quadlist<T>::init()
37 {
38     header = new QuadlistNode<T>;
39     trailer = new QuadlistNode<T>;
40     header->succ = trailer;
41     header->pred = NULL;
42     trailer->pred = header;
43     trailer->succ = NULL;
44     header->above = trailer->above = NULL;
45     header->below = trailer->below = NULL;
46     _size = 0;
47 }
48 template<typename T> T Quadlist<T>::remove(QlistNodePosi(T) p)
49 {
50     p->pred->succ = p->succ; p->succ->pred = p->pred;
51     T e = p->entry; delete p;
52     return e;
53 }
54 template<typename T> int Quadlist<T>::clear()
55 {
56     int oldsize = _size;
57     while (_size > 0) remove(header->succ);
58     return oldsize;
59 }

接下來,根據跳轉表的結構,我們選取四聯表作為每層的鏈表,所有的層數組成一個普通鏈表,實現跳轉表的結構:

template<typename K, typename V> class Skiplist :public Dictionary<K, V>, public List<Quadlist<Entry<K, V>>*>
{
protected:
    bool skipSearch(ListNode<Quadlist<Entry<K, V>>*>* &qlist, QuadlistNode<Entry<K, V>>* &p, K& k);
public:
    int size() const { return empty() ? 0 : last()->data->size(); }
    int level() { return List:; size(); }
    bool put(K k, V v);//插入,允許重復故必然成功
    V* get(K k);
    bool remove(K k);
};

跳轉表直接繼承了鏈表和詞典的接口,從而具備兩者的功能。鏈表的每個節點都存儲一個四聯表指針,即每個節點代表了跳轉表中的一層。

查找

查找接口skipSearch(),接受起始層數以及起始節點。get()可以通過調用skipSearch()來獲取關鍵碼對應的值。可以自然地想到,高層的四聯表中節點少,如果能查找到,可以大大減少時間。所以,查找元素的操作,從最上層開始,如果命中,直接返回;如果未找到目標關鍵碼,返回到不大於目標的節點,並轉入下層,繼續向后尋找。

 1 template<typename K, typename V> V* Skiplist<K,V>::get(K k)
 2 {
 3     if (empty()) return NULL;
 4     ListNode<Quadlist<Entry<K, V>>*>* qlist = first();
 5     QuadlistNode<Entry<K, V>>* p = qlist->data->first();
 6     return skipSearch(qlist, p, k) ? &(p->entry.value) : NULL;
 7 }
 8 template<typename K, typename V> bool Skiplist<K, V>::skipSearch(ListNode<Quadlist<Entry<K, V>>*>* &qlist,
 9     QuadlistNode<Entry<K, V>>* &p, K& k)
10 {
11     while (true)
12     {
13         while (p->succ && (p->entry.key <= k)) p = p->succ;
14         p = p->pred;//回撤一步
15         if (p->pred && (p->entry.key == k)) return true;
16         qlist = qlist->succ;
17         if (!qlist->succ) return false;//已經是鏈表的trailer,失敗
18         p = (p->pred) ? p->below : qlist->data->first();//轉到下一層(p已經是頭哨兵需要轉到下一層的頭哨兵)
19     }
20 }

因為有前面1/2概率生長的約定,空間復雜度的期望值應當為2n,總體為O(n)。相對於鏈表,只是增加了一個系數,但是查找時橫向和縱向的復雜度,都可以大大降低。具體證明就忽略了(其實只要簡單的概率論就可以了),可以證明,跳轉表的層數期望E(h)=O(logn),整個查找過程中橫向和縱向跳轉次數均為O(logn)。相對於鏈表,犧牲了少量的空間,換區了時間復雜度的大大提升。

插入

查找操作,首先驗空,若為空插入一個新的四聯表。調用skipSearch()轉到適當的插入位置。因為創建新節點的過程要在最底層開始,所以要轉到最底層,創建一個新的塔底。剩下的任務,就是根據1/2的概率生長,如果要繼續插入,那么找到上一層中的前驅節點,把新節點作為它的水平后繼、以及剛插入節點的垂直后繼插入。

 1 template<typename K, typename V> bool Skiplist<K, V>::put(K k, V v)
 2 {
 3     Entry<K, V> e = Entry<K, V>(k, v);
 4     if (empty()) InsertAsFirst(new Quadlist<Entry<K, V>>);//插入首個Entry(首層)
 5     ListNode<Quadlist<Entry<K, V>>*>* qlist = first();
 6     QuadlistNode<Entry<K, V>>* p = qlist->data->first();
 7     if (skipSearch(qlist, p, k))
 8         while (p->below) p = p->below;
 9     qlist = last();
10     QuadlistNode<Entry<K, V>>* b = qlist->data->insertAfterAbove(e, p);//在最底層上插入新的基座
11     while (rand() & 1)
12     {
13         while (qlist->data->valid(p) && !p->above) p = p->pred;//找到第一個比其高的前驅
14         if (!qlist->data->valid(p))
15         {
16             if (qlist == first())//需要升層而已經是最高層時
17                 InsertAsFirst(new Quadlist<Entry<K, V>>);//新加一層
18             p = qlist->pred->data->first()->pred;//轉至新加層的header
19         }
20         else
21             p = p->above;
22         qlist = qlist->pred;//升層
23         b = qlist->data->insertAfterAbove(e, p, b);
24     }
25     return true;
26 }

這里一定要注意一些情況,比如初始跳轉表為空、尋找上一層前驅時已經為頭哨兵、需要繼續向上層插入而跳轉表層數不足等情況。插入操作,需要進行一次查找,以及在上層尋找前驅的操作,其他的操作均為O(1)復雜度。總體上,插入操作的時間復雜度為O(logn)。

刪除

刪除操作相對於插入要容易一些,同樣進行一次查找,從上而下順次刪除塔即可。刪除完后,自上而下檢查一下本層跳轉表是否為空,清除空層。需要注意的是四聯表的垂直方向,因為刪除總是將同一個關鍵碼的節點刪除,每次刪除操作后整個塔都清空,故不必再格外清除垂直方向的指針了。

 1 template<typename K, typename V> bool Skiplist<K, V>::remove(K k)
 2 {
 3     if (empty()) return false;
 4     ListNode<Quadlist<Entry<K, V>>*>* qlist = first();
 5     QuadlistNode<Entry<K, V>>* p = qlist->data->first();
 6     if (!skipSearch(qlist, p, k)) return false;
 7     do
 8     {
 9         QuadlistNode<Entry<K, V>>* lower = p->below;
10         qlist->data->remove(p);
11         p = lower; qlist = qlist->succ;//記錄,向下深入刪除
12     } while (qlist->succ);
13     while (!empty() && first()->data->empty())//如果Quadlist為空,刪除
14         List::remove(first());
15     return true;
16 }

同樣,跳轉表的刪除操作,總體復雜度也不超過跳轉表層數,即O(logn)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM