【数据结构】Splay树(伸展树)

news/2025/4/1 3:38:26/

前置知识

二叉树

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

单旋:左旋 / 右旋

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

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

void zig(int &p) // 右旋 (以p为根)
{int q = tr[p].l; // 根结点的左儿子,我们要把这个结点旋到根结点tr[p].l = tr[q].r;tr[q].r = p;p = q;pushup(tr[p].r), pushup(p);
}void zag(int &p) // 左旋 (以p为根)
{int q = tr[p].r; // 根结点的右儿子,我们要把这个结点旋到根结点tr[p].r = tr[q].l;tr[q].l = p;p = q;pushup(tr[p].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[p].val) insert(spl[p].l, val); // 要插入的值比当前结点值小,递归进入左子树else if (val > spl[p].val) insert(spl[p].r, val); // 要插入的值比当前结点值大,递归进入右子树else spl[p].size ++ , spl[p].cnt ++ , splaying(p, root); // 当前平衡树中已有该值,直接更新
}

删除 remove

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

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

code

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

根据值查询排名

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

根据排名查询值

int getkey_byrank(int rank)
{int p = root;while (p){if (spl[spl[p].l].size < rank && rank <= spl[spl[p].l].size + spl[p].cnt) // 在这个范围内就是当前结点{splaying(p, root); // 把当前结点splaying到根结点break;}else if (rank <= spl[spl[p].l].size) p = spl[p].l; // 待求结点在当前结点的左子树else // 待求结点在当前结点的右子树{rank -= spl[spl[p].l].size + spl[p].cnt; // 减去左子树和本身的结点数p = spl[p].r;}}return spl[p].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[p].size = spl[p].cnt = 1; // 初始化size和cntspl[p].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[p].l;spl[p].l = spl[q].r;spl[q].r = p;p = q;pushup(spl[p].r), pushup(p);
}void zag(int &p) // 左旋 (以p为根)
{int q = spl[p].r;spl[p].r = spl[q].l;spl[q].l = p;p = q;pushup(spl[p].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[p].val) insert(spl[p].l, val); // 要插入的值比当前结点值小,递归进入左子树else if (val > spl[p].val) insert(spl[p].r, val); // 要插入的值比当前结点值大,递归进入右子树else spl[p].size ++ , spl[p].cnt ++ , splaying(p, root); // 当前平衡树中已有该值,直接更新
}void remove(int p, int val) // 当前结点为p,要删去值为val的结点
{if (spl[p].val == val){splaying(p, root); // 先把要删去的结点splaying到根结点if (spl[p].cnt > 1) spl[p].cnt -- , spl[p].size -- ; // 要删去的数在树中存在不止一个,更新cnt和size即可else if (!spl[root].r) root = spl[root].l; // 当前结点没有后继else // 当前结点有后继{// 找到当前结点后继qint 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[p].val) remove(spl[p].l, val); // 待删结点在当前结点左子树else remove(spl[p].r, val); // 待删结点在当前结点右子树
}int getrank_bykey(int val)
{int p = root, rank = 1;while (p){if (spl[p].val == val) // 找到值为val的结点{rank += spl[spl[p].l].size;splaying(p, root); // 把当前结点splaying到根结点break;}if (val <= spl[p].val) p = spl[p].l; // 待求结点在当前结点的左子树else // 待求结点在当前结点的右子树{rank += spl[spl[p].l].size + spl[p].cnt; // 加上左子树和本身的结点数p = spl[p].r;}}return rank;
}int getkey_byrank(int rank)
{int p = root;while (p){if (spl[spl[p].l].size < rank && rank <= spl[spl[p].l].size + spl[p].cnt) // 在这个范围内就是当前结点{splaying(p, root); // 把当前结点splaying到根结点break;}else if (rank <= spl[spl[p].l].size) p = spl[p].l; // 待求结点在当前结点的左子树else // 待求结点在当前结点的右子树{rank -= spl[spl[p].l].size + spl[p].cnt; // 减去左子树和本身的结点数p = spl[p].r;}}return spl[p].val;
}

例题(待补充)


http://www.ppmy.cn/news/1328760.html

相关文章

PyTorch 中的距离函数深度解析:掌握向量间的距离和相似度计算

目录 Pytorch中Distance functions详解 pairwise_distance 用途 用法 参数 数学理论公式 示例代码 cosine_similarity 用途 用法 参数 数学理论 示例代码 输出结果 pdist 用途 用法 参数 数学理论 示例代码 总结 Pytorch中Distance functions详解 pair…

[SS]语义分割_转置卷积

转置卷积&#xff08;Transposed Convolution&#xff09; 抽丝剥茧&#xff0c;带你理解转置卷积&#xff08;反卷积&#xff09; 目录 一、概念 1、定义 2、运算步骤 二、常见参数 一、概念 1、定义 转置卷积&#xff08;Transposed Convolution&#xff09;&#xf…

【Qt】—— Qt的基本介绍

目录 &#xff08;一&#xff09;什么是Qt &#xff08;二&#xff09; Qt的发展史 &#xff08;三&#xff09;Qt⽀持的平台 &#xff08;四&#xff09; Qt版本 &#xff08;五&#xff09;Qt的优点 &#xff08;六&#xff09;Qt的应⽤场景 &#xff08;七&#xff09…

QQ数据包解密

Windows版qq数据包格式&#xff1a; android版qq数据包格式&#xff1a; 密钥&#xff1a;16个0 算法&#xff1a;tea_crypt算法 pc版qq 0825数据包解密源码&#xff1a; #include "qq.h" #include "qqcrypt.h" #include <WinSock2.h> #include…

JavaSE核心基础-一维数组-笔记

1.数组概念 相同类型数据的集合&#xff0c;它在内存空间的存储是连续的。数组其实也是一个容器,用来存储固定个数相同类型的数据&#xff0c;数组中存储的数据叫做元素。 2.数组定义 方式1&#xff1a; 数据类型[] 数组名 new 数据类型[数组长度]; 数据类型 数组…

面试题-MySQL如何定位慢查询

慢查询出现的情况就这些&#xff1a;聚合查询、多表查询、表数据量过大查询、深度分页查询。 表象&#xff1a;页面加载过慢、接口压测响应时间过长&#xff08;超过1S&#xff09;。 假如你的业务接口就是比较慢&#xff0c;你怎么知道是SQL的问题呢&#xff1f;就算是SQL的…

卷积和滤波对图像操作的区别

目录 问题引入 解释 卷积 滤波 问题引入 卷积和滤波是很相似的&#xff0c;都是利用了卷积核进行操作 那么他们之间有什么区别呢&#xff1f; 卷积&#xff1a;会影响原图大小 滤波&#xff1a;不会影响原图大小 解释 卷积 我们用这样一段代码来看 import torch.nn as …

反序列化字符串逃逸(上篇)

首先&#xff0c;必须先明白&#xff0c;这个点并不难&#xff0c;我给大家梳理一遍就会明白。 反序列化字符串逃逸就是序列化过程中逃逸出来字符&#xff0c;是不是很简单&#xff0c;哈哈哈&#xff01; 好了&#xff0c;不闹了&#xff0c;其实&#xff1a; 这里你们只要懂…