平衡树之 Splay 伸展树

2022年01月17日,第四天

平衡树—— S p l a y Splay Splay 伸展树

伸展树之所以叫做伸展树,是因为其核心操作是伸展 ( s p l a y i n g ) (splaying) (splaying) 。伸展就是把一个结点通过 旋转 调整到某个结点处,一般都是伸展到根结点。

旋转就是 A V L AVL AVL 树中的左旋和右旋,不过在这里似乎有了更高级的名字:左旋 ( Z A G ) (ZAG) (ZAG) 和右旋 ( Z I G ) (ZIG) (ZIG)

伸展树非常灵活,可以用它完成许多别的操作,诸如分裂,合并,区间操作,搞 L C T LCT LCT 等等。

结点信息 和 辅助函数
struct node {
    int l, r;
    int val, size;
    int cnt;		// 当前结点重复次数
}spl[N];			// 内存池
int cnt, root;		// 内存池计数器和根结点编号
inline void newnode (int& now, int val) {
    spl[now = ++ cnt].val = val;
    spl[cnt].size ++;
    spl[cnt].cnt ++;
}
inline void pushup (int now) {
    spl[now].size = spl[spl[now].l].size + spl[spl[now].r].size + spl[now].cnt;
}
旋转函数:左旋 ( Z A G ) (ZAG) (ZAG) 和 右旋 ( Z I G ) (ZIG) (ZIG)
inline void zag (int& now) {
    int r = spl[now].r;
    spl[now].r = spl[r].l;
    spl[r].l = now;
    now = r;
    pushup (spl[now].l), pushup (now);
}
inline void zig (int& now) {
    int l = spl[now].l;
    spl[now].l = spl[l].r;
    spl[l].r = now;
    now = l;
    pushup (spl[now].r), pushup (now);
}
第一个操作:伸展 ( S p l a y i n g ) (Splaying) (Splaying)

这里采用 递归式 s p l a y i n g splaying splaying ,这样可以不用在结点信息里维护父结点编号且便于理解,当然牺牲了一些速度,掌握了 S p l a y Splay Splay 后可以使用迭代写法。

主要思想:传入两个参数,当前结点和终点结点的编号,判断当前结点往下走 1 1 1 步 (单旋转) 还是 2 2 2 步 (双旋转)以及旋转的方式。

void splaying (int x, int& y) { 	// 把 x 伸展到 y 那个位置
    if (x == y) return ;  				// 到达了终点,return
    int &l = spl[y].l, &r = spl[y].r; 	// temp
    if (x == l) zig (y);				// 如果左儿子是终点,那就单旋,右旋
    else if (x == r) zag (y); 			// 如果右儿子是终点,那就单旋,左旋
    else {								// 否则一定是双旋转
        if (spl[x].val < spl[y].val) {
            if (spl[x].val < spl[l].val) 
                splaying (x, spl[l].l), zig(y), zig(y);     	// 右旋右旋
            else splaying (x, spl[l].r), zag(l), zig (y);		// 左旋右旋
        }else {
            if (spl[x].val > spl[r].val) 
                splaying (x, spl[r].r), zag(y), zag(y);			// 左旋左旋
            else splaying (x, spl[r].l), zig(r), zag(y);		// 右旋左旋
        }
    }
}
第二个操作:插入

与二叉搜索树无异,只需要在插入后把插入的结点 s p l a y i n g splaying splaying 到根结点即可。

void insert (int& now, int val) {
    if (! now) newnode (now, val), splaying(now, root);
    else if (val < spl[now].val) insert (spl[now].l, val);
    else if (val > spl[now].val) insert (spl[now].r, val);
    else spl[now].size ++, spl[now].cnt ++, splaying (now, root); // 特判相同的情况
}
第三个操作:删除

先递归找到要删除的结点,将其伸展到根结点,如果根结点的重复次数大于 1 1 1 ,那就直接让重复次数减一就好了,否则如果根结点没有后继,那就直接让根结点变成当前根结点的左儿子就好。

否则的话,我们找到根结点的后继 (在根结点的右子树一直往左走就好),然后将其伸展到根结点的右儿子。根据二叉树的性质可知:此时根结点的右儿子的左儿子一定为空。让其变成根结点,再让根结点变成根结点的右儿子即可。

inline void delnode (int now) {
    splaying (now, root); 			// 将要删除的结点伸展至根结点
    if (spl[now].cnt > 1) spl[now].size --, spl[now].cnt --; // 如果有重复,令重复次数 --
    else if (spl[root].r) {			// 否则如果当前结点有后继
        int p = spl[root].r;
        while (spl[p].l) p = spl[p].l;		// 找到后继
        splaying (p, spl[root].r);			// 将其伸展至根结点右儿子
        spl[spl[root].r].l = spl[root].l;	// 右儿子的左儿子变根结点的左儿子
        root = spl[root].r;					// 根结点变为根结点右儿子
        pushup (root);						// 更新一下 size 信息
    }else root = spl[root].l; 				// 伸展之后没有后继,说明它就是最大的了,那就直接删除
}
void del (int now, int val) {
    if (spl[now].val == val) delnode (now);
    else if (val < spl[now].val) del (spl[now].l, val);
    else del (spl[now].r)
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值