与AVL树一样,伸展树(Splay Tree)也是平衡二叉搜索树的一致,伸展树无需时刻都严格保持整棵树的平衡,也不需要对基本的二叉树结点做任何附加改动,能够保持分摊意义下的高效率。

局部性

通常在任意数据结构的生命期内,执行不同操作的概率往往极不均衡,且各操作之间具有极强的关联性,比如数据局部性,所谓数据局部性包括:

  1. 刚刚被访问到的元素,很可能不久之后就再次被访问
  2. 将被访问的下一元素,很可能就处于不久之前被访问够的某个元素的附近

    故每次都只需将刚被访问到的节点及时地转移到树根(附近),即可加速后续的操作,转移操作与AVL树的旋转操作类似

双层伸展

每次都从当前节点v向上追溯两层,并根据其父亲p和祖父g的相对位置进行相应的旋转

  1. zig-zig/zag-zag

  1. zig-zag/zag-zig

  1. zig/zag(若深度为奇数,需要额外执行一次zig/zag操作)


// 从节点v开始逐层进行伸展
template<typename T>
inline SplayNodePos(T) SplayTree<T>::splay(SplayNodePos(T) & v) {//v为因最近访问而需要伸展的节点
if (!v) return NULL;
SplayNodePos(T) p = NULL;
SplayNodePos(T) g = NULL; //v的父亲以及祖父
while ((p = v->pa) && (g = p->pa) ){// p、g均存在,可以做双层伸展
SplayNodePos(T) gg = g->pa; //gg为v的曾祖父
if (IsLChild(v) && IsLChild(p)) {
attachAsLChild(p, v->rc);
attachAsLChild(g, p->rc);
attachAsRChild(v, p);
attachAsRChild(p, g);
}
else if (IsRChild(v) && IsRChild(p) ){
attachAsRChild(p, v->lc);
attachAsRChild(g, p->lc);
attachAsLChild(v, p);
attachAsLChild(p, g);
}
else if (IsLChild(v) && IsRChild(p)) {
attachAsRChild(g,v->lc);
attachAsLChild(p,v->rc);
attachAsRChild(v,p);
attachAsLChild(v,g);
}
else if (IsRChild(v) && IsLChild(p)) {
attachAsLChild(g, v->rc);
attachAsRChild(g, v->lc);
attachAsLChild(v, p);
attachAsRChild(v, g);
}
if (!gg) v->pa = NULL; //如果v的曾祖父gg不存在,则v现在是root
else //否则将v接到gg上
(g == gg->lc)? attachAsLChild(gg, v) :attachAsRChild(gg, v);
} //双层伸展结束,必有g==NULL,但是p可能非空
if (p = v->pa) { //若p非空,再单独做一次单选
if (IsLChild(v)) {
attachAsLChild(p, v->rc);
attachAsRChild(v,p);
}
else {
attachAsRChild(p, v->lc);
attachAsLChild(v, p);
}
}
v->pa = NULL;
return v;
}

查找操作

不同于其他平衡二叉搜索树的查找操作,伸展树的查找操作是一个动态操作,每次查找后要将最后一个访问的节点通过伸展操作splay到树根

//查找函数,若存在值为key的节点返回相应节点,若不存在则返回相应父节点
template<typename T>
inline SplayNodePos(T) SplayTree<T>::search(const T & key)
{
SplayNodePos(T) hot = NULL;//hot为返回节点的父节点
SplayNodePos(T) x= searchIn(key, root, hot);
if (x)
root = splay(x);
else
root = splay(hot);
return root;
}

插入操作

先利用search操作,注意search此时是一个动态操作,会将最后访问的节点t通过splay操作提升为树根,此时有如上图我们新建节点v并将其作为根接入原树,以t为左子,t的右子为其右子


//插入值为key的节点
template<typename T>
SplayNodePos(T) SplayTree<T>::insert(const T & key)
{
if (!root)//原树为空
return root = new SplayNode<T>(key);
if (key == search(key)->key) //存在值为key的节点,无需执行插入操作,且查找时已经执行了splay操作,此时root为值为key的节点
return root;
//目标节点不存在
SplayNodePos(T) t= root;
if (root->key < key) { //插入新根,分别以t和t->rc为左右孩子
t->pa = root = new SplayNode<T>(key, NULL, t, t->rc);
if (HasRChild(t)) {
t->rc->pa = root;
t->rc = NULL;
}
}else {//插入新根,分别以t->lc和t为左右孩子
t->pa = root = new SplayNode<T>(key, NULL, t->lc, t);
if (HasLChild(t)) {
t->lc->pa = root;
t->lc = NULL;
}
}
return root;
} //无论key是否存在于原树中,返回时总有root->key == key

删除操作

先利用search操作,注意search此时是一个动态操作,会将最后访问的节点v通过splay操作伸展为树根,如果要删除的节点存在于树中,则此时通过splay操作伸展为树根,我们不妨先将root的左右子树先分开,利用search和splay操作将右子树的值最小的节点伸展至树根,此时root必定没有左子树,再将原左子树接回新树,得到最终的树


//删除值为key的节点
template<typename T>
inline bool SplayTree<T>::remove(const T & key)
{
if (!root || key != search(key)->key) //若原树为空或目标不存在则不执行删除操作
return false;
SplayNodePos(T) t = root;
//注意上面执行了search操作,即此时已经伸展过原树,有root->key == key
if (!HasLChild(root)) { //若无左子树,直接删除根节点
root = root->rc;
if (root->rc)
root->rc->pa = NULL;
}
else if (!HasRChild(root)) { //若无右子树,直接删除根节点
root = root->lc;
if (root->lc)
root->lc->pa = NULL;
}else { //否则左右子树均存在,此时
SplayNodePos(T) ltree = root->lc;
ltree->pa = NULL; root->lc = NULL; //暂时切除左子树
root = root->rc; root->pa = NULL;//将删除节点和右子树分离
search(key); //再执行一次search操作,此时左侧key最小的节点会伸展至root,且root无左子树
root->lc = ltree; ltree->pa = root; //将原左子树接回整树中
}
delete t; //删除原根节点
return true; //成功删除
}
#endif // ! _SPLAYTREE_DECLARATION_H

性能分析

利用双层伸展,即使是单链的最坏情况,最终也可以将其压缩至长度大致折半,故即使每次都访问最深处节点,最坏情况也不会持续发生。伸展树虽然不能杜绝最坏情况的发生,但是却能有效地控制最坏情况发生的频度,从而保证分摊意义下的整体高效。利用势能分析法可以证明伸展树的到此操作均可在分摊的O(log n)的时间内完成。

完整源码

代码参考《数据结构(c++语言版)》--清华大学邓俊辉

"SplayTree_Define.h"



#ifndef _SPLAYTREE_DEFINE_H
#define _SPLAYTREE_DEFINE_H //#include"pch.h"
#include<iostream>
#define SplayNodePos(T) SplayNode<T> *
//宏定义
#define IsRoot(x) ( !((x)->pa) )
#define IsLChild(x) ( !(IsRoot(x) ) && (x)==(x)->pa->lc)
#define IsRChild(x) ( !(IsRoot(x) ) && (x)==(x)->pa->rc)
#define HasLChild(x) ((x)->lc )
#define HasRChild(x) ((x)->rc )
#define HasChild(x) (HasLChild(x) || HasRChild(x))
#define HasBothChild(x) (HasRChild(x) && HasLChild(x) )
#define IsLeaf(x) (! HasChild(x) ) //SplayNode 定义
template<typename T>
struct SplayNode {
public:
T key;
SplayNodePos(T) pa;//pa -- parent
SplayNodePos(T) lc;//lc -- left child
SplayNodePos(T) rc;//rc -- right child
//构造函数
SplayNode() :pa(NULL), lc(NULL), rc(NULL) {}
SplayNode(T elem, SplayNodePos(T) pa = NULL, SplayNodePos(T) lc = NULL, SplayNodePos(T) rc = NULL) :
key(elem), pa(pa), lc(lc), rc(rc) { }
};
//AVLTree 定义
template<typename T>
class SplayTree {
private:
SplayNodePos(T) root; //树根
public:
//构造函数和析构函数
SplayTree() :root(NULL) {}
~SplayTree() {} //只读函数
int height() { return Height(root); }
//遍历函数
void preOrder(); //前序遍历
void inOrder(); //中序遍历
void postOrder(); //后序遍历
//操作函数
SplayNodePos(T) search(const T &key); //查找函数,若存在值为key的节点返回相应节点,若不存在则返回NULL,基于searchIn函数实现
SplayNodePos(T) insert(const T &key); //插入值为key的节点
bool remove(const T &key); //移除值为key的节点 private: void preOrder(SplayNodePos(T) &cur); //以cur节点为root进行前序遍历
void inOrder(SplayNodePos(T) &cur); //以cur节点为root进行中序遍历
void postOrder(SplayNodePos(T) &cur); //以cur节点为root进行后序遍历
SplayNodePos(T) searchIn(const T & key, SplayNodePos(T) cur, SplayNodePos(T)& hot); //以cur节点为root查找值为key的节点,hot为返回节点的父节点
SplayNodePos(T) splay(SplayNodePos(T) &cur); // 从节点cur开始逐层进行伸展
};
template<typename NodePos>
inline void attachAsLChild(NodePos p, NodePos lc);//lc作为p的左子接入,lc可能为空
template<typename NodePos>
inline void attachAsRChild(NodePos p, NodePos rc);//rc作为p的右子子接入,lc可能为空
#endif

"SplayTree_Declaration.h"


#include"pch.h"
#ifndef _SPLAYTREE_DECLARATION_H
#define _SPLAYTREE_DECLARATION_H #include"SplayTree_Declaration.h"
#include<iostream>
template<typename T>
inline void SplayTree<T>::preOrder(SplayNodePos(T) & cur)
{
if (!cur) return;
std::cout << cur->key << " ";
preOrder(cur->lc);
preOrder(cur->rc);
} template<typename T>
inline void SplayTree<T>::inOrder(SplayNodePos(T) & cur)
{
if (!cur) return;
inOrder(cur->lc);
std::cout << cur->key << " ";
inOrder(cur->rc);
} template<typename T>
inline void SplayTree<T>::postOrder(SplayNodePos(T)& cur)
{
if (!cur) return;
postOrder(cur->lc);
postOrder(cur->rc);
std::cout << cur->key << " ";
} template<typename T>
inline void SplayTree<T>::preOrder()
{
preOrder(root);
} template<typename T>
inline void SplayTree<T>::inOrder()
{
inOrder(root);
} template<typename T>
inline void SplayTree<T>::postOrder()
{
postOrder(root);
} //lc作为p的左子接入,lc可能为空
template<typename NodePos>
inline void attachAsLChild(NodePos p, NodePos lc) {
p->lc = lc;
if (lc) lc->pa = p;
} //rc作为p的右子子接入,lc可能为空
template<typename NodePos>
inline void attachAsRChild(NodePos p, NodePos rc) {
p->rc = rc;
if (rc) rc->pa = p;
} //以cur节点为root查找值为key的节点,hot为返回节点的父节点
template<typename T>
inline SplayNodePos(T) SplayTree<T>::searchIn(const T & key, SplayNodePos(T) cur, SplayNodePos(T)& hot)
{
if (!cur || key == cur->key) return cur;
hot = cur;
return searchIn(key, (key < cur->key ? cur->lc : cur->rc), hot);
} //查找函数,若存在值为key的节点返回相应节点,若不存在则返回相应父节点
template<typename T>
inline SplayNodePos(T) SplayTree<T>::search(const T & key)
{
SplayNodePos(T) hot = NULL;//hot为返回节点的父节点
SplayNodePos(T) x= searchIn(key, root, hot);
if (x)
root = splay(x);
else
root = splay(hot);
return root;
} //插入值为key的节点
template<typename T>
SplayNodePos(T) SplayTree<T>::insert(const T & key)
{
if (!root)//原树为空
return root = new SplayNode<T>(key);
if (key == search(key)->key) //存在值为key的节点,无需执行插入操作,且查找时已经执行了splay操作,此时root为值为key的节点
return root;
//目标节点不存在
SplayNodePos(T) t= root;
if (root->key < key) { //插入新根,分别以t和t->rc为左右孩子
t->pa = root = new SplayNode<T>(key, NULL, t, t->rc);
if (HasRChild(t)) {
t->rc->pa = root;
t->rc = NULL;
}
}else {//插入新根,分别以t->lc和t为左右孩子
t->pa = root = new SplayNode<T>(key, NULL, t->lc, t);
if (HasLChild(t)) {
t->lc->pa = root;
t->lc = NULL;
}
}
return root;
} //无论key是否存在于原树中,返回时总有root->key == key //删除值为key的节点
template<typename T>
inline bool SplayTree<T>::remove(const T & key)
{
if (!root || key != search(key)->key) //若原树为空或目标不存在则不执行删除操作
return false;
SplayNodePos(T) t = root;
//注意上面执行了search操作,即此时已经伸展过原树,有root->key == key
if (!HasLChild(root)) { //若无左子树,直接删除根节点
root = root->rc;
if (root->rc)
root->rc->pa = NULL;
}
else if (!HasRChild(root)) { //若无右子树,直接删除根节点
root = root->lc;
if (root->lc)
root->lc->pa = NULL;
}else { //否则左右子树均存在,此时
SplayNodePos(T) ltree = root->lc;
ltree->pa = NULL; root->lc = NULL; //暂时切除左子树
root = root->rc; root->pa = NULL;//将删除节点和右子树分离
search(key); //再执行一次search操作,此时左侧key最小的节点会伸展至root,且root无左子树
root->lc = ltree; ltree->pa = root; //将原左子树接回整树中
}
delete t; //删除原根节点
return true; //成功删除
}
#endif // ! _SPLAYTREE_DECLARATION_H // 从节点v开始逐层进行伸展
template<typename T>
inline SplayNodePos(T) SplayTree<T>::splay(SplayNodePos(T) & v) {//v为因最近访问而需要伸展的节点
if (!v) return NULL;
SplayNodePos(T) p = NULL;
SplayNodePos(T) g = NULL; //v的父亲以及祖父
while ((p = v->pa) && (g = p->pa) ){// p、g均存在,可以做双层伸展
SplayNodePos(T) gg = g->pa; //gg为v的曾祖父
if (IsLChild(v) && IsLChild(p)) {
attachAsLChild(p, v->rc);
attachAsLChild(g, p->rc);
attachAsRChild(v, p);
attachAsRChild(p, g);
}
else if (IsRChild(v) && IsRChild(p) ){
attachAsRChild(p, v->lc);
attachAsRChild(g, p->lc);
attachAsLChild(v, p);
attachAsLChild(p, g);
}
else if (IsLChild(v) && IsRChild(p)) {
attachAsRChild(g,v->lc);
attachAsLChild(p,v->rc);
attachAsRChild(v,p);
attachAsLChild(v,g);
}
else if (IsRChild(v) && IsLChild(p)) {
attachAsLChild(g, v->rc);
attachAsRChild(g, v->lc);
attachAsLChild(v, p);
attachAsRChild(v, g);
}
if (!gg) v->pa = NULL; //如果v的曾祖父gg不存在,则v现在是root
else //否则将v接到gg上
(g == gg->lc)? attachAsLChild(gg, v) :attachAsRChild(gg, v);
} //双层伸展结束,必有g==NULL,但是p可能非空
if (p = v->pa) { //若p非空,再单独做一次单选
if (IsLChild(v)) {
attachAsLChild(p, v->rc);
attachAsRChild(v,p);
}
else {
attachAsRChild(p, v->lc);
attachAsLChild(v, p);
}
}
v->pa = NULL;
return v;
}

"main.cpp"


// SplayTree.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
// #include "pch.h"
#include <iostream>
#include"SplayTree_Declaration.h"
#include"SplayTree_Define.h"
using namespace std;
int main()
{
SplayTree<int> t;
int n;
cout << "insert number:";
cin >> n;
cout << "insert:";
for (int i = 0; i < n; i++) {
int tmp;
cin >> tmp;
t.insert(tmp);
}
cout << "PreOrder:";
t.preOrder();
cout << endl;
cout << "InOrder:";
t.inOrder();
cout << endl;
cout << "PostOrder:";
t.postOrder();
cout << endl;
cout << "remove number:";
cin >> n;
cout << "remove:";
for (int i = 0; i < n; i++) {
int tmp;
cin >> tmp;
t.remove(tmp);
}
cout << "PreOrder:";
t.preOrder();
cout << endl;
cout << "InOrder:";
t.inOrder();
cout << endl;
cout << "PostOrder:";
t.postOrder();
cout << endl;
return 0;
return 0;
}

高级搜索树-伸展树(Splay Tree)的更多相关文章

  1. 纸上谈兵: 伸展树 (splay tree)[转]

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢!  我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每 ...

  2. K:伸展树(splay tree)

      伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(lgN)内完成插入.查找和删除操作.在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使 ...

  3. 树-伸展树(Splay Tree)

    伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二 ...

  4. 【BBST 之伸展树 (Splay Tree)】

    最近“hiho一下”出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codefor ...

  5. 伸展树 Splay Tree

    Splay Tree 是二叉查找树的一种,它与平衡二叉树.红黑树不同的是,Splay Tree从不强制地保持自身的平衡,每当查找到某个节点n的时候,在返回节点n的同时,Splay Tree会将节点n旋 ...

  6. 伸展树(Splay tree)的基本操作与应用

    伸展树的基本操作与应用 [伸展树的基本操作] 伸展树是二叉查找树的一种改进,与二叉查找树一样,伸展树也具有有序性.即伸展树中的每一个节点 x 都满足:该节点左子树中的每一个元素都小于 x,而其右子树中 ...

  7. HDU 4453 Looploop (伸展树splay tree)

    Looploop Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  8. hdu 2871 Memory Control(伸展树splay tree)

    hdu 2871 Memory Control 题意:就是对一个区间的四种操作,NEW x,占据最左边的连续的x个单元,Free x 把x单元所占的连续区间清空 , Get x 把第x次占据的区间输出 ...

  9. 伸展树 Splay 模板

    学习Splay的时候参考了很多不同的资料,然而参考资料太杂的后果就是模板调出来一直都有问题,尤其是最后发现网上找的各种资料均有不同程度的错误. 好在啃了几天之后终于算是啃下来了. Splay也算是平衡 ...

随机推荐

  1. day9 python之文件操作

    1.文件操作 1.1 基本模式 # 格式 f = open("相对路径/绝对路径",mode = "模式",encoding = "编码级" ...

  2. P1433 吃奶酪(洛谷)状压dp解法

    嗯?这题竟然是个绿题. 这个题真的不(很)难,我们只是不会计算2点之间的距离,他还给出了公式,这个就有点…… 我们直接套公式去求出需要的值,然后普通的状压dp就可以了. 是的状压dp. 这个题的数据加 ...

  3. vue : 项目起手式 - router组件通用模板

    每次新建文件都要找来找去,麻烦,干脆贴到这里好了. <template> <div id="page"> </div> </templat ...

  4. p46_IPv4地址

    IP地址:全世界唯一的32位/4字节标识符,标识路由器主机的接口. IP地址::={<网络号>,<主机号>} 图中有6个子网 比如222.1.3.0是网络号,3是主机号,222 ...

  5. 题解 洛谷 P4694 【[PA2013]Raper】

    首先考虑题目的性质,不难发现光盘的花费是一个凸函数.当生产 \(0\) 张光盘时,其花费为 \(0\),随着光盘生产数的增加,最优情况肯定是先选择工厂便宜的时刻,所以花费会增长越来越快,因此其为一个下 ...

  6. 通过C语言获取MAC地址(转)

    原文:http://blog.chinaunix.net/uid-25885064-id-3125167.html #include <sys/ioctl.h>#include <n ...

  7. linux dig 命令使用

    linux dig 命令使用方法 2018.04.20 15:47 43101浏览   dig 命令主要用来从 DNS 域名服务器查询主机地址信息. 查询单个域名的 DNS 信息 dig 命令最典型的 ...

  8. IO—》字节流&字符流

    字节流 一.字节输出流OutputStream OutputStream此抽象类,是表示输出字节流的所有类的超类.操作的数据都是字节,定义了输出字节流的基本共性功能方法. FileOutputStre ...

  9. 仓库储存,存在添加减少,不存在插入ON DUPLICATE KEY UPDATE

    原文链接:https://blog.csdn.net/qq_42269354/article/details/100589640 首先声明:ON DUPLICATE KEY UPDATE 为MySQL ...

  10. mongoose子文档生成不了_id,查询困难的解决办法

    以下是我的数据库表 本来是想设置一个自增属性,比如listId,来定义我的子list,sliderlist这些,这样能确保他的唯一性,结果尝试了很久都无法成功,原生,插件都无法成功, 这两个网址是讲的 ...