数据结构Splay树(伸展树)

前置知识

二叉树

就是一个长这样的树,树中每个结点都有一个父结点(除了根结点没有父结点)和最多两个子结点,每个结点的左儿子一定比它小,右儿子一定比它大。
在这里插入图片描述
这棵树的先序遍历很容易知道就是:1 2 3 4 5 6 7 (根左右)
我们还可以从另一个角度理解先序遍历:把整棵树映射到 x 轴上,也就是把它压扁也就是这样:
在这里插入图片描述
先序遍历从左到右读出来就可以了

单旋:左旋 / 右旋

口诀:左旋拎右左挂右,右旋拎左右挂左

图示:
在这里插入图片描述
code

void zig(int &p) // 右旋 (以p为根)
{
    int q = tr
.l; // 根结点的左儿子,我们要把这个结点旋到根结点 tr
.l = tr[q].r; tr[q].r = p; p = q; pushup(tr
.r), pushup(p); } void zag(int &p) // 左旋 (以p为根) { int q = tr
.r; // 根结点的右儿子,我们要把这个结点旋到根结点 tr
.r = tr[q].l; tr[q].l = p; p = q; pushup(tr
.l), pushup(p); }

主要内容

结构体

后续代码全部基于下方结构体定义:

struct Node
{
    int l, r; // 分别表示左右儿子
    int val; // 表示当前点权值
    int size; // 表示以当前点为根的子树中有多少结点
    int cnt; // 表示有多少与当前点权值一样的数
}spl[maxn];

双旋(伸展)

假设我们每次都要将最下层的结点挪到根结点,对于三个结点的组合,有以下四种双旋方式,其中对于一字型的调整也叫做同构调整,对于之字形的调整叫做异构调整

zig-zig

在这里插入图片描述
对于这样的一字型,且向左边,我们采用两次右旋

zag-zag

在这里插入图片描述
对于这样的一字型,且向右边,我们采用两次左旋

zig-zag

在这里插入图片描述
对于这样的之字形,大于号形状,我们采用先对蓝色结点右旋,再对根结点左旋

zag-zig

在这里插入图片描述
对于这样的之字形,小于号形状,我们采用先对蓝色结点左旋,再对根结点右旋

code

void splaying(int x, int &y) // 把x挪到y的位置
{
    if (x == y) return;
    int &l = spl[y].l, &r = spl[y].r;
    if (x == l) zig(y); // x是y的左儿子
    else if (x == r) zag(y); // x是y的右儿子
    else // x至少要经过一次双旋才能到y
    {
        if (spl[x].val < spl[y].val) // x在y的左子树
        {
            if (spl[x].val < spl[l].val) // x在y的左儿子的左子树
            {
                splaying(x, spl[l].l); // 先把x旋转到y的左儿子的左儿子
                zig(y), zig(y); // 进行zig-zig双旋
            }
            else // x在y的左儿子的右子树
            {
                splaying(x, spl[l].r); // 先把x旋转到y的左儿子的右儿子
                zag(l), zig(y); // 进行zag-zig双旋
            }
        }
        else // x在y的右子树
        {
            if (spl[x].val < spl[r].val) // x在y的右儿子的左子树
            {
                splaying(x, spl[r].l); // 先把x旋转到y的右儿子的左儿子
                zig(r), zag(y); // 进行zig-zag双旋
            }
            else // x在y的右儿子的右子树
            {
                splaying(x, spl[r].r); // 先把x旋转到y的右儿子的右儿子
                zag(y), zag(y); // 进行zag-zag双旋
            }
        }
    }
}

插入 insert

递归找到该插入的位置,然后再把这个结点splaying到根结点

void insert(int &p, int val)
{
    if (!p) // 如果p结点不存在,创建结点并将其splaying到根结点
        getnode(p, val), splaying(p, root);
    else if (val < spl
.val) insert(spl
.l, val); // 要插入的值比当前结点值小,递归进入左子树 else if (val > spl
.val) insert(spl
.r, val); // 要插入的值比当前结点值大,递归进入右子树 else spl
.size ++ , spl
.cnt ++ , splaying(p, root); // 当前平衡树中已有该值,直接更新 }

删除 remove

先把要删除的结点splaying到根结点,然后:

  • 如果根结点没有右子树(也就是没有后继(也就是根结点就是最大值)),那么直接删掉即可,也就是把根结点变成当前根结点的左儿子
  • 如果根结点有右子树(也就是有后继),就递归找到根结点的后继(也就是从根结点开始,先往右走一步,再一直往左走),把后继splaying到根结点的右儿子,此时这个后继一定没有左子树(因为有左子树,后继就会是左子树里的值),我们要删去根结点,直接让根结点的右儿子的左儿子指向根结点的左儿子即可

code

void remove(int p, int val) // 当前结点为p,要删去值为val的结点
{
    if (spl
.val == val) { splaying(p, root); // 先把要删去的结点splaying到根结点 if (spl
.cnt > 1) spl
.cnt -- , spl
.size -- ; // 要删去的数在树中存在不止一个,更新cnt和size即可 else if (!spl[root].r) root = spl[root].l; // 当前结点没有后继 else // 当前结点有后继 { // 找到当前结点后继q int q = spl[root].r; while (spl[q].l) q = spl[q].l; splaying(q, spl[root].r); // 把后继splaying到根结点的右儿子 spl[spl[root].r].l = spl[root].l; // 后继的左儿子变为根结点的左儿子 root = spl[root].r; // 根结点变为后继 pushup(root); } } else if (val < spl
.val) remove(spl
.l, val); // 待删结点在当前结点左子树 else remove(spl
.r, val); // 待删结点在当前结点右子树 }

根据值查询排名

int getrank_bykey(int val)
{
    int p = root, rank = 1;
    while (p)
    {
        if (spl
.val == val) // 找到值为val的结点 { rank += spl[spl
.l].size; splaying(p, root); // 把当前结点splaying到根结点 break; } if (val <= spl
.val) p = spl
.l; // 待求结点在当前结点的左子树 else // 待求结点在当前结点的右子树 { rank += spl[spl
.l].size + spl
.cnt; // 加上左子树和本身的结点数 p = spl
.r; } } return rank; }

根据排名查询值

int getkey_byrank(int rank)
{
    int p = root;
    while (p)
    {
        if (spl[spl
.l].size < rank && rank <= spl[spl
.l].size + spl
.cnt) // 在这个范围内就是当前结点 { splaying(p, root); // 把当前结点splaying到根结点 break; } else if (rank <= spl[spl
.l].size) p = spl
.l; // 待求结点在当前结点的左子树 else // 待求结点在当前结点的右子树 { rank -= spl[spl
.l].size + spl
.cnt; // 减去左子树和本身的结点数 p = spl
.r; } } return spl
.val; }

完整代码模板

#include <bits/stdc++.h>

using namespace std;

const int maxn = 100010;

struct Node
{
    int l, r; // 分别表示左右儿子
    int val; // 表示当前点权值
    int size; // 表示以当前点为根的子树中有多少结点
    int cnt; // 表示有多少与当前点权值一样的数
}spl[maxn];

int idx; // 记录结点编号开到几
int root; // 记录当前根结点编号是几

void getnode(int &p, int val) // 建立一个编号为p 权值是val的结点
{
    if (!p) p = ++ idx; // 给新结点编号
    spl
.size = spl
.cnt = 1; // 初始化size和cnt spl
.val = val; // 赋初值 } void pushup(int u) { spl[u].size = spl[spl[u].l].size + spl[spl[u].r].size + spl[u].cnt; } void zig(int &p) // 右旋 (以p为根) { int q = spl
.l; spl
.l = spl[q].r; spl[q].r = p; p = q; pushup(spl
.r), pushup(p); } void zag(int &p) // 左旋 (以p为根) { int q = spl
.r; spl
.r = spl[q].l; spl[q].l = p; p = q; pushup(spl
.l), pushup(p); } void splaying(int x, int &y) // 把x挪到y的位置 { if (x == y) return; int &l = spl[y].l, &r = spl[y].r; if (x == l) zig(y); // x是y的左儿子 else if (x == r) zag(y); // x是y的右儿子 else // x至少要经过一次双旋才能到y { if (spl[x].val < spl[y].val) // x在y的左子树 { if (spl[x].val < spl[l].val) // x在y的左儿子的左子树 { splaying(x, spl[l].l); // 先把x旋转到y的左儿子的左儿子 zig(y), zig(y); // 进行zig-zig双旋 } else // x在y的左儿子的右子树 { splaying(x, spl[l].r); // 先把x旋转到y的左儿子的右儿子 zag(l), zig(y); // 进行zag-zig双旋 } } else // x在y的右子树 { if (spl[x].val < spl[r].val) // x在y的右儿子的左子树 { splaying(x, spl[r].l); // 先把x旋转到y的右儿子的左儿子 zig(r), zag(y); // 进行zig-zag双旋 } else // x在y的右儿子的右子树 { splaying(x, spl[r].r); // 先把x旋转到y的右儿子的右儿子 zag(y), zag(y); // 进行zag-zag双旋 } } } } void insert(int &p, int val) { if (!p) // 如果p结点不存在,创建结点并将其splaying到根结点 getnode(p, val), splaying(p, root); else if (val < spl
.val) insert(spl
.l, val); // 要插入的值比当前结点值小,递归进入左子树 else if (val > spl
.val) insert(spl
.r, val); // 要插入的值比当前结点值大,递归进入右子树 else spl
.size ++ , spl
.cnt ++ , splaying(p, root); // 当前平衡树中已有该值,直接更新 } void remove(int p, int val) // 当前结点为p,要删去值为val的结点 { if (spl
.val == val) { splaying(p, root); // 先把要删去的结点splaying到根结点 if (spl
.cnt > 1) spl
.cnt -- , spl
.size -- ; // 要删去的数在树中存在不止一个,更新cnt和size即可 else if (!spl[root].r) root = spl[root].l; // 当前结点没有后继 else // 当前结点有后继 { // 找到当前结点后继q int q = spl[root].r; while (spl[q].l) q = spl[q].l; splaying(q, spl[root].r); // 把后继splaying到根结点的右儿子 spl[spl[root].r].l = spl[root].l; // 后继的左儿子变为根结点的左儿子 root = spl[root].r; // 根结点变为后继 pushup(root); } } else if (val < spl
.val) remove(spl
.l, val); // 待删结点在当前结点左子树 else remove(spl
.r, val); // 待删结点在当前结点右子树 } int getrank_bykey(int val) { int p = root, rank = 1; while (p) { if (spl
.val == val) // 找到值为val的结点 { rank += spl[spl
.l].size; splaying(p, root); // 把当前结点splaying到根结点 break; } if (val <= spl
.val) p = spl
.l; // 待求结点在当前结点的左子树 else // 待求结点在当前结点的右子树 { rank += spl[spl
.l].size + spl
.cnt; // 加上左子树和本身的结点数 p = spl
.r; } } return rank; } int getkey_byrank(int rank) { int p = root; while (p) { if (spl[spl
.l].size < rank && rank <= spl[spl
.l].size + spl
.cnt) // 在这个范围内就是当前结点 { splaying(p, root); // 把当前结点splaying到根结点 break; } else if (rank <= spl[spl
.l].size) p = spl
.l; // 待求结点在当前结点的左子树 else // 待求结点在当前结点的右子树 { rank -= spl[spl
.l].size + spl
.cnt; // 减去左子树和本身的结点数 p = spl
.r; } } return spl
.val; }

例题(待补充)