caioj1130:伸展树(模版)

版权声明:有女朋友的老江的博客,转载请告知老江 https://blog.csdn.net/qq_42367531/article/details/84591736

目录

题目描述

伸展树的基本概念

定义结构体

更新控制的节点数的函数

增加一个点的函数

rotate旋转的函数(重要)

 找某个值的编号的函数

插入的函数

删除的函数

找排名的函数

 找某个排名对应的值的函数

找前驱的函数

找后继的函数

智障的主函数

完整代码


题目描述

【题意描述】
写一种数据结构,来维护一些数,其中需要提供以下操作: 
1. 插入x数 
2. 删除x数(若有多个相同的数,应只删除一个) 
3. 查询x数的排名(若有多个相同的数,应输出最小的排名) 
4. 查询排名为x的数 
5. 求x的前驱(前驱定义为小于x,且最大的数) 
6. 求x的后继(后继定义为大于x,且最小的数) 
【输入格式】
第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号(1<=opt<=6) 
(n < = 100000, 所有数字均在-10^7到10^7内 )
【输出格式】
对于操作3,4,5,6每行输出一个数,表示对应答案
Sample Input 
8
1 10
1 20
1 30
3 20
4 2
2 10
5 25
6 -1

Sample Output 
2
20
20
20

伸展树的基本概念

伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的。 

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

它的优势在于不需要记录用于平衡树的冗余信息。

伸展操作

伸展操作Splay(x,S)是在保持伸展树有序性的前提下,通过一系列旋转将伸展树S中的元素x调整至树的根部。在调整的过程中,要分以下三种情况分别处理: 

情况一:节点x的父节点y是根节点。这时,如果x是y的左孩子,我们进行一次Zig(右旋)操作如果x是y的右孩子,则我们进行一次Zag(左旋)操作。经过旋转,x成为二叉查找树S的根节点,调整结束。即:如果当前结点父结点即为根结点,那么我们只需要进行一次简单旋转即可完成任务,我们称这种旋转为单旋转。

情况二:节点x的父节点y不是根节点,y的父节点为z,且x与y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,我们进行一次Zig-Zig操作或者Zag-Zag操作。即:设当前结点为X,X的父结点为Y,Y的父结点为Z,如果Y和X同为其父亲的左孩子或右孩子,那么我们先旋转Y,再旋转X。我们称这种旋转为一字形旋转。

情况三:节点x的父节点y不是根节点,y的父节点为z,x与y中一个是其父节点的左孩子而另一个是其父节点的右孩子。这时,我们进行一次Zig-Zag操作或者Zag-Zig操作。即:这时我们连续旋转两次X。我们称这种旋转为之字形旋转。

如图4所示,执行Splay(1,S),我们将元素1调整到了伸展树S的根部。再执行Splay(2,S),如图5所示,我们从直观上可以看出在经过调整后,伸展树比原来“平衡”了许多。而伸展操作的过程并不复杂,只需要根据情况进行旋转就可以了,而三种旋转都是由基本得左旋和右旋组成的,实现较为简单。

  • 查找操作

Find(x,S):判断元素x是否在伸展树S表示的有序集中。

首先,与在二叉查找树中的查找操作一样,在伸展树中查找元素x。如果x在树中,则再执行Splay(x,S)调整伸展树。

  • 加入操作

Insert(x,S):将元素x插入伸展树S表示的有序集中。

首先,也与处理普通的二叉查找树一样,将x插入到伸展树S中的相应位置上,再执行Splay(x,S)。

  • 删除操作

Delete(x,S):将元素x从伸展树S所表示的有序集中删除

首先,用在二叉查找树中查找元素的方法找到x的位置。如果x没有孩子或只有一个孩子,那么直接将x删去,并通过Splay操作,将x节点的父节点调整

到伸展树的根节点处。否则,则向下查找x的后继y,用y替代x的位置,最后执行Splay(y,S),将y调整为伸展树的根。

  • 合并操作

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

  • 启发式合并

当S1和S2元素大小任意时,将规模小的伸展树上的节点一一插入规模大的伸展树,总时间复杂度O(Nlg^2N)。

  • 划分操作

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

  • 其他操作

除了上面介绍的五种基本操作,伸展树还支持求最大值、求最小值、求前驱、求后继等多种操作,这些基本操作也都是建立在伸展操作的基础上的。

通常来说,每进行一种操作后都会进行一次Splay操作,这样可以保证每次操作的平摊时间复杂度是O(logn)

优势

  • 可靠的性能——它的平均效率不输于其他平衡树

  • 存储所需的内存少——伸展树无需记录额外的什么值来维护树的信息,相对于其他平衡树,内存占用要小。 

由于Splay Tree仅仅是不断调整,并没有引入额外的标记,因而树结构与标准红黑树没有任何不同,从空间角度来看,它比TreapSBT、AVL要高效得多。因为结构不变,因此只要是通过左旋和右旋进行的操作对Splay Tree性质都没有丝毫影响,因而它也提供了BST中最丰富的功能,包括快速的拆分和合并,并且实现极为便捷。这一点是其它结构较难实现的。其时间效率也相当稳定,和Treap基本相当,常数较高。

缺点

伸展树最显著的缺点是它有可能会变成一条。这种情况可能发生在以非降顺序访问n个元素之后。然而均摊的最坏情况是对数级的——O(logn)。

【来源:百度百科】

定义结构体

struct trnode
{
    int d,n,c,f,son[2];
	/*
		d为值,f为父亲的编号,
		c为控制的节点个数,以他为根节点的那棵树的所有的节点数 
		n为同值的节点个数,把多个同样的值浓缩成一个结构体 
		(这一步是为了省空间,比如说3个为100的数,可以说这个d为100,n为3) 
		son[0]为左孩子,son[1]为右孩子 
	*/ 
}tr[110000]; int len;//len表示用到了第几个节点

结构体的每一步定义都要搞清楚,这个关乎到了代码的所有意义。这里有一个很重要的东西,就是结构体中的n,他把相同的数字都保存在了一起,大大减少了内存,不占用空间。

更新控制的节点数的函数

void update(int x)//更新编号为x的节点所控制的节点数 
{
    int lc=tr[x].son[0];//左孩子的编号 
	int rc=tr[x].son[1];//右孩子的编号 
    tr[x].c=tr[lc].c+tr[rc].c+tr[x].n;
    //x总共的节点数=左孩子的节点数+右孩子的节点数+同值的数 
}

更新节点数的这个函数在整个代码当中起到了一个很重要的作用,因为几乎每一步的操作都离不开这个update函数,所以这个函数必须要记住,其实也很简单的,就是 左孩子+右孩子+重复的 就是更新后的节点数

增加一个点的函数

void add(int d,int f)//添加值为d的点,认f为父亲,同时,f也认他为孩子 
{
    len++;//增加一个节点数 
    tr[len].d=d; tr[len].n=1; tr[len].c=1; 
    /*
    	这一步是关于加入的值的
		加入的这个值的值就是定义的d——值,
		然后只有他自己一个,所以n=1
		同时他控制的节点数也只有他自己一个 
    */
    tr[len].f=f; if(d<tr[f].d) tr[f].son[0]=len; else tr[f].son[1]=len;
    /*
    	这一步是关于他认的父亲的操作
		这个节点的父亲就是定义的f-父亲
		我们默认比父亲节点的值小的为左孩子,比父亲节点的值大的为右孩子
		所以说如果加入的这个节点的值比父亲节点的值大,就为左孩子,否则右孩子
		状态:左小右大 
		
		这里可能会考虑到如果这个节点原本就有孩子怎么办?
		这个的话我也解释不清楚,
		因为我们是增加进去的
		所以我们只要找到合适的位置插入就好了
		比如说
		                8
		              /   \
			     3     25
			    / \   /  \
		           2   4 20  30
		 假如我们要插入10的话,10离4最近,所以应该插入到4的下面,
		 但是这一整棵子树的每一个节点都要比根节点小
		 所以这样的话,这个10就只能认20为父亲,也是最接近的答案了 
	*/ 
    tr[len].son[0]=tr[len].son[1]=0;
    /*
    	定义最开始的加入的这个点,是一个叶子节点 
		既没有左孩子,也没有右孩子 
    */
}

这个函数主要实在插入的时候用的,用这个函数使得插入的时候少了一大串东西,而且也可以直接统计好更新之后的节点数。 

rotate旋转的函数(重要)

void rotate(int x,int w)
/*
	这是整个代码当中的一个关键点
	首先我们定义了x是我们要选择旋转的节点
	w有两个形式,一个是0,一个是1
	0表示左转,1表示右转
	(x,0)表示x这个点左转
	(x,1)表示x这个点右转
	注意,我们要转的可能是x,但是变化的不止x,和x有关系的也有变动 
*/ 
{
    int y=tr[x].f; int z=tr[y].f;//x在旋转之前,要确定x的父亲y和爷爷z 
    //下来建立关系 
    int r,R;//r表示儿辈,R表示父辈 (ren,Ren) 
    //有四个角色:我x,我的儿子,我的父亲,我的爷爷
	/*
		接下来的就是在旋转的时候发生的关系
		x为左孩子才可以右转,为右孩子才可以左转 
 		在这里可能要有图才讲得清楚 
 		      y                               x  
 		     / \                             / \     
 		    c   x                           y   b
 		       / \                         / \    
 		      a   b                       c   a
 		左转前                            左转后
		 
		          y                               x
			 / \                             / \
			x   c                           a   y
		       / \                             / \
		      a   b                           b   c
		  右转前                              右转后
		稍微解释一下:
		我们之前定义过左孩子的值比父亲节点的值要小,
		右孩子比父亲节点的值要大,是吧?
		那么这个时候我们就可以看到,
		x左转之后一定跟y换了位置,这个是必然的,
		然后,y是比x小的(x是y的右孩子),
		所以x替代了y的位置之后,y就成为了x的左孩子(比x小),
		然后c是y的右孩子,比y小,旋转之后跟着y成为y的右孩子即可。
		然后我们知道b是右孩子,比x要大,所以依旧成为x的右孩子即可,
		那么剩下a,首先我们知道a是比x小的,但是总体来看是比y要大的,
		因为x比y大,所以a也比y大,然而x的左右孩子都有了,
		y还有右孩子的空位,那么a又比y大,所以a在y的右孩子的位置刚刚好。
 
	*/ 
	//更换过程是从下到上的,而且是儿子先认父亲,父亲再认儿子 
    r=tr[x].son[w]; R=y;//x的儿子->准备当新儿子
	/*
		左边旋转的话,x的左孩子就变成别人的孩子;
		右边旋转的话,x的右孩子就变成别人的孩子。 
		然后这个孩子的新父亲就是x的父亲y
	*/ 
    tr[R].son[1-w]=r;
    /*
    	左边旋转的话,x的左孩子就变成y的右孩子;
					  x的右孩子仍然是x的右孩子
		右边旋转的话,x的右孩子就变成y的左孩子。
					  x的左孩子仍然是x的左孩子 
    */
    if(r!=0) tr[r].f=R;
    //如果这个x的孩子节点不是0的话,这个孩子节点的父亲就是前面认过的y节点
      
    r=x; R=z;//x->准备当新儿子 
    if(tr[R].son[0]==y) tr[R].son[0]=r; 
    /*
    	首先我们知道,x左转之后就变成了z的孩子节点,
		因为y原来是z的孩子,现在x代替了y的位置
		所以z就是x的父亲节点
	*/ 
    //如果y所在的是z的左孩子,那么x的位置就是z的左孩子 
    else tr[R].son[1]=r; 
    //否则就为z的右孩子,其实就是顶替的y的位置,其他不变
    tr[r].f=R;
    //x的父亲节点变为z
      
    r=y; R=x;//x的父亲y->准备当新儿子
	//y这个时候变成了孩子节点,他的父亲节点是x 
    tr[R].son[w]=r;
    /* 
	    左转的话,y就是x的左孩子
				  y的左孩子仍然是y的左孩子
		右转的话,y就是x的右孩子
			      y的右孩子仍然是y的右孩子
	*/ 
    tr[r].f=R;
    //x就是y的父亲节点
          
    update(y);//先更新处于下层的点y,因为我们是先换下面的 
    update(x);//再更新上层的x,后换上面的 
}
  
void splay(int x,int rt)
//该函数的功能是:经过旋转之后,使x成为rt的孩子(左右都可以) 
//最关键的操作 
{
    while(tr[x].f!=rt)//如果rt是x的父亲,则什么都不用做,否则x就要不断向上旋转
    {
        int f=tr[x].f; int ff=tr[f].f;//准备x的父亲和爷爷
        if(ff==rt)//如果x的爷爷是rt,那么x只需要旋转一次(相当于跳一层)
        {
            if(tr[f].son[0]==x) rotate(x,1); else rotate(x,0);
            //如果x是f的左孩子的话,就右旋,也只能右旋
			//如果x是f的右孩子的话,就左旋,也只能左旋 
        }
        else//rt在ff的上面 
        {
                 if(tr[ff].son[0]==f && tr[f].son[0]==x) {rotate(f,1); rotate(x,1);}
            /*
            	      ff   第一次右转    f      第二次右转     x 
		      /     f变成爷爷   / \     x变成爷爷       \
		     f                x   ff                    f
		    /                                             \  
		   x                                               ff
			*/ 
            else if(tr[ff].son[1]==f && tr[f].son[1]==x) {rotate(f,0); rotate(x,0);}
            /*
                      ff   第一次左转   f      第二次右转      x           
                        \  f变成爷爷   / \     x变成爷爷      /   
                         f           ff  x                 f
                          \                               /
                           x                             ff
            */ 
            else if(tr[ff].son[0]==f && tr[f].son[1]==x) {rotate(x,0); rotate(x,1);}
            /*
            	      ff  第一次左转   ff      第二次右转      x 
            	     /    x变成父亲  /        x变成爷爷      / \ 
            	    f              x                      f   ff
		     \            /
		      x          f  
				   这一次的旋转比较特殊,如果f右转的话就会出现这样的情况
					  f
					   \
					    ff
					    /
					   x
					转了跟没转一样,所以只能转x,不能动y 
			*/ 
            else if(tr[ff].son[1]==f && tr[f].son[0]==x) {rotate(x,1); rotate(x,0);}
            /*
            	      ff  第一次右转   ff      第二次右转     x 
            	        \ x变成父亲      \     x变成爷爷     / \ 
            	         f                x               ff  f   
			/                  \
		       x                    f
					跟上面一样也是只能转x,不能转y 
            */
        }
    }
    if(rt==0) root=x;
    /*
    	每一棵树都要有一个最终极的根节点,如果x不能成为rt的孩子节点的话
		说明x就是最终级的根节点 
	*/ 
}

这一步是非常重要的,因为伸展树最大的作用就是把访问过的移到根节点的位置,那么这样来说,就可以通过rotate这个函数来实现,所以代码要记住,建议不要死背,按照每一种方法的图来背是最有效的。注意一下,调整的只是我们选中以x为中心的父亲和爷爷,以及孩子,出来这四方,在x孩子的以下是不会受到改变的,他们的孩子节点跟着父亲走就可以了。

rotate的几种情况

也就是旋转的情况,这个是挺重要的一个函数

Zig Step

当p为根节点时,进行zip step操作。

当x是p的左孩子时,对x右旋;

当x是p的右孩子时,对x左旋。

Zig-Zig Step

当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。

当x和p同为左孩子时,依次将p和x右旋;

当x和p同为右孩子时,依次将p和x左旋。

Zig-Zag Step

当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。

当p为左孩子,x为右孩子时,将x左旋后再右旋。

当p为右孩子,x为左孩子时,将x右旋后再左旋。

这里只有一个图,但是规律就是这样的。

 找某个值的编号的函数

int findip(int d)
//找值为d的节点的地址,补充:如果不存在d,就找到有可能是接近d的(或大或小)
{
    int x=root;//root表示的是根节点,从根节点出发,x是我们找到的合适的值 
    //接下来就要判断往左边走还是往右边走 
    while(tr[x].d!=d)//如果根节点的值等于要找的d的值,就不用找了 
    {
        if(d<tr[x].d)//如果d小于根节点值 
        {
            if(tr[x].son[0]==0) break;
			/*
				那就往左边找,因为左孩子小于根节点
				如果没有左孩子,就退出,因为找不到合适的,只能去较大的右边找 
			*/ 
            else x=tr[x].son[0];
            /*
				如果有左孩子,那么x就为根节点的左孩子,因为最开始的是最接近的
				再往下的也不及根节点自己的孩子和根节点最近 
		    */ 
        }
        else//if(tr[x].d<d) //如果d大于根节点的值 
        {
            if(tr[x].son[1]==0) break;
            /*
            	那就往右边找,因为右孩子大于根节点
				如果没有右孩子,就退出,因为找不到合适的,只能去较小的地方找 
            */
            else x=tr[x].son[1];//如果有右孩子,那么x就为根节点的右孩子 
        }
    }
    return x;
    /*
    	返回x的编号 
		伸展树就是保存了我们访问过的所有数据,使得最快的找到我们要找的
		而这一步其实就解决了我们题目中的第5步和第6步,找前驱和找后继 
	*/ 
}

显然这一步是为了后面的寻找的,这在寻找中会起到很重要的作用,知道一个值是不够的,要知道这个值的编号才能进行整一棵伸展树的调整。 

插入的函数

void ins(int d)
//插入数值为d的一个节点 
//这可以说是伸展树的一大亮点,插入和删除,是之前所有树形结构所做不到的 
{
    if(root==0) {add(d,0); root=len; return ;}
    /*
		root为0,说明没有根节点,表示这是一棵空树
		既然没有,那就增加一个点,父亲为0,也就是当前的root
		root不能等于1,因为len是全局变量
		但是如果原来有一棵树但是被全部删掉之后,len是没有清除数据的
		所以这个时候我们就要接着len往下建树 
	*/
    
    int x=findip(d);//先看看能不能找到d 
    if(tr[x].d==d)//如果在这棵树中找到了d,那就很简单了 
	//比如说,要找7,但是编号为3的节点的值就为7,
	//那就直接增加编号3的n(相同值的个数)就可以了 
    {
        tr[x].n++;//直接把x相同的再增加一个,就算插入了 
        update(x);//更新x控制的人数,就是增加一个人 
        splay(x,0);
        /*
        	把x提高到根节点
			因为增加了一个,但是这个数据要汇报给根节点
			所以就是要让x为根节点
			提高的过程中不断旋转,不断更新孩子与父亲的关系
			所以我们找到的这个7的节点在跳的过程中
			会不断告诉别人7控制了多少个节点
			这样就不会混乱,也不会影响后面 
        */
    }
    else//如果找不到 
    {
        add(d,x);//增加一个值为d的点 
        update(x);//更新x 
        splay(len,0);
		/*
			新的这个点要拉上去,作为根节点
			成为根节点就是伸展树最神奇的地方
			因为伸展树把访问过的点都提拔到了根节点
			因为他觉得之后还会访问,而且也确实如此,所以才能够更快的实现寻找 
		*/ 
    }
}

插入要判断,判断是否要真正意义上的插入,还是只是增加一个相同的值。每一次的插入,都要更新节点数,所以update是一个众观全局的函数。 

删除的函数

void del(int d)//删除数值为d的一个节点
{
    int x=findip(d); splay(x,0);
    /*
		找人,并且让找到的这个人旋转到根节点
		这就是我们伸展树的优点,把访问过的旋转到根节点 
	*/ 
      
    if(tr[x].n>1) {tr[x].n--; update(x); return ;}
	//如果重复度大于一,减少一个然后再更新一下就好了 
      
         if(tr[x].son[0]==0 && tr[x].son[1]==0) {root=0; len=0;}
         /*
         	我们已经把这个点提到了根节点的话
        	而如果我们要删的这个点既没有左孩子也没有右孩子 
			那就说明全世界只有他一个点,那删掉之后就什么都为0
			根节点为0,节点数也为0 
         */
    else if(tr[x].son[0]==0 && tr[x].son[1]!=0) {root=tr[x].son[1]; tr[root].f=0;}
    /*
    	如果这个点没有左孩子但是有右孩子的话
		右孩子成为根节点,并且这个右孩子没有父亲节点 
    */
    else if(tr[x].son[0]!=0 && tr[x].son[1]==0) {root=tr[x].son[0]; tr[root].f=0;}
    /*
    	如果这个点没有右孩子但是有左孩子的话
		左孩子成为根节点,并且这个左孩子没有父亲节点 
    */
    else//if(tr[x].son[0]!= 0 && tr[x].son[1]!=0) //既有左孩子,也有右孩子 
    {
        int p=tr[x].son[0];//定义p为x的左孩子 
		while(tr[p].son[1]!=0)//如果p有右孩子的话
		{ 
			p=tr[p].son[1];//那么p就更新为自己的右孩子 
			splay(p,x);//把右孩子旋转到x的孩子节点,也就是转到p的位置
			/*
				一直往右边跳,因为右边是比根节点的值要大的,所以往右边 
			*/ 
      	}//循环到没有有孩子的时候,这个值就是最大的
		//又因为没有这个p点没有了右孩子,所以就可以收x的右孩子成为自己的右孩子 
      	
        int r=tr[x].son[1];//小人为x节点的右孩子 
		int R=p;//大人为p,也就是x节点的右孩子成为p节点的右孩子
		/*
				 4         经过第一次             4
		              /     \      转动了3               /  
			     2       6     而且4的右孩子也       3
			    / \     / \    成为了3的右孩子      / \
			   1   3   5   7                     2   6
			                                    /   / \
			                                   1   5   7
			这个时候就成为了我们要的,只有一个孩子节点 
		*/ 
          
        tr[R].son[1]=r;
        tr[r].f=R;//定下结论,我们现在只有一个子树了 
          
        root=R; tr[root].f=0;
		/*
			这个时候新的root就等于我们找到的最大的值
			目的就是把每一个访问过的都记录下来 
		*/ 
        update(R);//更新这一整棵树就好了 
    }
}

删除在意义上和插入有几分相似,大概也是判断直接删除重复的值还是删除单个的值,但是这里要比插入复杂一点,因为我们删掉的那个值之后,可能会导致整棵伸展树的倒塌,所以在背代码的时候要考虑清楚这些细节的东西。 

找排名的函数

int findpaiming(int d)//找排名 
{
    int x=findip(d); splay(x,0);
	//先找到这个值,然后让他成为根节点   
    return tr[tr[x].son[0]].c+1;
    //左孩子的控制人数再+1就是自己的排名
	/*
			 100       第一次      100        第二次         23
			 / \	   移动23      / \        旋转23          \ 
			55 120                23 120                      100
		       /     \                 \   \                      / \  
		      23     144               55  144                   55 144
		      \                       /                         /  
		      34                     34                        34 
		      /                      /                         /
		     30                     30                        30
		比如说我们要找23的排名
		排名为1
		这样就对了因为我们要找的是从小到大的排名
		所以23最小就为1 
	*/ 
}

找排名是极其简单的一个函数,排名是值从小到大排序,只要搞清楚为什么是左孩子控制的人数+1就是自己的排名就可以了。 

 找某个排名对应的值的函数

int findzhi(int k)//找排名为k的值
{
    int x=root;//定义x为根节点,从根节点开始找 
    while(1)//
    {
        int lc=tr[x].son[0]; int rc=tr[x].son[1];//左边和右边 
        if(k<=tr[lc].c) x=lc;
        /*
        	如果k的这个排名比左边控制的人数还要少
			就去左边找
			这个时候就把左边设置为要继续往下找的一个终点位置
			其实就是伸展树的好处,记录访问过的 
        */
        else if(k>tr[lc].c+tr[x].n)
        /*
        	如果这个排名比(左边控制的人数+根节点重复的节点数)都要大
			就去右边找 
        */
		{
			k-=tr[lc].c+tr[x].n; 
			/*
				注意:光继续在右边找还不够
				要减去(左边的控制人数+根节点重复的节点数)
				比如说:我们要找17 
				          3
					 / \
					10  ? 
				这个时候右边控制的人数+根节点重复的节点数=13
				比17要小,说明我们要去右边找
				去右边找的就是 17-13=4,找排名为4的节点的值 
			*/
			x=rc;//去右边继续找
		}
        else break;//否则要找的排名就在根节点中间 
    }
    splay(x,0);//把找到的合适的移到根节点 
    return tr[x].d;//把我们找到的这个节点的值返回给函数findzhi 
}

这个要稍微复杂一点,因为你要判断当前这个排名是在左孩子还是右孩子,其他的就很简单了。 

找前驱的函数

int findqianqu(int d)//找前驱
{
    int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
    if(d<=tr[x].d && tr[x].son[0]!=0)
	//如果是if( d<tr[x].d && tr[x].son[0]!=0 )则找到的是:小于等于d的前驱
	//如果这个值比根节点的值要小,并且有左孩子的话
    {
        x=tr[x].son[0];//把这个点的左孩子移到根节点 
        while(tr[x].son[1]!=0) x=tr[x].son[1];
        /*
			找完之后一直往右边跳(也就是寻找),找右边的最大值
			前驱是比要找的值小的最大值
			所以只要是左孩子的话就一定比d要小
			那么左孩子的右孩子就是比d小而且是比左孩子要大的
			这样就可以找到最大的值 
		*/
    }
    if(tr[x].d>=d) x=0;//如果是if(tr[x].d>d)则找到的是:小于等于d的前驱
    /*
    	如果我们找到的这个值大于等于d的话
		说明以d为根节点的这棵数没有左孩子
		那就说明没有合适的前驱
		就只能为0 
    */
    return x;//返回x的值 
}

找前驱,一点都不难,唯一要注意的就是要搞清楚一个节点的左孩子的值比自己要小,右孩子的值比自己要大,还有一个就是要判断没有前驱的情况。大概就是这三种。 

找后继的函数

int finddouji(int d)//找后继
{
    int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
    if(tr[x].d<=d && tr[x].son[1]!=0)
    //如果这个值比根节点的值要大,并且有右孩子的话
    {
        x=tr[x].son[1];//把这个点的右孩子移到根节点
        while(tr[x].son[0]!=0) x=tr[x].son[0];
        /*
			找完之后一直往左边跳(也就是寻找),找左边的最小值
			后继是比要找的值大的最小值
			所以只要是右孩子的话就一定比d要大 
			那么右孩子的左孩子就是比d大而且是比右孩子要小的
			这样就可以找到最小的值 
		*/ 
    }
    if(tr[x].d<=d) x=0;
    /*
    	如果我们找到的这个值小于等于d的话
		说明以d为根节点的这棵数没有右孩子
		那就说明没有合适的后继 
		就只能为0 
    */
    return x;//返回x的值 
}

找后继,跟找前驱一样的道理。 

智障的主函数

int main()
{
    int n; n=read();
    root=0; len=0;//初始化没有根节点,也没有节点 
    for(int i=1;i<=n;i++)
    {
        int cz,x; cz=read(); x=read();
             if(cz==1) ins(x);//插入 
        else if(cz==2) del(x);//删除 
        else if(cz==3) printf("%d\n",findpaiming(x));//找排名 
        else if(cz==4) printf("%d\n",findzhi(x));//找排名值 
        else if(cz==5) printf("%d\n",tr[findqianqu(x)].d);//找前驱 
        else if(cz==6) printf("%d\n",tr[finddouji(x)].d);//找后继 
    }
    return 0;
}

不解释 

最后,我的思路相对来讲会没有那么完善,但是详解都在代码里面了,把函数的作用搞清楚就可以了。

完整代码

/*
    要求:画图理解并且默打
    记住:是理解性默打(不然背死你) 
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
inline int read()
{
    char c=getchar();
    int x=0,f=1;
    while(c<48 || c>57)
    {
        if(c=='-') f=-1;
        c=getchar();
    }
    while(c>=48 && c<=57)
    {
        x=x*10+c-48;
        c=getchar();
    }
    return x*f;
}
int root;//存储根节点 
struct trnode
{
    int d,n,c,f,son[2];
    /*
        d为值,f为父亲的编号,
        c为控制的节点个数,以他为根节点的那棵树的所有的节点数 
        n为同值的节点个数,把多个同样的值浓缩成一个结构体 
        (这一步是为了省空间,比如说3个为100的数,可以说这个d为100,n为3) 
        son[0]为左孩子,son[1]为右孩子 
    */
}tr[110000]; int len;//len表示用到了第几个节点 
   
void update(int x)//更新编号为x的节点所控制的节点数 
{
    int lc=tr[x].son[0];//左孩子的编号 
    int rc=tr[x].son[1];//右孩子的编号 
    tr[x].c=tr[lc].c+tr[rc].c+tr[x].n;
    //x总共的节点数=左孩子的节点数+右孩子的节点数+同值的数 
}
void add(int d,int f)//添加值为d的点,认f为父亲,同时,f也认他为孩子 
{
    len++;//增加一个节点数 
    tr[len].d=d; tr[len].n=1; tr[len].c=1; 
    /*
    	这一步是关于加入的值的
		加入的这个值的值就是定义的d——值,
		然后只有他自己一个,所以n=1
		同时他控制的节点数也只有他自己一个 
    */
    tr[len].f=f; if(d<tr[f].d) tr[f].son[0]=len; else tr[f].son[1]=len;
    /*
    	这一步是关于他认的父亲的操作
		这个节点的父亲就是定义的f-父亲
		我们默认比父亲节点的值小的为左孩子,比父亲节点的值大的为右孩子
		所以说如果加入的这个节点的值比父亲节点的值大,就为左孩子,否则右孩子
		状态:左小右大 
		
		这里可能会考虑到如果这个节点原本就有孩子怎么办?
		这个的话我也解释不清楚,
		因为我们是增加进去的
		所以我们只要找到合适的位置插入就好了
		比如说
		                8
		              /   \
			     3     25
			    / \   /  \
		           2   4 20  30
		 假如我们要插入10的话,10离4最近,所以应该插入到4的下面,
		 但是这一整棵子树的每一个节点都要比根节点小
		 所以这样的话,这个10就只能认20为父亲,也是最接近的答案了 
	*/ 
    tr[len].son[0]=tr[len].son[1]=0;
    /*
    	定义最开始的加入的这个点,是一个叶子节点 
		既没有左孩子,也没有右孩子 
    */
}
void rotate(int x,int w)
/*
	这是整个代码当中的一个关键点
	首先我们定义了x是我们要选择旋转的节点
	w有两个形式,一个是0,一个是1
	0表示左转,1表示右转
	(x,0)表示x这个点左转
	(x,1)表示x这个点右转
	注意,我们要转的可能是x,但是变化的不止x,和x有关系的也有变动 
*/ 
{
    int y=tr[x].f; int z=tr[y].f;//x在旋转之前,要确定x的父亲y和爷爷z 
    //下来建立关系 
    int r,R;//r表示儿辈,R表示父辈 (ren,Ren) 
    //有四个角色:我x,我的儿子,我的父亲,我的爷爷
	/*
		接下来的就是在旋转的时候发生的关系
		x为左孩子才可以右转,为右孩子才可以左转 
 		在这里可能要有图才讲得清楚 
 		      y                               x  
 		     / \                             / \     
 		    c   x                           y   b
 		       / \                         / \    
 		      a   b                       c   a
 		左转前                            左转后
		 
		          y                               x
			 / \                             / \
			x   c                           a   y
		       / \                             / \
		      a   b                           b   c
		  右转前                              右转后
		稍微解释一下:
		我们之前定义过左孩子的值比父亲节点的值要小,
		右孩子比父亲节点的值要大,是吧?
		那么这个时候我们就可以看到,
		x左转之后一定跟y换了位置,这个是必然的,
		然后,y是比x小的(x是y的右孩子),
		所以x替代了y的位置之后,y就成为了x的左孩子(比x小),
		然后c是y的右孩子,比y小,旋转之后跟着y成为y的右孩子即可。
		然后我们知道b是右孩子,比x要大,所以依旧成为x的右孩子即可,
		那么剩下a,首先我们知道a是比x小的,但是总体来看是比y要大的,
		因为x比y大,所以a也比y大,然而x的左右孩子都有了,
		y还有右孩子的空位,那么a又比y大,所以a在y的右孩子的位置刚刚好。
 
	*/ 
	//更换过程是从下到上的,而且是儿子先认父亲,父亲再认儿子 
    r=tr[x].son[w]; R=y;//x的儿子->准备当新儿子
	/*
		左边旋转的话,x的左孩子就变成别人的孩子;
		右边旋转的话,x的右孩子就变成别人的孩子。 
		然后这个孩子的新父亲就是x的父亲y
	*/ 
    tr[R].son[1-w]=r;
    /*
    	左边旋转的话,x的左孩子就变成y的右孩子;
					  x的右孩子仍然是x的右孩子
		右边旋转的话,x的右孩子就变成y的左孩子。
					  x的左孩子仍然是x的左孩子 
    */
    if(r!=0) tr[r].f=R;
    //如果这个x的孩子节点不是0的话,这个孩子节点的父亲就是前面认过的y节点
      
    r=x; R=z;//x->准备当新儿子 
    if(tr[R].son[0]==y) tr[R].son[0]=r; 
    /*
    	首先我们知道,x左转之后就变成了z的孩子节点,
		因为y原来是z的孩子,现在x代替了y的位置
		所以z就是x的父亲节点
	*/ 
    //如果y所在的是z的左孩子,那么x的位置就是z的左孩子 
    else tr[R].son[1]=r; 
    //否则就为z的右孩子,其实就是顶替的y的位置,其他不变
    tr[r].f=R;
    //x的父亲节点变为z
      
    r=y; R=x;//x的父亲y->准备当新儿子
	//y这个时候变成了孩子节点,他的父亲节点是x 
    tr[R].son[w]=r;
    /* 
	    左转的话,y就是x的左孩子
				  y的左孩子仍然是y的左孩子
		右转的话,y就是x的右孩子
			      y的右孩子仍然是y的右孩子
	*/ 
    tr[r].f=R;
    //x就是y的父亲节点
          
    update(y);//先更新处于下层的点y,因为我们是先换下面的 
    update(x);//再更新上层的x,后换上面的 
}
  
void splay(int x,int rt)
//该函数的功能是:经过旋转之后,使x成为rt的孩子(左右都可以) 
//最关键的操作 
{
    while(tr[x].f!=rt)//如果rt是x的父亲,则什么都不用做,否则x就要不断向上旋转
    {
        int f=tr[x].f; int ff=tr[f].f;//准备x的父亲和爷爷
        if(ff==rt)//如果x的爷爷是rt,那么x只需要旋转一次(相当于跳一层)
        {
            if(tr[f].son[0]==x) rotate(x,1); else rotate(x,0);
            //如果x是f的左孩子的话,就右旋,也只能右旋
			//如果x是f的右孩子的话,就左旋,也只能左旋 
        }
        else//rt在ff的上面 
        {
                 if(tr[ff].son[0]==f && tr[f].son[0]==x) {rotate(f,1); rotate(x,1);}
            /*
            	      ff   第一次右转    f      第二次右转     x 
		      /     f变成爷爷   / \     x变成爷爷       \
		     f                x   ff                    f
		    /                                             \  
		   x                                               ff
			*/ 
            else if(tr[ff].son[1]==f && tr[f].son[1]==x) {rotate(f,0); rotate(x,0);}
            /*
                      ff   第一次左转   f      第二次右转      x           
                        \  f变成爷爷   / \     x变成爷爷      /   
                         f           ff  x                 f
                          \                               /
                           x                             ff
            */ 
            else if(tr[ff].son[0]==f && tr[f].son[1]==x) {rotate(x,0); rotate(x,1);}
            /*
            	      ff  第一次左转   ff      第二次右转      x 
            	     /    x变成父亲  /        x变成爷爷      / \ 
            	    f              x                      f   ff
		     \            /
		      x          f  
				   这一次的旋转比较特殊,如果f右转的话就会出现这样的情况
					  f
					   \
					    ff
					    /
					   x
					转了跟没转一样,所以只能转x,不能动y 
			*/ 
            else if(tr[ff].son[1]==f && tr[f].son[0]==x) {rotate(x,1); rotate(x,0);}
            /*
            	      ff  第一次右转   ff      第二次右转     x 
            	        \ x变成父亲      \     x变成爷爷     / \ 
            	         f                x               ff  f   
			/                  \
		       x                    f
					跟上面一样也是只能转x,不能转y 
            */
        }
    }
    if(rt==0) root=x;
    /*
    	每一棵树都要有一个最终极的根节点,如果x不能成为rt的孩子节点的话
		说明x就是最终级的根节点 
	*/ 
}
int findip(int d)
//找值为d的节点的地址,补充:如果不存在d,就找到有可能是接近d的(或大或小)
{
    int x=root;//root表示的是根节点,从根节点出发,x是我们找到的合适的值 
    //接下来就要判断往左边走还是往右边走 
    while(tr[x].d!=d)//如果根节点的值等于要找的d的值,就不用找了 
    {
        if(d<tr[x].d)//如果d小于根节点值 
        {
            if(tr[x].son[0]==0) break;
			/*
				那就往左边找,因为左孩子小于根节点
				如果没有左孩子,就退出,因为找不到合适的,只能去较大的右边找 
			*/ 
            else x=tr[x].son[0];
            /*
				如果有左孩子,那么x就为根节点的左孩子,因为最开始的是最接近的
				再往下的也不及根节点自己的孩子和根节点最近 
		    */ 
        }
        else//if(tr[x].d<d) //如果d大于根节点的值 
        {
            if(tr[x].son[1]==0) break;
            /*
            	那就往右边找,因为右孩子大于根节点
				如果没有右孩子,就退出,因为找不到合适的,只能去较小的地方找 
            */
            else x=tr[x].son[1];//如果有右孩子,那么x就为根节点的右孩子 
        }
    }
    return x;
    /*
    	返回x的编号 
		伸展树就是保存了我们访问过的所有数据,使得最快的找到我们要找的
		而这一步其实就解决了我们题目中的第5步和第6步,找前驱和找后继 
	*/ 
}
void ins(int d)
//插入数值为d的一个节点 
//这可以说是伸展树的一大亮点,插入和删除,是之前所有树形结构所做不到的 
{
    if(root==0) {add(d,0); root=len; return ;}
    /*
		root为0,说明没有根节点,表示这是一棵空树
		既然没有,那就增加一个点,父亲为0,也就是当前的root
		root不能等于1,因为len是全局变量
		但是如果原来有一棵树但是被全部删掉之后,len是没有清除数据的
		所以这个时候我们就要接着len往下建树 
	*/
    
    int x=findip(d);//先看看能不能找到d 
    if(tr[x].d==d)//如果在这棵树中找到了d,那就很简单了 
	//比如说,要找7,但是编号为3的节点的值就为7,
	//那就直接增加编号3的n(相同值的个数)就可以了 
    {
        tr[x].n++;//直接把x相同的再增加一个,就算插入了 
        update(x);//更新x控制的人数,就是增加一个人 
        splay(x,0);
        /*
        	把x提高到根节点
			因为增加了一个,但是这个数据要汇报给根节点
			所以就是要让x为根节点
			提高的过程中不断旋转,不断更新孩子与父亲的关系
			所以我们找到的这个7的节点在跳的过程中
			会不断告诉别人7控制了多少个节点
			这样就不会混乱,也不会影响后面 
        */
    }
    else//如果找不到 
    {
        add(d,x);//增加一个值为d的点 
        update(x);//更新x 
        splay(len,0);
		/*
			新的这个点要拉上去,作为根节点
			成为根节点就是伸展树最神奇的地方
			因为伸展树把访问过的点都提拔到了根节点
			因为他觉得之后还会访问,而且也确实如此,所以才能够更快的实现寻找 
		*/ 
    }
}
void del(int d)//删除数值为d的一个节点
{
    int x=findip(d); splay(x,0);
    /*
		找人,并且让找到的这个人旋转到根节点
		这就是我们伸展树的优点,把访问过的旋转到根节点 
	*/ 
      
    if(tr[x].n>1) {tr[x].n--; update(x); return ;}
	//如果重复度大于一,减少一个然后再更新一下就好了 
      
         if(tr[x].son[0]==0 && tr[x].son[1]==0) {root=0; len=0;}
         /*
         	我们已经把这个点提到了根节点的话
        	而如果我们要删的这个点既没有左孩子也没有右孩子 
			那就说明全世界只有他一个点,那删掉之后就什么都为0
			根节点为0,节点数也为0 
         */
    else if(tr[x].son[0]==0 && tr[x].son[1]!=0) {root=tr[x].son[1]; tr[root].f=0;}
    /*
    	如果这个点没有左孩子但是有右孩子的话
		右孩子成为根节点,并且这个右孩子没有父亲节点 
    */
    else if(tr[x].son[0]!=0 && tr[x].son[1]==0) {root=tr[x].son[0]; tr[root].f=0;}
    /*
    	如果这个点没有右孩子但是有左孩子的话
		左孩子成为根节点,并且这个左孩子没有父亲节点 
    */
    else//if(tr[x].son[0]!= 0 && tr[x].son[1]!=0) //既有左孩子,也有右孩子 
    {
        int p=tr[x].son[0];//定义p为x的左孩子 
		while(tr[p].son[1]!=0)//如果p有右孩子的话
		{ 
			p=tr[p].son[1];//那么p就更新为自己的右孩子 
			splay(p,x);//把右孩子旋转到x的孩子节点,也就是转到p的位置
			/*
				一直往右边跳,因为右边是比根节点的值要大的,所以往右边 
			*/ 
      	}//循环到没有有孩子的时候,这个值就是最大的
		//又因为没有这个p点没有了右孩子,所以就可以收x的右孩子成为自己的右孩子 
      	
        int r=tr[x].son[1];//小人为x节点的右孩子 
		int R=p;//大人为p,也就是x节点的右孩子成为p节点的右孩子
		/*
				 4         经过第一次             4
		              /     \      转动了3               /  
			     2       6     而且4的右孩子也       3
			    / \     / \    成为了3的右孩子      / \
			   1   3   5   7                     2   6
			                                    /   / \
			                                   1   5   7
			这个时候就成为了我们要的,只有一个孩子节点 
		*/ 
          
        tr[R].son[1]=r;
        tr[r].f=R;//定下结论,我们现在只有一个子树了 
          
        root=R; tr[root].f=0;
		/*
			这个时候新的root就等于我们找到的最大的值
			目的就是把每一个访问过的都记录下来 
		*/ 
        update(R);//更新这一整棵树就好了 
    }
}
int findpaiming(int d)//找排名 
{
    int x=findip(d); splay(x,0);
	//先找到这个值,然后让他成为根节点   
    return tr[tr[x].son[0]].c+1;
    //左孩子的控制人数再+1就是自己的排名
	/*
			 100       第一次      100        第二次         23
			 / \	   移动23      / \        旋转23          \ 
			55 120                23 120                      100
		       /     \                 \   \                      / \  
		      23     144               55  144                   55 144
		      \                       /                         /  
		      34                     34                        34 
		      /                      /                         /
		     30                     30                        30
		比如说我们要找23的排名
		排名为1
		这样就对了因为我们要找的是从小到大的排名
		所以23最小就为1 
	*/ 
}
int findzhi(int k)//找排名为k的值
{
    int x=root;//定义x为根节点,从根节点开始找 
    while(1)//
    {
        int lc=tr[x].son[0]; int rc=tr[x].son[1];//左边和右边 
        if(k<=tr[lc].c) x=lc;
        /*
        	如果k的这个排名比左边控制的人数还要少
			就去左边找
			这个时候就把左边设置为要继续往下找的一个终点位置
			其实就是伸展树的好处,记录访问过的 
        */
        else if(k>tr[lc].c+tr[x].n)
        /*
        	如果这个排名比(左边控制的人数+根节点重复的节点数)都要大
			就去右边找 
        */
		{
			k-=tr[lc].c+tr[x].n; 
			/*
				注意:光继续在右边找还不够
				要减去(左边的控制人数+根节点重复的节点数)
				比如说:我们要找17 
				          3
					 / \
					10  ? 
				这个时候右边控制的人数+根节点重复的节点数=13
				比17要小,说明我们要去右边找
				去右边找的就是 17-13=4,找排名为4的节点的值 
			*/
			x=rc;//去右边继续找
		}
        else break;//否则要找的排名就在根节点中间 
    }
    splay(x,0);//把找到的合适的移到根节点 
    return tr[x].d;//把我们找到的这个节点的值返回给函数findzhi 
}
int findqianqu(int d)//找前驱
{
    int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
    if(d<=tr[x].d && tr[x].son[0]!=0)
	//如果是if( d<tr[x].d && tr[x].son[0]!=0 )则找到的是:小于等于d的前驱
	//如果这个值比根节点的值要小,并且有左孩子的话
    {
        x=tr[x].son[0];//把这个点的左孩子移到根节点 
        while(tr[x].son[1]!=0) x=tr[x].son[1];
        /*
			找完之后一直往右边跳(也就是寻找),找右边的最大值
			前驱是比要找的值小的最大值
			所以只要是左孩子的话就一定比d要小
			那么左孩子的右孩子就是比d小而且是比左孩子要大的
			这样就可以找到最大的值 
		*/
    }
    if(tr[x].d>=d) x=0;//如果是if(tr[x].d>d)则找到的是:小于等于d的前驱
    /*
    	如果我们找到的这个值大于等于d的话
		说明以d为根节点的这棵数没有左孩子
		那就说明没有合适的前驱
		就只能为0 
    */
    return x;//返回x的值 
}
int finddouji(int d)//找后继
{
    int x=findip(d); splay(x,0);//找到d的编号,使他成为根节点
    if(tr[x].d<=d && tr[x].son[1]!=0)
    //如果这个值比根节点的值要大,并且有右孩子的话
    {
        x=tr[x].son[1];//把这个点的右孩子移到根节点
        while(tr[x].son[0]!=0) x=tr[x].son[0];
        /*
			找完之后一直往左边跳(也就是寻找),找左边的最小值
			后继是比要找的值大的最小值
			所以只要是右孩子的话就一定比d要大 
			那么右孩子的左孩子就是比d大而且是比右孩子要小的
			这样就可以找到最小的值 
		*/ 
    }
    if(tr[x].d<=d) x=0;
    /*
    	如果我们找到的这个值小于等于d的话
		说明以d为根节点的这棵数没有右孩子
		那就说明没有合适的后继 
		就只能为0 
    */
    return x;//返回x的值 
}
int main()
{
    int n; n=read();
    root=0; len=0;//初始化没有根节点,也没有节点 
    for(int i=1;i<=n;i++)
    {
        int cz,x; cz=read(); x=read();
             if(cz==1) ins(x);//插入 
        else if(cz==2) del(x);//删除 
        else if(cz==3) printf("%d\n",findpaiming(x));//找排名 
        else if(cz==4) printf("%d\n",findzhi(x));//找排名值 
        else if(cz==5) printf("%d\n",tr[findqianqu(x)].d);//找前驱 
        else if(cz==6) printf("%d\n",tr[finddouji(x)].d);//找后继 
    }
    return 0;
}

 

 

 

 

猜你喜欢

转载自blog.csdn.net/qq_42367531/article/details/84591736