Splay树(伸展树)

来源:互联网 发布:淘宝水印美图秀秀教程 编辑:程序博客网 时间:2024/05/15 01:04

伸展树(Splay Tree),是一种二叉平衡搜索树,时间复杂度均摊为O(logN),即splay树做m次操作的复杂度为O(mlogN)。

在伸展树上的一般操作都基于伸展操作/提根操作

假设想要对一个二叉搜索树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉搜索树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

伸展树与AVL树,Treap树不同的是,Splay树不需维护其他性质,他是通过对每次查询,插入处理到的节点进行提根操作来达到这一效果的。它的优势在于不需要记录用于平衡树的冗余信息。

可以证明:伸展树时间复杂度均摊为O(logN)。

伸展/提根的具体形式:两种单旋,四种双旋

单旋:zig(左旋),zag(右旋)

双旋:zig-zig,zag-zag,zag-zig,zig-zag

zig操作(zig(x)):当x的父亲p是原树的根,并且x是p的左儿子的时候进行,效果如图(zag操作类似)。


zig-zig操作(zig-zig(x)):当x是父亲p的左儿子,p有父亲并且p是父亲g的左儿子的时候进行,效果等价于先zig(p),再zig(x)(zag-zag操作类似)。


zag-zig操作(zag-zig(x)):当x是父亲p的右儿子,p有父亲并且p是父亲g的左儿子的时候进行,效果等价于先zag(x),再zig(x)(zig-zag操作类似)。


伸展树可有两种形式:

点操作

splay树可以一个一个的插入结点,这样的splay树是有序树,结点权值大于左儿子小于右儿子,这样就是点操作。

区间操作

还有就是可以自己建树,这样的splay树就不是按权值的有序树,它不满足结点权值大于左儿子小于右儿子,但是它也是有顺序的,无论怎么伸展,把它的结点中序遍历结果就是原来的数组顺序,因此自己建树可以操作区间。

点操作的具体操作:

插入

将元素x插入伸展树S表示的有序集中,首先,也与处理普通的二叉查找树一样,将x插入到伸展树S中的相应位置上,再对x执行提根操作。

删除

法一:将元素x从伸展树S所表示的有序集中删除,首先,用在二叉查找树中查找元素的方法找到x的位置。如果x没有孩子或只有一个孩子,那么直接将x删去,并通过Splay操作,将x节点的父节点调整到伸展树的根节点处。否则,则向下查找x的右子树中最小的位置,用y替代x的位置,最后将y调整为伸展树的根。

法二:将待删除的节点提根,再合并左右子树,合并只需要将较大树中的最小值提根,然后将较小树直接接在较大树根节点的左边即可。

查找

判断元素x是否在伸展树S表示的有序集中,首先,与在二叉查找树中的查找操作一样,在伸展树中查找元素x。如果x在树中,则再执行Splay操作调整伸展树。

合并

join(S1,S2):将两个伸展树S1与S2合并成为一个伸展树。其中S1的所有元素都小于S2的所有元素。首先,我们找到伸展树S1中最大的一个元素x,再通过Splay操作将x调整到伸展树S1的根。然后再将S2作为x节点的右子树。这样,就得到了新的伸展树S

划分

Split(x,S):以x为界,将伸展树S分离为两棵伸展树S1和S2,其中S1中所有元素都小于x,S2中的所有元素都大于x。首先执行Find(x,S),将元素x调整为伸展树的根节点,则x的左子树就是S1,而右子树为S2

代码实现:


数组实现,基于类封装。splay树点操作基于指针实现较好,代码略


点操作里,也常有findKth(),找前驱,后继操作,有时也可出现重复值,代码没加上。

这是自己写的代码,acm资料中的代码与这不同,有空可以补充在此


#include <cstdio>#include <cstring>#include <algorithm>#include <iostream>#include <fstream>#include <sstream>using namespace std;#define MAXN 100005class Splay {private:int root,cnt;int key[MAXN], child[MAXN][2], parent[MAXN];public:Splay() {root = 0;cnt = 0;}void zig(int fa,int x) {//左旋bool flag = fa == root;//x更新到根后,需要更新rootint index = child[parent[fa]][0] == fa ? 0 : 1;child[fa][0] = child[x][1];child[x][1] = fa;parent[child[fa][0]] = fa;parent[x] = parent[fa];parent[fa] = x;child[parent[x]][index] = x;if (flag) {root = x;}}void zag(int fa,int x) {//右旋bool flag = fa == root;int index = child[parent[fa]][0] == fa ? 0 : 1;child[fa][1] = child[x][0];child[x][0] = fa;parent[child[fa][1]] = fa;parent[x] = parent[fa];parent[fa] = x;child[parent[x]][index] = x;if (flag) {root = x;}}void left_rotate(int fa,int x) {zig(fa, x);}void right_rotate(int fa,int x) {zag(fa, x);}void left_left_rotate(int grandfa,int fa,int x) {zig(grandfa, fa);zig(fa, x);}void right_right_rotate(int grandfa, int fa, int x) {zag(grandfa, fa);zag(fa, x);}void left_right_rotate(int grandfa, int fa, int x) {zag(fa, x);zig(grandfa, x);}void right_left_rotate(int grandfa, int fa, int x) {zig(fa, x);zag(grandfa, x);}void splayTree(int &r,int x) {//提根操作if (r == x) {//r == x,无需提根return;}while (r != x && child[r][0] != x && child[r][1] != x) {//r != x很重要,当x已更新到根r,也出循环int grandfa = parent[parent[x]];int fa = parent[x];int flag1 = child[grandfa][0] == fa;int flag2 = child[fa][0] == x;if (!flag1 && !flag2) {right_right_rotate(grandfa, fa, x);}else if (!flag1 && flag2) {right_left_rotate(grandfa, fa, x);}else if (flag1 && !flag2) {left_right_rotate(grandfa, fa, x);}else {left_left_rotate(grandfa, fa, x);}}if (child[r][0] == x) {left_rotate(r, x);}else if(child[r][1] == x){right_rotate(r, x);}}void _insert(int fa,int &x, int value) {//假设没有重复值if (!x) {key[++cnt] = value;x = cnt;child[x][0] = child[x][1] = 0;parent[x] = fa;splayTree(root,x);}else if (value < key[x]) {_insert(x, child[x][0], value);}else if (value > key[x]) {_insert(x, child[x][1], value);}}int _find(int x,int value) {if (!x || value == key[x]) {return x;}else if (value < key[x]) {return _find(child[x][0], value);}else {return _find(child[x][1], value);}}int _findMin(int x) {if (!x || !child[x][0]) {return x;}return _findMin(child[x][0]);}void deletes(int value) {if (!root) {return;}int loc = _find(root, value);if (!loc) {return;}splayTree(root,loc);int x = child[root][0];int y = child[root][1];if (x == 0) {root = y;parent[y] = 0;return;}if (y == 0) {root = x;parent[x] = 0;return;}loc = _findMin(y);splayTree(child[root][1],loc);child[loc][0] = x;parent[x] = loc;root = loc;parent[loc] = 0;}void insert(int value) {_insert(0, root,value);}int find(int value) {int i = _find(root, value);if (i) {splayTree(root, i);}return i;}};int main() {Splay *tree = new Splay();tree->insert(2);tree->insert(1);tree->insert(5);tree->deletes(2);cout << tree->find(1) << endl;delete tree;return 0;}

区间操作

在一些附加信息和标记传递的帮助下,Splay可以比线段树更灵活地维护一些区间信息:我们可以把Splay里的每个节点用来表示一个单位区间,而且,splay树的区间操作不满足数据的大小决定节点在树中的位置,而是由原始数组下标大小决定该节点在树中的相对位置,而对区间[a,b]进行操作的时候,我们可以把(a - 1)元素旋转到树根,再把(b + 1)元素旋转到(a - 1)元素的右子树的树根,这样(b + 1)元素的左子树就代表了整个区间[a,b],我们就可以在上面进行各类操作,例如区间反转等等。

splay树能完成很多线段树完成不了的操作,如:区间插入,区间删除,区间反转

以下的splay树区间操作模板基于数组实现(很少有指针实现),包括一些常见操作

注意:

开始针对一段数组区间建树的过程,并不是数组区间下标就对应splay树所用空间的下标,还是按照建一个节点就取一个可用空间的过程,只不过对于每个子树和其对应的区间,均先建根节点(根节点对应区间中点),再去建左右子树,就像线段树一样,最后对splay树中序遍历的结果即为一个连续的原始数组数据;还有!!!二叉搜索树无论怎么旋转(在采用正确的规则以不破坏二叉搜索树性质的前提下),中序遍历结果均为有序的,此处为无论怎么旋转,均为连续的原始数组数据。

代码:

主要功能如图



#include <cstdio>#include <cstring>#include <algorithm>#include <string>#include <iostream>using namespace std;#define MAXN 50005#define INF 0X3f3f3f3f#define RL child[child[root][1]][0]class Splay {private:int root;int cnt1, a[MAXN];//a数组中为原始数据int key[MAXN], child[MAXN][2] ,pre[MAXN];int size[MAXN], sum[MAXN], same[MAXN], reverse[MAXN];//same,reverse用于延迟标记,取相同值和翻转int lx[MAXN], rx[MAXN], mx[MAXN];//最大子序列和,lx/rx/mx必取到左/右/中或均不取int s[MAXN], cnt2;//内存池和容量,为了减少浪费空间public:Splay(int n) {root = cnt1 = cnt2 = 0;key[root] = child[root][0] = child[root][1] = pre[root] = 0;size[root] = sum[root] = same[root] = reverse[root] = 0;lx[root] = rx[root] = mx[root] = -INF;//为了让操作区间被封闭起来,开始建树先加入两个节点,建的位置相当于数组的第一个和最后一个位置newNode(root, 0, -1);newNode(child[root][1], root, -1);for (int i = 0; i < n; i++) {scanf("%d", &a[i]);}build(RL, 0, n - 1, child[root][1]);pushUp(child[root][1]);pushUp(root);}void newNode(int &r, int fa, int value) {if (cnt2) {r = s[cnt2--];}else {r = ++cnt1;}pre[r] = fa;size[r] = 1;key[r] = sum[r] = value;child[r][0] = child[r][1] = 0;same[r] = reverse[r] = 0;lx[r] = rx[r] = mx[r] = value;}void build(int &r, int left, int right, int fa) {//递归建树if (left > right) {return;}int middle = (left + right) / 2;newNode(r, fa, a[middle]);build(child[r][0], left, middle - 1, r);build(child[r][1], middle + 1, right, r);pushUp(r);}void updateSame(int r, int value) {if (!r) {return;}key[r] = value;sum[r] = size[r] * value;lx[r] = rx[r] = mx[r] = max(value, sum[r]);same[r] = 1;//重点!!!}void updateReverse(int r) {if (!r) {return;}swap(child[r][0], child[r][1]);swap(lx[r], rx[r]);reverse[r] ^= 1;//重点!!! 翻转的标记,可以两两抵消}void pushUp(int r) {//上滤int lson = child[r][0], rson = child[r][1];size[r] = size[lson] + size[rson] + 1;sum[r] = sum[lson] + sum[rson] + key[r];//重点!!!lx[r] = max(lx[lson], sum[lson] + key[r] + max(0, lx[rson]));rx[r] = max(rx[rson], sum[rson] + key[r] + max(0, rx[lson]));mx[r] = max(0, rx[lson]) + key[r] + max(0, lx[rson]);mx[r] = max(mx[r], max(mx[lson], mx[rson]));}void pushDown(int r) {//下滤if (same[r]) {updateSame(child[r][0], key[r]);updateSame(child[r][1], key[r]);same[r] = 0;}if (reverse[r]) {updateReverse(child[r][0]);updateReverse(child[r][1]);reverse[r] = 0;}}void rotate(int r, int kind) {//0为左旋 1为右旋int y = pre[r];//重点!!! 用到的节点,把之前延迟标记去掉,否则会出错;而且去掉延迟标记要自上而下,不可自下而上!!!pushDown(y);pushDown(r);child[y][kind] = child[r][!kind];pre[child[y][kind]] = y;if (pre[y]) {child[pre[y]][child[pre[y]][1] == y] = r;}pre[r] = pre[y];child[r][!kind] = y;pre[y] = r;pushUp(y);//为了效率,每次只更新该次旋转的父亲节点,当最后的目标节点到达要求位置,再去单独更新(即splay()中最后的那个pushUP())}void splay(int r, int goal) {//把r旋转到goal的下面pushDown(r);//此处pushDown(r)为了对应while()外的那个pushUp(r)while (pre[r] != goal) {if (pre[pre[r]] == goal) {//重点!!! 用到的节点先下滤,把之前延迟标记去掉,否则会出错pushDown(pre[r]);     pushDown(r);rotate(r, child[pre[r]][1] == r);}else {pushDown(pre[pre[r]]);//重点!!! 用到的节点先下滤,把之前延迟标记去掉,否则会出错pushDown(pre[r]);     //下滤如果不是自上而下,而是自下而上,那么可能最后所用到节点还是存在延迟标记的,那样节点的相对位置不对,旋转就是错的pushDown(r);          int y = pre[r];int kind = child[pre[y]][1] == y;if (child[y][kind] == r) {rotate(y, kind);rotate(r, kind);}else {rotate(r, !kind);rotate(r, kind);}}}pushUp(r);//此处pushUp(r)为了目标节点完成旋转后的更新,为了针对于while()内的操作;但如果没进while(),而且r有延迟标记,          //只有加上开头那个pushDown(r)才正确if (goal == 0) {root = r;}}int getKth(int r,int k) {//得到原始数组中第k个元素在树中的位置//先下滤,否则下一层递归,子树的左右子树相对位置正确与否未知pushDown(r);int t = size[child[r][0]] + 1;if (t == k) {return r;}else if (t > k) {return getKth(child[r][0], k);}else {return getKth(child[r][1],k - t);}}//在第pos个数后面插入tot个数://把第pos + 1(要加上边界-1,所以是pos + 1)个数旋转到树根,第pos + 2个数旋转到右子树的树根,这样右子树的左子树为空,再去插数据void insert(int pos, int tot) {for (int i = 0; i < tot; i++) {scanf("%d", &a[i]);}splay(getKth(root, pos + 1),0);splay(getKth(root, pos + 2), root);build(RL, 0, tot - 1, child[root][1]);pushUp(child[root][1]);pushUp(root);}//删除子树void erase(int r) {if (!r) {return;}s[++cnt2] = r;erase(child[r][0]);erase(child[r][1]);}//从第pos个数开始连续删除tot个数//把第pos个数旋转到树根,第pos + tot + 1个数旋转到右子树的树根,那么右子树的左子树即为要删除的区间[pos + 1,pos + tot]void deletes(int pos, int tot) {splay(getKth(root,pos),0);splay(getKth(root,pos + tot + 1),root);erase(RL);pre[RL] = 0;RL = 0;pushUp(child[root][1]);pushUp(root);}//将从第pos个数开始的连续的tot个数修改为value//把第pos个数旋转到树根,第pos + tot + 1个数旋转到右子树的树根,那么右子树的左子树即为要修改的区间[pos + 1,pos + tot]void makeSame(int pos, int tot,int value) {splay(getKth(root, pos), 0);splay(getKth(root, pos + tot + 1), root);updateSame(RL, value);pushUp(child[root][1]);pushUp(root);}//将第pos个数开始的连续tot个数进行翻转//把第pos个数旋转到树根,第pos + tot + 1个数旋转到右子树的树根,那么右子树的左子树即为要翻转的区间[pos + 1,pos + tot]void makeReverse(int pos, int tot) {splay(getKth(root, pos), 0);splay(getKth(root, pos + tot + 1), root);updateReverse(RL);pushUp(child[root][1]);pushUp(root);}//得到第pos个数开始的tot个数的和//把第pos个数旋转到树根,第pos + tot + 1个数旋转到右子树的树根,那么右子树的左子树即为要求和的区间[pos + 1,pos + tot]int getSum(int pos, int tot) {splay(getKth(root, pos), 0);splay(getKth(root, pos + tot + 1), root);return sum[RL];}//得到第pos个数开始的tot个数中最大的子段和//把第pos个数旋转到树根,第pos + tot + 1个数旋转到右子树的树根,那么右子树的左子树即为要求最大子段和的区间[pos + 1,pos + tot]int getMaxSum(int pos, int tot) {splay(getKth(root, pos), 0);splay(getKth(root, pos + tot + 1), root);return mx[RL];}//splay树的中序遍历void inOrder(int r) {if (!r) {return;}pushDown(r);inOrder(child[r][0]);printf(" %d", key[r]);inOrder(child[root][1]);}int getRoot() {return root;}};int main() {int n, q, a, b, c;string op;Splay *tree;while (scanf("%d%d", &n, &q) != EOF) {tree = new Splay(n);for (int i = 0; i < q; i++) {cin >> op;if (op == "insert") {scanf("%d%d", &a, &b);tree->insert(a,b);}else if (op == "deletes") {scanf("%d%d", &a, &b);tree->deletes(a, b);}else if (op == "makeSame") {scanf("%d%d%d", &a, &b, &c);tree->makeSame(a, b, c);}else if (op == "makeReverse") {scanf("%d%d", &a, &b);tree->makeReverse(a, b);}else if (op == "getSum") {scanf("%d%d", &a, &b);cout << tree->getSum(a, b) << endl;}else if (op == "getMaxSum") {scanf("%d%d", &a, &b);cout << tree->getMaxSum(a, b) << endl;}else if (op == "inOrder") {tree->inOrder(tree->getRoot());}}delete tree;}return 0;}