早上考了 OOP 感觉还好,只是录屏有点意料之中的问题可能会被批判。
Splay 树
局部性:节点一旦被访问很可能继续被访问。考虑访问一个节点就把节点推送到根方便下次访问。
深度为 d 的节点经过 d 次旋转一定能到达根节点。
对于每次操作,最坏情况需要 O (n) 的时间。
双层伸展:向上追溯两层而不是只考虑一层。每次根据祖孙三代的相对位置进行两次旋转。
LR / RL:和单层伸展相同
LL / RR:上面是单层下面是双层。看到双层是先转爷爷再转爸爸。
L / R:没有祖父,只会在最后一步出现。只需单次旋转。
均摊分析
定义势能:$\Phi (S)=\sum_{v \in S}\Phi (v)=\sum {log (size (v))}=log (size (S))$,越平衡的树势能越小
考查查找操作,在规模固定为 n 的伸展树中查找 v,设 v 深度 d,总共伸展⌈d / 2⌉步, 每一步只有三种情况
总之,均摊复杂度 O (logn) 而且局部性强,如果反复访问任一个子集均摊成本为常数
插入
首先查找 search (e),把 e 的临近者伸展到根。创建新的根节点作为插入节点。看情况当左父亲还是右父亲。
1 2 3 4 5 6 7 8 9
| 0001 template <typename T> BNP<T> Splay<T>::insert( const T& e ) { 0002 if ( !_root ) { _size = 1; return _root = new BinNode<T>( e ); } 0003 search( e ); if ( e == _root->data ) return _root; 0004 if ( _p->data < e ) 0005 { _root = new BinNode<T>( e, nullptr, _p, _p->rc ); _p->rc = nullptr; } 0006 else 0007 { _root = new BinNode<T>( e, nullptr, _p->lc, _p ); _p->lc = nullptr; } 0008 _p->updateHeightAbove(); _size++; return _root; 0009 }
|
删除
先 search (e) 并拿走伸展到根的节点 e。此时在右子树中 search (e) 必然找不到,而会让右子树中最小的节点伸展到右子树根。这个点没有左子树。把这个节点和左子树连接在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 0001 template <typename T> bool Splay<T>::remove( const T& e ) { 0002 search(e); 0003 if ( !_root || e != _root->data ) return false; 0004 BNP<T> L = _root->lc, R = _root->rc; delete _root; _size--; 0005 if ( !R ) { 0006 if ( _root = L ) 0007 _root->parent = nullptr; 0008 } else { 0009 (_root = R)->parent = nullptr; search( e ); 0010 _root->attachLc( L ); _root->updateHeight(); 0011 } 0012 return true; 0013 }
|
B 树
分级存储的现实背景:高速、小容量的存储层级到慢速,大容量的存储层级。需要尽可能利用空间局部性,尽可能访问连续的内容。
把 g 代合并成一个多路节点,存储 2g-1 个关键码,有 2g 个分支
要求内部节点在满载和半载之间,即限制分支数上下限:⌈b / 2⌉=a<=way<=b,称 (a,b) 树
根节点至少两只手,除非节点不够。
理想平衡:所有外部节点都处于同一深度。
每个节点实际都是一个有序向量,内部每一层都满足层次遍历有序:同一个节点内部的分支子树满足左小右大
1 2 3 4 5 6 7 8 9
| 0001 template <typename T> struct BTNode; 0002 template <typename T> using BTNP = BTNode<T>*; 0003 template <typename T> struct BTNode { 0004 BTNP<T> parent; Rank height; 0005 Vector<T> key; Vector<BTNP<T>> way; 0006 BTNode( Rank b, Rank h = 1, BTNP<T> c = nullptr ) 0007 : key(b), way(b+1), parent(nullptr), height(h) 0008 { if (c) { way.insert(c); c->parent = this; } } 0009 };
|
查找
从根节点开始在当前节点中顺序查找,找到关键码就返回;没找到就找到对应的分支继续向下
1 2 3 4 5 6 7
| 0001 template <typename T> BTNP<T> BTree<T>::search( const T& e ) { 0002 for ( _x = _root; ; _x = _x->way[_k+1] ) { 0003 _k = _x->key.search( e ); 0004 if ( (-1 != _k) && (e == _x->key[_k]) ) return _x; 0005 if ( !_x->way[_k+1] ) return nullptr; 0006 } 0007 }
|
性能取决于读磁盘的 IO 树,也就是向下找分支的次数,不超过树高。而最高的 B 树也比 BST 矮的多。
插入
1 2 3 4 5
| 0001 template <typename T> bool BTree<T>::insert( const T& e ) { 0002 if ( search( e ) ) return false; 0003 _x->key.insert( _k+1, e ); _x->way.insert( nullptr ); 0004 _N++; solveOverflow(); return true; 0005 }
|
如果一个节点内部的关键码超了就修复上溢。由于都是在叶子处插入新点,所以外部节点始终处于叶子脚下保持同一高度。
每次发生上溢就分裂,这样保证分支不丢但能让一个节点到上一层去。把中位节点放到上层。而放到上层节点的位置恰好是通向该层分支的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 0001 template <typename T> struct LinkParent { 0002 BTNP<T> parent; LinkParent(BTNP<T> p) : parent(p) {} 0003 void operator() (BTNP<T> x) { if (x) x->parent = parent; } 0004 }; 0005 0006 template <typename T> void BTree<T>::solveOverflow() { 0007 while (_b < _x->way.size()) { 0008 Rank s = _b - _a + 1; 0009 BTNP<T> u = new BTNode<T>(_b, _x->height); 0010 u->key.insert(0, _x->key, s, _b); _x->key.remove(s, _b); 0011 u->way.insert(0, _x->way, s, _b+1); _x->way.remove(s, _b+1); 0012 u->way.traverse(LinkParent<T>(u)); 0013 BTNP<T> p = _x->parent; 0014 if (!p) _root = p = new BTNode<T>(_b, 1 + _x->height, _x); 0015 _k = p->way.find(_x); 0016 p->key.insert(_k, _x->key.remove(s - 1)); 0017 p->way.insert(_k+1, u); u->parent = p; 0018 _x = p; 0019 } 0020 }
|
删除
找对应右分支的左藤蔓终点交换,删除 e 和其下的外部节点。让删除操作都是在最底层进行,维护平衡性。
下溢:
情况 1L: 有一个足够胖的左兄弟,通过旋转向兄弟借关键码。
1 2 3 4 5 6 7 8
| 0010 BTNP<T> ls = (0 < _k) ? p->way[_k-1] : nullptr; 0011 if (ls && (_a < ls->way.size())) { 0012 _x->key.insert(0, p->key[_k-1]); 0013 p->key[_k-1] = ls->key.remove(); 0014 _x->way.insert(0, ls->way.remove()); 0015 if (_x->way[0]) _x->way[0]->parent = _x; 0016 break; 0017 }
|
情况 1R: 找右兄弟借,省略。
情况 2:合并,有可能触发连锁合并,需要 while
1 2 3 4 5 6 7
| 0026 0027 if (ls) rs = _x; else { ls = _x; _k++; } 0028 ls->key.insert(p->key.remove(_k-1)); p->way.remove(_k); 0029 rs->way.traverse(LinkParent<T>(ls)); 0030 ls->way.insert(rs->way); ls->key.insert(rs->key); 0031 delete rs; 0032 _x = p;
|
特判树根,缩小一层:
1 2 3 4 5
| 0004 if (!p) { 0005 if ((_x->key.size() < 1) && (0 < _N)) 0006 { _root = _x->way[0]; _root->parent = nullptr; delete _x; } 0007 break; 0008 }
|
红黑树
并发优势:结构变动小
持久性:拓扑结构稳定
结构
0. 由红黑两种节点构成;1. 树根必定是黑色;2. 外部节点均为黑色;3. 红节点的孩子必定黑色(红节点必有黑父亲;没有同为红色的父子)
4. 从任何节点 x 到 x 的每一个后代 nullptr,经过的黑点数量相等,称为黑高度。
将黑节点提升到和红节点等高处,发现每一棵红黑树都对应一个 (2,4) 树。
将黑节点和红孩子视作关键码并合并,有黑/黑红/红黑/红黑红 四种可能
红黑树也是 BBST:路径总节点数 = 红节点 + 黑节点,而 r<=b,所以 h<=2b-1<2b,黑高度上限 log2 (n + 1),因为 (2,4) 树每个节点都有黑节点,而高度不超过满树。h < 2log2 (n + 1)
复用 BST 的 search 接口。
插入
1 2 3 4 5
| 0001 template <typename T> BNP<T> RedBlack<T>::insert( const T& e ) { 0002 BST::insert( e, 1 ); 0003 if ( _p != _x ) solveDoubleRed(); 0004 return _x; 0005 }
|
双红修正:图中第三种情况违背了条件 3。考虑 x 父亲的兄弟的颜色:
如果 u 是黑色:
旋转 g 点,按照 AVL 的失衡旋转那样,最后一定有红的被转到上面并回复平衡。根染黑把原来的根染红即可。
不能只染色不旋转:这样 g 变成红色 p 变成黑色之后,从 B 树视角,g 和上面的合并,px 在一起,被撕裂了。
u 是红色:
B 树视角相当于发生上溢。发生三次染色但是没有旋转。g 上去之后可能继续发生双红。还是当作行插入的节点继续迭代,直到所有条件都满足了。如果蔓延到根则把根染黑。
仅需 O (logn) 时间,有 O (logn) 次染色和 O (1) 次旋转
删除
删除操作先用 BST 的删除,转到只有一个子树的节点删除。
1 2 3 4 5 6 7 8 9
| 0001 template <typename T> bool RedBlack<T>::remove( const T& e ) { 0002 if ( !BST::remove( e ) ) return false; 0003 if ( !RbtBalanced(_p) ) 0004 if ( IsRed(_r) ) 0005 _r->color = RBT_BLACK; 0006 else 0007 solveDoubleBlack(); 0008 return true; 0009 }
|
我还是没搞清楚为什么要搞一个虚拟子树 k 出来。
如果 x 和 r 其中有一个是红,直接删掉,让 r 继承黑色不会影响黑高度。
双黑修正:删掉 x 并以 r 代之必然失衡。
B 树视角:双黑代表下溢,因为删掉了 B 树格子的代表人,需要旋转借关键码。
x 的兄弟 s,考查 s 和他的孩子。
s 黑色且孩子至少有 1 个红节点:
s 为黑且两个孩子均黑:
B 树视角,p 红的情况,说明 p 依附于上层的某个黑色节点,把 p 拉下来合并不会对上层造成影响。如果要合并的话,s 必须染红才能和 r 在一起。
B 树视角,p 黑说明上层只有 p 一个节点,拉下来会导致上层空了,继续向上传递。
因为在 x 还在的时候是平衡的,所以 r 视作带 2 个黑色权重,现在 p 的黑高度不平衡了。
s 红色:
太绕了。
跳表
逐层逐渐稀疏的列表依次叠放,横向成层,纵向成塔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 0001 template <typename T> struct Tower; 0002 template <typename T> using TowerPosi = LNP<Tower<T>>; 0003 template <typename T> struct Brick { 0004 TowerPosi<T> pred, succ; 0005 Brick( TowerPosi<T> p = nullptr, TowerPosi<T> s = nullptr ) : pred(p), succ(s) {} 0006 }; 0007 #define key(t) ((t)->data.data) 0008 #define tower(t) ((t)->data.tower) 0009 #define height(t) (tower(t).size()) 0010 template <typename T> struct Tower { 0011 T data; Vector<Brick<T>> tower; 0012 Tower( T e = T{} ): data(e) {} 0013 }; 0014 template <typename T> struct Skiplist : public List<Tower<T>> { 0015 TowerPosi<T> _x; 0016 TowerPosi<T> search( const T& ); 0017 TowerPosi<T> insert( const T& ); 0018 bool remove( const T& ); 0019 };
|
设 Sk 中的关键码有 p = 1 / 2 的概率在 Sk + 1 中出现。约定塔高为初始 1,逐渐生长。p 概率能继续生长一层,(1-p) 的概率停止生长。
P (h = k)=(1-p) p^(k-1),E (h)=1/(1-p)=O (1),空间成线性。
插入删除
插入:首先在底部找到对应位置,然后抛硬币盖塔(如果超过左右哨兵高度,哨兵会加盖);寻找同一层的前驱后继,利用 h-1 层的前驱向前摸直到找到第一个高度 > h 的塔,链接。(不变性保证 h-1 层的前驱的高度一定不小于 h)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 0001 template <typename T> TowerPosi<T> Skiplist<T>::insert( const T& e ) { 0002 search( e ); if ( (_x != head) && (e == key(_x)) ) return _x; 0003 TowerPosi<T> pred = _x, succ = _x->succ; 0004 _x = List::insert( _x, Tower<T>( e ) ); 0005 for ( Rank h = 0; !h || (rand()&1); h++ ) { 0006 if ( height(head) < h+1 ) { 0007 tower(head).insert( Brick<T>( nullptr, tail ) ); 0008 tower(tail).insert( Brick<T>( head, nullptr ) ); 0009 } 0010 while ( height( pred ) == h ) 0011 pred = tower( pred )[h - 1].pred; 0012 while ( height( succ ) == h ) 0013 succ = tower( succ )[h - 1].succ; 0014 tower( _x ).insert( Brick<T>( pred, succ ) ); 0015 tower( pred )[h].succ = tower( succ )[h].pred = _x; 0016 } 0017 return _x; 0018 }
|
删除比较简单。
1 2 3 4 5 6 7 8 9 10 11
| 0001 template <typename T> bool Skiplist<T>::remove( const T& e ) { 0002 search( e ); if ( (_x == head) || (e != key(_x)) ) return false; 0003 for ( Rank h = height(_x) - 1; -1 != h; h-- ) { 0004 Brick<T>& b = tower(_x)[h]; 0005 tower(b.pred)[h].succ = b.succ; 0006 tower(b.succ)[h].pred = b.pred; 0007 if ( head == b.pred && b.succ == tail ) 0008 { tower(head).remove(h); tower(tail).remove(h); } 0009 } 0010 List::remove( _x ); return true; 0011 }
|
查找
尽可能在该层向右走,走过了才考虑向下走。为什么是塔顶:因为如果不是塔顶的话,说明下一个塔在更高处还有砖块,那在更高的地方就应该跳到下一个塔了。
1 2 3 4 5 6 7
| 0001 template <typename T> TowerPosi<T> Skiplist<T>::search( const T& e ) { 0002 _x = head; 0003 for ( Rank h = height(_x) - 1; -1 != h; h-- ) 0004 while ( ( tower(_x)[h].succ != tail ) && ( key(tower(_x)[h].succ) <= e ) ) 0005 _x = tower(_x)[h].succ; 0006 return _x == head ? nullptr : _x; 0007 }
|
复杂度:
纵向分析,单个塔到达 k 层的概率是 pk,设有 n 座塔,有塔超过 k 层的概率 <=n * pk,跳表的高度几乎就是 O (logn),于是期望要走 O (logn)
横向分析,由于每一层经过的都是塔顶而且紧邻。同一层紧邻塔顶构成区间的长度记 Y,构成几何分布,长度期望是常数,O (logn) 层就是 O (logn)
于是复杂度 O (logn)