伸展树

数据结构 -文泉学堂 课程链接

伸展树

宽松平衡、局部性

  • AVL树的平衡准则过于苛刻。
  • 刚被访问过的数据,极有可能很快地再次被访问,推广:下一将要访问的节点极有可能在刚被访问的节点的附近。
  • 连续的m次查找(b>>b = |BBST|),采用AVL供需O(mlogn)的时间,那么我们仿效自适应链表,利用局部性,可以使的访问速度更快吗?

自适应调整

类比链表结构,链表中元素的访问效率取决于元素的位置,即元素越靠前,访问效率越高,那么我们根据局部性设置一条策略。每访问一个元素,便把该元素提到链表最前方。
这样经过一段时间的访问,链表靠前的元素将具有较高的访问频率,这在一定程度上提高了链表的整体访问效率。
对比上述链表,BST顶部元素的访问效率也高于底部的访问效率,我们不妨将BST中访问到元素通过某种方式将它们移动到更靠近树根的位置。

逐层伸展

设置一条策略,节点v一旦被访问,随即转移至树根,达成这一目的的方法无非是反复进行zig和zag旋转,直到v抵达树根。这个过程在视觉山更像是“节点v左右摇摆不断伸展”,因此将这种调整成为伸展。
概括的说,就是使几点v逐层单旋,直至到达树根。
最坏情况:BST退化为链表,每次访问最深的节点。旋转次数呈周期性的算术级数演变,每一周期累计O(n^2),分摊到每一步为O(n),这个效率伸展远低于AVL树的O(logn)。

双层伸展

向上追溯两层,而非一层。
反复考察祖孙三代,g=parent§, p=parent(z),根据他们的相对位置经两次旋转,使v上升两层成为子树根。

  • 子孙异侧
    旋转方式同AVL树双旋再平衡相同,对之字形节点做zigzag或者zagzig操作。

    进一步观察,这种调整方式与逐层伸展等效。
  • 子孙同侧
    但在子孙三代成一字型排列时,即zigzig或者zagzag时,在AVL树中的做法:
    先旋转父节点,再旋转祖父节点。

    而在双层伸展的伸展树中:
    先旋转祖父节点,再旋转父节点。

    这种伸展方式,看似与AVL树的伸展方式没有区别,但对于最坏情况(即树退化为链表),双层伸展会有效的减少树高(接近原树的一半),而对于不断地恶意访问(每次访问最深的叶子节点),树高将持续的降低。
    伸展树的这种调整方法具有折叠效果,一旦访问坏节点,对应路径的长度便会减半,使得我们本需要避免的访问最深节点的最坏情况不会持续发生,树退化成链表的情况也不会持续发生。
    单趟伸展操作,分摊O(logn)的时间。

    若v的深度导致他只有父节点,而没有祖父节点呢,即v的父亲是树根的情况,且每轮调整中最多出现一次。
    此时便视具体形态,做单次旋转。

算法实现

接口
#define BinNodePosi(T) BinNode<T>*   //节点位置
template<typename T>
class Splay: public BST<T>  //由BST派生
{
protected:
    //实现伸展操作
    BinNodePosi(T) splay( BinNodePosi(T) v);
public:
    //与其他BST不同,查找接口也需要重写
    //因为其他BST的查找操作不会改变树的拓扑结构,而伸展树需要将查找到的节点提前
    BinNodePosi(T) & search( const T &e);
    //重写插入、删除接口
    BinNodePosi(T) insert(const T &e);
    bool remove(const T &e);
}
算法实现 伸展操作
template<typename T> BinNodePosi(T) Splay<t>::splay( BinNodePosi(T) v)
{
    if( !v)
        return NULL;
    BinNodePosi(T) p;  //父节点
    BinNodePosi(T) g;  //祖父节点
    while( (p = v->parent) && (g = p->parent) )  //自下而上,反复做双层伸展
    {
        BinNodePosi(T) gg = g->parent;  //每轮之后,v都将以原曾祖父节点为父节点
        if( IsLchild(*v) )
            if(isLChild(*p))
            {/*zig-zig*/}
            else
            {/*zig-zag*/}
        else 
            if(isRChild(*p)
            {/*zag-zag*/}
            else
            {/*zag-zig*/}
        if( !gg) v->parent = NULL;  //若无曾祖父节点,则v为树根,否则,gg应以v为左或右孩子
        else (g==gg->lc)? attachAsLChild(gg, v) : attachAsRChild(gg, v);
        updateHeight(g);
        updateHeight(p);
        updateHeight(v);
    }  //双层伸展结束,必有g==NULL,但p可能非空
    if( p = v->parent)
    {/*若p是根,则额外单旋一次*/}
    v->parent = NULL;
    return v;  //伸展完成,v抵达树根
}

zig-zig情况,类比于3+4重构,我们使用拼接的方式代替旋转。

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*/}
else
    {/*...*/}
算法实现 查找算法

伸展树的查找操作,与常规的BST::search()不同,很可能会改变树的拓扑结构,因此不再属于静态操作。

template<typename T> BinNodePosi(T) & Splay<T>::search(const T &e)
{
    //调用标准BST查找接口定位目标节点
    BinNodePosi(T) p=searchIn(_root, e, _hot=NULL);
    //无论成功与否,最后被访问的节点都将伸展至根
    _root=splay( p ? p : _hot);
    //总是返回根节点,无论成功或者失败,我们都会在树根得到一个等于或者接近目标值的节点
    return _root;
}
算法实现 插入算法

直观方法:调用BST标准插入算法,再将新节点伸展至根,其中首先调用BST::search()。
但我们重写的Splay::search()已经集成了splay()操作,在查找失败后,_hot即是根节点,那我们便可以将搜索后的树做一个拆分,再将待插入节点与拆分的两部分组合。

算法实现 删除算法

直观方法:调用BST标准删除算法,再将_hot伸展至根。
同样的,Splay::search() 查找成功之后,目标节点即是树根,我们不妨就在树根附近完成目标节点的摘除。

具体来说,即是现将根节点释放,然后选择一种方法,拼接两棵子树,可以从右子树中取最小的节点作为新的树根,也可以在左子树中取最大的节点作为新的树根。

综合评价

  • 无需记录节点高度或平衡因子;编程实现简单易行----优于AVL树
    分摊复杂度O(logn)----与AVL树相当
  • 局部性强,缓存命中率极高时(即 k<<n<<m),效率甚至可以更高----自适应的O(logk),任何连续的m次查找,都可在O(mlogk + nlogn)时间内完成,那么在命中率极高时,我们甚至可以将数据集大小只看做k。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3AeWsLwI-1581684070251)(en-resource://database/654:1)]
  • 仍不能保证单次最坏情况的出现,不适用于效率敏感的场合。
  • 复杂度分析稍显复杂。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值