伸展树
与AVL树类似, 伸展树也是二叉搜索树的一种形式, 伸展树无需保证时刻保持全树的平衡,也不需要像AVL树一样要求记录平衡因子的附加信息
伸展树的提出源于信息访问的局部性(刚被访问过的信息有可能再次被访问,要被访问的元素可能位于刚访问过的元素的附近), 就伸展树而言,可采用刚被访问的元素移至数据列表的前端,从而降低后续的操作时间
简易伸展树的最坏情况
每次使用search,将访问后的元素移至树根节点, 如果采用单次zig/zag旋转(先对该节点的父节点进行旋转,再对祖父进行旋转,每旋转一次,节点上升一层), 如果总节点数为n,那么总的旋转次数将高达O(n*n), 平摊到每个节点,复杂度(O(n))都高于AVL树, 简易伸展并不会改变当前树的拓扑结构
如图:
双层旋转
由于单层旋转将会造成O(n)多,高于AVL树,就此提出改进措施:
进行双层旋转, 每次的旋转不再是从当前节点的parent开始,而是从当前节点的grandpa开始;先对g旋转,再对p旋转,这种旋转方式将在zig-zig,zag-zag下展现效果,如图
对比简易伸展与双层旋转,将会发现双层旋转每进行一次search操作, 那么树的高度将会折半,那么如果访问最坏的节点也不会出现O(n)的复杂度,单次操作可在O(logn)时间内完成, 但是如果旋转方式为zig-zag/zag-zig, 双层旋转并不能改变树高,双层旋转将导致树的拓扑结构发生改变
有一种情况为:可能在最后的双层旋转过程中,v只有parent而没有grandpa,但是这种情况只会在最后出现,并且只出现一次,所以并不会影响树的整体旋转效果,因此双层旋转的方式是有效的
伸展树接口定义
基于二叉平衡搜索树类,重写search()(查找过程也会改变树的拓扑结构,所以有必要进行重写), insert(), remove()接口
伸展树的伸展调整算法:
template<typename T>inline void attachAsLChild(NodePosi p, NodePosi lc){p->lChild=lc;if(lc)lc->parent=p;//为p,lc建立父(左)子的关系,可能lc为null
}
template<typename T>inline void attachAsRChild(NodePosi p, NodePosi rc){p->rChild=rc;if(rc)rc->parent=p;//为p,lc建立父(左)子的关系,可能lc为null
}template<typename T>BinNodePosi* Splay<T>::splay(BinNodePosi* v){//从v的位置开始进行伸展操作if(!v) return null;BinNodePosi* p,g;//v的父亲与祖父while((p=v->parent)&&(g=p->parent)){//自下而上进行双层伸展BinNodePosi* gg=g->parent;if(p->lChild==v)if(g->lChild==p){//zig-zig//忽略具体旋转过程,直接拼接操作attachAsLChild(g,p->rChild);//将g拼接到p上attachAsLChild(p,v->rChild);//将p拼接到v上attachAsRChild(p,g);attachAsRChild(v,p);//操作同上}else{//zig-zagattachAsLChild(p,v->rChild);attachAsRChild(g,v->lChild);attachAsLChild(v,g);attachAsRChild(v,p);}else if(g->RChild==p){//zag-zagattachAsRChild(g,p->lChild);attachAsRChild(p,v->lChild);attachAsLChild(p,g);attachAsLChild(v,p);} else{//zag-zigattachAsRChild(p,v-lChild);attachAsLChild(g,v->rChild);attachAsRChild(v,g);attachAsLChild(v,p);}if(!gg) v->parent=null;//v伸展至最顶端,变为rootelse//曾祖父存在,直接将v作为曾祖父的左孩子或右孩子(g==gg->lChild)?attachAsLChild(gg,v):attachAsRChild(gg,v);updateHeight(g);updateHeight(p);updateHeight(v);}if(p=v->parent){//v旋转到最后还有一个p,需要完成一次单旋if(p-lChild==v){attachAsLChild(p,v->rChild);attachAsRChild(v,p);}else {attachAsRChild(p,v->lChild);attachAsLChild(v,p);}updateHeight(p);updateHeight(v);}v->parent=null; returnv;//v已移至root位置
}
search实现
//在search过程中,将节点移动
template<typename T>BinNodePosi* Splay<T>::search(const T& e){BinNodePosi* p=searchIn(_root,e,_hot=null);//从根节点位置开始查找_root=splay((p?p:root));//无论查找是否成功都将返回与之对应的那个节点或者相似的节点至_root位置return _root;
}
insert实现
//由于查找的过程中就实现了将要找的相似元素移动至_root位置,再将要插入的元素与root位置的节点作比较,节点重连实现插入操作
template<typename T>BinNodePosi* Splay<T>::insert(const T& e){if(!_root) {++_size; return _root= new BinNode<T>(e);}//为空树的情况if(e==search(e)->data) return _root;//元素已存在无需插入++_size; BinNodePosi* t=_root;//创建新节点if(_root->data<e){//插入e时,以roo为分界线包含root,左边部分t变为e的左孩子,右边部分变为e的右孩子t->parent=_root=BinNode<T>(e,null,t,t->rChild);//构造e节点,左右孩子为t,t->rChildif(t->rChild) {t->rChild->parent=_root;t->rChild=null;}//将t的rChild的parent由t改为_root,自身连接位置改为null,防止野指针}else{t->parent=_root=new BinNode<T>(e,null,t->lChild,t);if(t->lChild) {t->lChild->parent=_root;t->lChild=null;}//原理同上}updateHeight(t);//更新祖先高度return _root;//无论e是否在原树中,返回值_root->data总是等于e
}
删除实现
原理与insert一致,当进行完search操作后,位于root位置的则是要删除的元素或者是相似的元素,在root的左子树内查找要删除元素的直接后继succ()(表示二叉树中序遍历序列列表)将其替代即可完成删除(这步操作与平衡搜索树的remove一致),保证树的局部性
template<typename T>bool Splay<T>::remove(const T& e){if(!_root||(e!=search(e)->data)) return false;//查找的节点信息未在树中BinNodePosi* w=_root;//要删除的节点已经伸展至树根位置if(!_root->lChild) {//若无左子树_root=_root->rChild; if(_root) _root->parent=null;}else if(!_root->rChild){//无右子树_root=_root->lChild; if(_root) _root->parent=null;}else{//左右子树都存在BinNodePosi* lTREE=_root->lChild;lTREE->parent=null;_root->lChild=null;//将左子树暂时切除_root=_root->rChild;_root->parent=null;search(w->data);//该查找必定失败,但是也将右子树中的最小节点伸展至root的位置//该过程也可调用直接后继,根据中序遍历在右子树中查找最小节点_root->lChild=lTREE;lTREE->parent=_root;//将左子树重新归位}delete w;_size--;//释放要删除的节点if(_root) updateHeight(_root);//树非空时,更新高度return true;
}
总结:伸展树复杂度与AVL树一致O(logn),局部性较强,平均效率较高, 但是针对最坏情况下的单次操作时任需要O(n), 不适用于对效率敏感的场所