五、平衡二叉树——伸展树Splay

伸展树

由于数据存在局部性原理,刚刚访问过的数据有很大可能在短时间内被再次访问,其相邻结点被访问的可能性也会提高。为了提高局部性查询的效率,伸展树诞生了。

伸展树的原理

伸展树在每次查询完一个数据后就将该数据对应的结点旋转至树根,在旋转的同时保持树的搜索性不变(中序遍历不变)。于是下次在查询相同的数据或是相邻的数据时,时间就会大大缩短。
伸展树在总体上的均摊效率为O(logn),m次查询的复杂度为O(mlogn)

实现细节

由于无需保持整棵树的平衡,伸展树的结点只需要记录左右子树和父结点的信息即可。
为了在后续的操作中将左右子树的操作合并在一个函数中,用数组来记录左右子树的下标。

struct tree
{
    int val, fa;
    int son[2] = {0, 0};
};

伸展树涉及的操作主要有:旋转、伸展、查找、插入、分裂、删除(包含合并操作)

旋转

在这里插入图片描述
在这里插入图片描述
如图,包含左旋和右旋操作,和其他树中的旋转操作相同。
由于采用数组形式来存储子树,因此用一个函数就可以实现左右旋转。

void rotate(int x) //旋转x节点
{
    int y = tr[x].fa, z = tr[y].fa;
    int c = (tr[y].son[0] == x); // x为左子树时c==1,右子树时c==0
    tr[y].son[c ^ 1] = tr[x].son[c];
    tr[tr[x].son[c]].fa = y;
    tr[x].fa = z;
    tr[x].son[c] = y;
    tr[y].fa = x;
    //修改z的子树指向,如果x已经是根了,就不用修改
    if (z) tr[z].son[tr[z].son[1] == y] = x;
}

伸展

有单层伸展和双层伸展两种操作,双层伸展的效率更高且伸展后的树高更小。
这里仅介绍双层伸展。
假设当前结点为x,双层旋转会结合x的父结点y,以及y的父结点z进行相应的操作。
一共可以分为三种情况:

  • y是根结点,此时只要将x进行旋转即可。
    在这里插入图片描述
  • y不是根结点,且x和y相对于其父节点不在同一侧
    在这里插入图片描述
  • y不是根结点,且x和y相对于其父节点在同一侧
    在这里插入图片描述

根据图示可以得到伸展函数的代码,由于rotate函数将左右旋转写在一起了,因此我们只要关注旋转那个点即可,而无需关注旋转的方向。

void splay(int x, int goal) //双层伸展,直到x成为goal的子树,goal==0时将x旋转至树根
{
    int y, z;
    while (tr[x].fa != goal)
    {
        y = tr[x].fa;  //第一层父节点
        z = tr[y].fa;  //第二层父节点
        if (z != goal) //y非根结点
        	//如果两个表达式异或的结果为0,表明两个子树在同一侧
            (tr[z].son[0] == y) ^ (tr[y].son[0] == x) ? rotate(x) : rotate(y);
        rotate(x);
    }
    if (!goal) root = x; // goal为0,将根改为x
}

查找

找到元素后将其旋转至树根。

bool Find(int val)
{
    int x = root;
    while (x)
    {
        if (tr[x].val == val)
        {
            //如果找到了就将x旋转至树根,然后返回true
            splay(x, 0);
            return true;
        }
        //如果x进入了不存在的结点,在while的循环条件判断里会自动退出
        x = tr[x].son[tr[x].val < val];
    }
    return false;
}

插入

void insert(int val) //插入
{
    int x = root;
    while (tr[x].son[tr[x].val < val])
        x = tr[x].son[tr[x].val < val];
    tr[x].son[tr[x].val < val] = ++cnt;
    tr[cnt].fa = x, tr[cnt].val = val;
    splay(cnt, 0); //将插入的结点旋转至树根
}

分裂

将树分裂成两棵树,一棵里全部小于val,一棵全部大于val,只要将val旋转至树根然后取左右子树即可。
分裂操作除了在删除结点时会用到以外,在进行区间操作时也会用到,比如将大于val的全部删除,那么根据val进行分裂即可。

bool split(int val, int &t1, int &t2)
{
    if (Find(val))
    {
        t1 = tr[root].son[0];
        t2 = tr[root].son[1];
        tr[t1].fa = tr[t2].fa = 0;
        return true;
    }
    return false;
}

删除

首先根据要删除的元素值将树进行分裂,分裂后进行合并,合并的操作为:将t1中最大的元素旋转至树根,然后将t2作为其右子树即可。

bool Delete(int val) //删除元素为val的结点
{
    int t1, t2;
    if (split(val, t1, t2))
    {
        //由于合并只在删除结点中会用到,因此和删除函数写在一起
        while (tr[t1].son[1])
            t1 = tr[t1].son[1];
        splay(t1, 0);
        tr[root].son[1] = t2;
        tr[t2].fa = root;
        return true;
    }
    return false;
}

POJ3481 平衡二叉树简单应用 涉及删除 查询 插入 操作

本题共有三种操作:插入一个元素、删除最大的元素、删除最小的元素。
删除元素时将其序号输出。
可用平衡树来实现。

#include <stdio.h>
const int N = 1e5 + 5;
using namespace std;

struct tree
{
    int val, fa, idx;
    int son[2];
    tree() { son[0] = son[1] = 0; }
} tr[N];
int cnt = 0, root = 0;

void rotate(int x)
{
    int y = tr[x].fa, z = tr[y].fa;
    int c = (tr[y].son[0] == x);
    tr[y].son[c ^ 1] = tr[x].son[c];
    tr[tr[x].son[c]].fa = y;
    tr[x].fa = z;
    tr[x].son[c] = y;
    tr[y].fa = x;
    if (z) tr[z].son[tr[z].son[1] == y] = x;
}
void splay(int x, int goal)
{
    int y, z;
    while (tr[x].fa != goal)
    {
        y = tr[x].fa;
        z = tr[y].fa;
        if (z != goal)
            (tr[z].son[0] == y) ^ (tr[y].son[0] == x) ? rotate(x) : rotate(y);
        rotate(x);
    }
    if (!goal) root = x;
}

void insert(int val, int idx)
{
    int x = root;
    while (tr[x].son[tr[x].val < val])
        x = tr[x].son[tr[x].val < val];
    tr[x].son[tr[x].val < val] = ++cnt;
    tr[cnt].fa = x;
    tr[cnt].val = val;
    tr[cnt].idx = idx;
    splay(cnt, 0);
}

//将删除结点、输出结点都写在一起了
void Find(int c) // c=2找最大值 c=3找最小值
{
    c = (c - 2) ^ 1;
    if (root == 0)
        putchar('0');
    else
    {
        int x = root;
        while (tr[x].son[c])
            x = tr[x].son[c];
        splay(x, 0);
        printf("%d", tr[root].idx);
        root = tr[root].son[c ^ 1];
        tr[root].fa = 0;
        //如果树空了,要清空根的左右子树
        if (root == 0) tr[root].son[0] = tr[root].son[1] = 0;
    }
    putchar('\n');
}

int main()
{
    int op, k, p;
    while (~scanf("%d", &op) && op)
    {
        if (op == 1)
        {
            scanf("%d%d", &k, &p);
            insert(p, k);
        }
        else
            Find(op);
    }
    return 0;
}

HDU3487 伸展树进阶应用 区间操作

初始时给定一条长为n的序列:1,2,…,n
本题涉及两种操作:
1、CUT l r pos 将区间[l,r]取出连接在取出后的序列中的第pos个元素后面
2、FLIP l r 将区间[l,r]进行反转
最后输出序列经过m次操作后的结果。
本题弱化了伸展树中元素有序的功能(因为最后序列不是有序的),主要使用伸展树旋转的特性进行操作。
对于CUT操作,首先将l-1旋转至树根,然后将r+1旋转至根的右子树,那么r+1的左子树就是[l,r]区间了。然后将pos旋转至根,将pos+1旋转至右子树,最后将[l,r]区间挂载到pos+1的左子树即可。为了避免越界,还要插入两个虚拟结点-inf和inf,对于伸展树来说,这是区间操作的常用方法。
在这里插入图片描述

对于FLIP操作,由于伸展树没有针对的操作,因此用懒标记的方法。相应的要写标记下传的函数。前半部分操作和CUT一样,将l-1旋转至根,将r+1旋转至根的右子树,然后将r+1的左子树打上懒标记。
为了方便找到第k大的元素,在定义结点时增加一个size属性记录子树的大小。
建议手敲一遍,还是有很多细节需要注意的。

#include <algorithm>
#include <cstdio>
#define inf 0x3f3f3f3f
const int N = 3e5 + 5;
using namespace std;

bool spc = 0;

struct node
{
    int son[2], val, fa, size;
    bool rev;
} tr[N];
int root, cnt;

inline int new_node(int father, int val)
{
    tr[++cnt].fa = father;
    tr[cnt].val = val;
    tr[cnt].son[0] = tr[cnt].son[1] = 0;
    tr[cnt].rev = 0;
    tr[cnt].size = 1;
    return cnt;
}

void pushdown(int x) //懒标记下传
{
    if (tr[x].rev)
    {
        int y;
        tr[x].rev = 0;
        swap(tr[x].son[0], tr[x].son[1]);
        if (y = tr[x].son[0]) tr[y].rev ^= 1;
        if (y = tr[x].son[1]) tr[y].rev ^= 1;
    }
}
void update_size(int x) //更新x点的size
{
    int y;
    tr[x].size = 1;
    if (y = tr[x].son[0]) tr[x].size += tr[y].size;
    if (y = tr[x].son[1]) tr[x].size += tr[y].size;
}

void rotate(int x) //旋转x节点
{
    pushdown(x); //旋转前将x的懒标记下传
    int y = tr[x].fa, z = tr[y].fa;
    int c = (tr[y].son[0] == x); // x为左子树时c==1,右子树时c==0
    tr[y].son[c ^ 1] = tr[x].son[c];
    tr[tr[x].son[c]].fa = y;
    tr[x].fa = z;
    tr[x].son[c] = y;
    tr[y].fa = x;
    //修改z的子树指向,如果x已经是根了,就不用修改
    if (z) tr[z].son[tr[z].son[1] == y] = x;
    //旋转过程中x和y的子树发生了改变,要更新size
    update_size(y);
    update_size(x);
}

void splay(int x, int goal) //双层伸展,直到x成为goal的子树,goal==0时将x旋转至树根
{
    int y, z;
    while (tr[x].fa != goal)
    {
        y = tr[x].fa;  //第一层父节点
        z = tr[y].fa;  //第二层父节点
        if (z != goal) //如果两个表达式异或的结果为0,表明两个子树在同一侧
            (tr[z].son[0] == y) ^ (tr[y].son[0] == x) ? rotate(x) : rotate(y);
        rotate(x);
    }
    if (!goal) root = x; // goal为0,将根改为x
}
int find_kth_node(int k) //找第k大的结点
{
    int x = root, l;
    while (true)
    {
        pushdown(x);
        l = tr[x].son[0];
        if (tr[l].size >= k)
            x = l;
        else if (tr[l].size == k - 1)
            return x;
        else
            x = tr[x].son[1], k -= tr[l].size + 1;
    }
}
void cut(int l, int r, int pos)
{
    int x = find_kth_node(l - 1), y = find_kth_node(r + 1);
    splay(x, 0);
    splay(y, x);
    int tmp = tr[y].son[0];         //暂存要移动的区间子树编号
    tr[y].son[0] = 0;               //删除子树
    update_size(y), update_size(x); //删除了子树,所以要更新size,注意更新顺序,先y后x
    x = find_kth_node(pos), y = find_kth_node(pos + 1);
    splay(x, 0);
    splay(y, x);
    tr[y].son[0] = tmp;
    tr[tmp].fa = y;
    update_size(y), update_size(x);
}
void flip(int l, int r)
{
    int x = find_kth_node(l - 1), y = find_kth_node(r + 1);
    splay(x, 0), splay(y, x);
    tr[tr[y].son[0]].rev ^= 1;
}
int build(int l, int r, int fa) //建树
{
    if (l > r) return 0;
    int mid = (l + r) >> 1;
    int nd = new_node(fa, mid);
    tr[nd].son[0] = build(l, mid - 1, nd);
    tr[nd].son[1] = build(mid + 1, r, nd);
    update_size(nd);
    return nd;
}
void init(int n) //初始化n个结点
{
    cnt = 0;
    tr[0].son[0] = tr[0].son[1] = 0;
    root = new_node(0, -inf);
    int rson = new_node(root, inf);
    tr[root].son[1] = rson;
    tr[rson].son[0] = build(1, n, rson);
    update_size(rson);
    update_size(root);
}
void print(int x) //中序遍历输出结果
{
    pushdown(x);

    if (tr[x].son[0]) print(tr[x].son[0]);
    int val = tr[x].val;
    if (val != inf && val != -inf)
    {
        if (spc)
            putchar(' ');
        else
            spc = 1;
        printf("%d", val);
    }
    if (tr[x].son[1]) print(tr[x].son[1]);
}

int main()
{
    int n, m;
    char op[10];
    while (~scanf("%d%d", &n, &m) && n != -1 && m != -1)
    {
        init(n);
        int l, r, pos;
        while (m--)
        {
            scanf("%s", op);
            if (op[0] == 'C')
            {
                scanf("%d%d%d", &l, &r, &pos);
                //由于插入了虚拟结点,需要将区间左右加一
                cut(l + 1, r + 1, pos + 1);
            }
            else
            {
                scanf("%d%d", &l, &r);
                //由于插入了虚拟结点,需要将区间左右加一
                flip(l + 1, r + 1);
            }
        }
        spc = 0;
        print(root);
        printf("\n");
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值