詞典,顧名思義,就是通過關鍵碼來查詢的結構。二叉搜索樹也可以作為詞典,不過各種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)。