和之前介绍的AVL树一样,伸展树也是平衡二叉树中的一种,但是它和AVL的原理不同,相比之下,伸展树的实现更加便捷。这里先回顾一下AVL树的特点。
一、回顾AVL树的特点
AVL树保证静态查找效率的关键技巧是添加约束条件,使在任意时刻(不包含瞬时状态)树的高度始终在一个很低的范围。具体的策略是引入平衡因子的概念,以及在树失衡时,使用AVL自己的一套等效旋转技巧使树恢复平衡。
这种方案具有一定的缺陷:
(a) 借助高度或者平衡因子,所以需要改造元素结构,或者额外封装这一属性。
(b) 插入或删除后的旋转成本高,删除操作后,最多要旋转logn次(一般5次操作发生一次这种情况),所以频繁插入删除成本高。
(c) 单次动态调整后,全树拓扑结构的变化量可能高达longn,这在实际的应用中是很避讳的。
二、伸展树概念
1、相比于AVL树,伸展树适用范围更广,主要源于以下两个主要优势:
(a) 伸展树无需时刻都严格地保持全树的平衡,但却能够在任何足够长的真实操作序列中,保持分摊意义上的高效率;
(b) 伸展树也不需要对基本的二叉树节点结构做任何附加的要求或者改动,不需要记录平衡因子或者高度之类的额外信息。因为其策略是在插入、删除、查找操作中融入双层伸展的技巧,通过这种操作技巧,就能缩减树的高度,并使最可能被访问的节点尽可能地处于根节点附近,所以它对节点内部的数据结构没有任何要求。
2、伸展树策略的出发点:
在实际中,通常在任意数据结构的生命周期内,不仅不同操作的概率极不均衡,而且各操作之间有很强的相关性,并在整体上呈现出极强的规律性,其中最为典型的就是“数据局部性”,包含如下两个方面:
(a) 刚刚被访问过的元素,极有可能在不久之后再次被访问到;
(b) 即将被访问的元素,极有可能就处于不久前刚刚访问过的某个元素附加。
总而言之,伸展树策略的出发点就是认为在实际中,在短时间内一般操作的数据都在局部范围内,不会跨越很远。
3、伸展策略:双层伸展(单层的话最坏情况为O(n),而且不明显缩短分支深度)
双层伸展总共会遇到4总不同的情况:
(a) zig-zig/zag-zag(两种对称情况)
子孙同侧,这种情况的关键是第一次旋转要越级,双层伸展的优势就是靠这个实现的。
(b) zig-zag/zag-zig(两种对称情况)
一个祖先是左孩子,而一个祖先是又孩子。这种情况双旋的效果和逐层单旋及AVL的双旋没什么区别。
(c) zig/zag(最后的一下单旋)
4、插入删除时的技巧:
(a) 插入:
通过search(集成了伸展操作)找到目标节点的直接前续后后继后(已被置于root),以此顶点分裂成两个子树,然后以待插入的节点为根拼接成完整的树。
(b) 删除:
通过search(集成了伸展操作)找到待删除节点e(已被置于root),这时此节点为根,然后以此节点为分裂点分成左右两个子树。在其右子树中执行查找e的操作,这时会返回e的直接后继,再以其为根拼接成完整的树。
5、伸展树的效果:
(a) 每次操作都可以使被操作节点本身(search,insert)或者直接前续,直接后继(search,remove)移动到根部,方便下次使用(类似于自适应列表)。
(b) 可令被访问节点所在的分支以几何级数(等比数列)的速度缩减。
如上图连续两次恶意访问,相应分支的高度均被优化(几何级数)。
尽管在任一时刻伸展树中都可能存在很深的节点,但与含羞草类似,一点这类坏节点被触碰到,经过随后的双层伸展,其对应的分支都会收缩至大致长度折半。于是,即便每次都恶意地访问最底层节点,最坏情况也不会持续发生。可见,伸展树虽不能杜绝最坏情况的发生,却能有效地控制最坏情况发生的频度,从而在分摊意义下保证整体的效率(被证明是O(logn)),而且如果数据的局部性很强,则效率甚至更高。
那么什么时候伸展树会变长呢?
(1) 如果需要插入到最底层,那么会因为查找时伸展,从而高度折半;
(2) 如果需要插入到根,就不会伸展,形成一条更长的“链”,如从小到大依次插入 1、2、...、n,就会退化成一条“链”。
6.综合评价
优点:
(1) 无需记录节点高度或平衡因子,编程实现简单易行(优于AVL树),在不考虑局部性时分摊复杂度O(logn)(与AVL树相当)。
(2) 局部性强,在缓存命中率极高时(即k<<n<<m),效率甚至可以更高O(logk),任何连续的m次查找,都可以在O(mlogk+nlogn)时间内完成。
缺点:
(1) 仍不能保证单次最坏情况的出现,不适用于对单次率敏感的场合(如核电站、医院)。
三、伸展树的实现
伸展树类splay有二叉搜索树bst类继承而来,因为它本身也是二叉搜索树结构(中序遍历非降),主要不同在于其增加了splayNode(),并重写了search(),insert(),remove()操作。
操作 | 功能 | 对象 |
splayNode(binNode<T>* v) | 将节点v伸展至根部(双层伸展) | 伸展树 |
search(const T& e) | 删除指定节点(包含双层伸展操作) | 伸展树 |
insert(const T& e) | 插入指定节点(包含双层伸展操作) | 伸展树 |
remove(const T& e) | 删除指定节点(包含双层伸展操作) | 伸展树 |
splay.h
#pragma once
#include"bst.h"
#define IsRoot(x) (!((x).parent))
#define IsLChild(x) (!IsRoot(x)&&(&(x)==(x).parent->lc))
template<typename T> inline void attachAsLChild(binNode<T>* p, binNode<T>* lc) //内联辅助函数,建立lc作为p左孩子的指针关联
{
p->lc = lc;
if (lc) lc->parent = p;
}
template<typename T> inline void attachAsRChild(binNode<T>* p, binNode<T>* rc) //内联辅助函数,建立lc作为p的右孩子的指针关联
{
p->rc = rc;
if (rc) rc->parent = p;
}
template<typename T> class splay :public bst<T>
{
protected:
binNode<T>* splayNode(binNode<T>* v); //将节点v伸展至根部
public:
binNode<T>* & search(const T& e); //删除指定节点(重写bst中的虚函数)
binNode<T>* insert(const T& e); //插入指定节点(重写bst中的虚函数)
bool remove(const T& e); //删除指定节点(重写bst中的虚函数)
};
template<typename T> binNode<T>* splay<T>::splayNode(binNode<T>* v)
{
if (!v) return nullptr; //若节点不存在则直接返回
binNode<T>* p; binNode<T>* g; //可能p自己都不存在
while ((p=v->parent)&&(g=p->parent)) //如果g和v均存在,则至少可以来一次双层伸展
{
binNode<T>* gp = g->parent; //缓存这个局部树的父亲
if (IsLChild(*v))
{
if (IsLChild(*p))//需要zig/zig旋转
{
attachAsLChild(g, p->rc); attachAsLChild(p, v->rc);
attachAsRChild(p, g); attachAsRChild(v, p);
}
else //需要zig/zag旋转
{
attachAsRChild(g, v->lc); attachAsLChild(p, v->rc);
attachAsRChild(v, p); attachAsLChild(v, g);
}
}
else
{
if (!IsLChild(*p)) //需要zag/zag旋转
{
attachAsRChild(g, p->lc); attachAsRChild(p, v->lc);
attachAsLChild(p, g); attachAsLChild(v, p);
}
else //需要zag/zig旋转
{
attachAsRChild(p, v->lc); attachAsLChild(g, v->rc);
attachAsRChild(v, g); attachAsLChild(v, p);
}
}
//旋转完毕后需要接入到母树上
if (!gp) v->parent = nullptr;
else
(g = gp->lc) ? attachAsLChild(gp, v) : attachAsRChild(gp, v); //虽然此时g的父亲不是gp,但是gp的还是能够通过指针索引到g的
updateHeight(g); updateHeight(p); updateHeight(v);//更新3个顶点的高度
}
//可能单次伸展,也可能不需要伸展
if (p = v->parent) //如果v上层依然存在,则单旋一次
{
if (IsLChild(*v)) //zig
{
attachAsLChild(p, v->rc); attachAsRChild(v, p);
}
else
{
attachAsRChild(p, v->lc); attachAsLChild(v, p);
}
updateHeight(p); updateHeight(v);//更新2个顶点的高度
}
//至此节点v的伸展完毕
v->parent = nullptr;
return v;
}
template<typename T> binNode<T>* & splay<T>::search(const T& e)
{
//不同二叉搜索树的搜索功能+目标节点伸展
binNode<T>* p = searchIn(_root, e, _hot = nullptr);//返回时,返回值指向命中节点或假想的哨兵节点,hot指向其parent
_root = splayNode(p ? p : _hot);
return _root;
}
template<typename T> binNode<T>* splay<T>::insert(const T& e)
{
if (!_root) //空树就直接处理
{
_size++; return _root = new binNode<T>(e);
}
if (search(e)->data == e) return _root; //已经存在就不用插入
//经过search()操作,此时_root变为和e大小最近的点(直接前续,直接后继),下面开始分裂重组
_size++;
binNode<T>* t = _root;
if (e > (t->data)) //e应该在t的右边
{
t->parent = _root = new binNode<T>(e, nullptr, t, t->rc);
if (t->rc) //若t有右孩子
{
t->rc->parent = _root; t->rc = nullptr;
}
}
else //e应该在t的左边
{
t->parent = _root = new binNode<T>(e, nullptr, t->lc, t);
if (t->lc) //若t有左孩子
{
t->lc->parent = _root; t->lc = nullptr;
}
}
updateHeightAbove(t);
return _root;
}
template<typename T> bool splay<T>::remove(const T& e)
{
if (!_root || (search(e)->data = !e)) return false; //如果空树或者待删除的节点不存在,则直接返回
//在经过search()处理后,此时_root只可能为待删除的顶点
binNode<T>* w = _root; //w即为到删除的顶点
if (!(w->lc)) //若w无左孩子,则直接删除即可
{
_root = w->rc; if (_root) _root->parent = nullptr;
}
else if (!(w->rc)) //若w无右孩子,则直接删除即可
{
_root = w->lc; if (_root) _root->parent = nullptr;
}
else //否则w有两个孩子,这时候需要把右子树重新搜索以把w的直接后继伸展上来
{
binNode<T>* lt = w->lc; //缓存w的左子树
lt->parent = nullptr; _root->lc = nullptr;
_root = w->rc; _root->parent = nullptr;
_root = search(e); //搜索右子树.伸展w的直接后继
_root->lc = lt; lt->parent = _root;
}
delete w;
_size--;
if (_root) updateHeight(_root);
return true;
}