PHP算法 《树形结构》 之 伸展树(1) - 基本概念

伸展树的介绍

一、出处:http://dongxicheng.org/structure/splay-tree/html

A、 概述 算法

 

二叉查找树(Binary Search Tree,也叫二叉排序树,即Binary Sort Tree)可以支持多种动态集合操做,它能够用来表示有序集合、创建索引等,于是在实际应用中,二叉排序树是一种很是重要的数据结构。编程

 

从算法复杂度角度考虑,咱们知道,做用于二叉查找树上的基本操做(如查找,插入等)的时间复杂度与树的高度成正比。对一个含n个节点的彻底二叉树,这些操做的最坏状况运行时间为O(log n)。但若是由于频繁的删除和插入操做,致使树退化成一个n个节点的线性链(此时即为一个单链表),则这些操做的最坏状况运行时间为O(n)。为了克服以上缺点,不少二叉查找树的变形出现了,如红黑树、AVL树,Treap树等。网络

 

本文介绍了二叉查找树的一种改进数据结构–伸展树(Splay Tree)。它的主要特色是不会保证树一直是平衡的,但各类操做的平摊时间复杂度是O(log n),于是,从平摊复杂度上看,二叉查找树也是一种平衡二叉树。另外,相比于其余树状数据结构(如红黑树,AVL树等),伸展树的空间要求与编程复杂度要小得多。数据结构

 

B、 基本操做性能

 

伸展树的出发点是这样的:考虑到局部性原理(刚被访问的内容下次可能仍会被访问,查找次数多的内容可能下一次会被访问),为了使整个查找时间更小,被查频率高的那些节点应当常常处于靠近树根的位置。这样,很容易得想到如下这个方案:每次查找节点以后对树进行重构,把被查找的节点搬移到树根,这种自调整形式的二叉查找树就是伸展树。每次对伸展树进行操做后,它均会经过旋转的方法把被访问节点旋转到树根的位置。学习

 

为了将当前被访问节点旋转到树根,咱们一般将节点自底向上旋转,直至该节点成为树根为止。“旋转”的巧妙之处就是在不打乱数列中数据大小关系(指中序遍历结果是全序的)状况下,全部基本操做的平摊复杂度仍为O(log n)。spa

 

伸展树主要有三种旋转操做,分别为单旋转,一字形旋转和之字形旋转。为了便于解释,咱们假设当前被访问节点为X,X的父亲节点为Y(若是X的父亲节点存在),X的祖父节点为Z(若是X的祖父节点存在)。设计

 

(1)    单旋转3d

 

节点X的父节点Y是根节点。这时,若是X是Y的左孩子,咱们进行一次右旋操做;若是X 是Y 的右孩子,则咱们进行一次左旋操做。通过旋转,X成为二叉查找树T的根节点,调整结束。

 

 

(2)    一字型旋转

 

节点X 的父节点Y不是根节点,Y 的父节点为Z,且X与Y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,咱们进行一次左左旋转操做或者右右旋转操做。

 

 

(3)    之字形旋转

 

节点X的父节点Y不是根节点,Y的父节点为Z,X与Y中一个是其父节点的左孩子而另外一个是其父节点的右孩子。这时,咱们进行一次左右旋转操做或者右左旋转操做。

 

 

C、伸展树区间操做

 

在实际应用中,伸展树的中序遍历即为咱们维护的数列,这就引出一个问题,怎么在伸展树中表示某个区间?好比咱们要提取区间[a,b],那么咱们将a前面一个数对应的结点转到树根,将b 后面一个结点对应的结点转到树根的右边,那么根右边的左子树就对应了区间[a,b]。缘由很简单,将a 前面一个数对应的结点转到树根后, a 及a 后面的数就在根的右子树上,而后又将b后面一个结点对应的结点转到树根的右边,那么[a,b]这个区间就是下图中B所示的子树。

 

 

利用区间操做咱们能够实现线段树的一些功能,好比回答对区间的询问(最大值,最小值等)。具体能够这样实现,在每一个结点记录关于以这个结点为根的子树的信息,而后询问时先提取区间,再直接读取子树的相关信息。还能够对区间进行总体修改,这也要用到与线段树相似的延迟标记技术,即对于每一个结点,额外记录一个或多个标记,表示以这个结点为根的子树是否被进行了某种操做,而且这种操做影响其子结点的信息值,当进行旋转和其余一些操做时相应地将标记向下传递。

 

与线段树相比,伸展树功能更强大,它能解决如下两个线段树不能解决的问题:

 

(1) 在a后面插入一些数。方法是:首先利用要插入的数构造一棵伸展树,接着,将a 转到根,并将a 后面一个数对应的结点转到根结点的右边,最后将这棵新的子树挂到根右子结点的左子结点上。

 

(2)  删除区间[a,b]内的数。首先提取[a,b]区间,直接删除便可。

 

二、出处:http://www.cnblogs.com/skywang12345/p/3604238.html

伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操做。它由Daniel Sleator和Robert Tarjan创造。
(01) 伸展树属于二叉查找树,即它具备和二叉查找树同样的性质:假设x为树中的任意一个结点,x节点包含关键字key,节点x的key值记为key[x]。若是y是x的左子树中的一个结点,则key[y] <= key[x];若是y是x的右子树的一个结点,则key[y] >= key[x]。
(02) 除了拥有二叉查找树的性质以外,伸展树还具备的一个特色是:当某个节点被访问时,伸展树会经过旋转使该节点成为树根。这样作的好处是,下次要访问该节点时,可以迅速的访问到该节点。

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

相比于"二叉查找树"和"AVL树",学习伸展树时须要重点关注是"伸展树的旋转算法"。

旋转

旋转的代码:

/* 
 * 旋转key对应的节点为根节点,并返回根节点。
 */
Node* splaytree_splay(SplayTree tree, Type key)
{
    Node N, *l, *r, *c;

    if (tree == NULL) 
        return tree;

    N.left = N.right = NULL;
    l = r = &N;

    for (;;)
    {
        if (key < tree->key)
        {
            if (tree->left == NULL)
                break;
            if (key < tree->left->key)
            {
                c = tree->left;                           /* 01, rotate right */
                tree->left = c->right;
                c->right = tree;
                tree = c;
                if (tree->left == NULL) 
                    break;
            }
            r->left = tree;                               /* 02, link right */
            r = tree;
            tree = tree->left;
        }
        else if (key > tree->key)
        {
            if (tree->right == NULL) 
                break;
            if (key > tree->right->key) 
            {
                c = tree->right;                          /* 03, rotate left */
                tree->right = c->left;
                c->left = tree;
                tree = c;
                if (tree->right == NULL) 
                    break;
            }
            l->right = tree;                              /* 04, link left */
            l = tree;
            tree = tree->right;
        }
        else
        {
            break;
        }
    }

    l->right = tree->left;                                /* 05, assemble */
    r->left = tree->right;
    tree->left = N.right;
    tree->right = N.left;

    return tree;
}

上面的代码的做用:将"键值为key的节点"旋转为根节点,并返回根节点。它的处理状况共包括:

(a):伸展树中存在"键值为key的节点"。
        将"键值为key的节点"旋转为根节点。
(b):伸展树中不存在"键值为key的节点",而且key < tree->key。
        b-1) "键值为key的节点"的前驱节点存在的话,将"键值为key的节点"的前驱节点旋转为根节点。
        b-2) "键值为key的节点"的前驱节点存在的话,则意味着,key比树中任何键值都小,那么此时,将最小节点旋转为根节点。
(c):伸展树中不存在"键值为key的节点",而且key > tree->key。
        c-1) "键值为key的节点"的后继节点存在的话,将"键值为key的节点"的后继节点旋转为根节点。
        c-2) "键值为key的节点"的后继节点不存在的话,则意味着,key比树中任何键值都大,那么此时,将最大节点旋转为根节点。 

下面列举个例子分别对a进行说明。

在下面的伸展树中查找10,共包括"右旋"  --> "右连接"  --> "组合"这3步。

 

第一步: 右旋
对应代码中的"rotate right"部分

 

第二步: 右连接
对应代码中的"link right"部分

 

第三步: 组合
对应代码中的"assemble"部分

提示:若是在上面的伸展树中查找"70",则正好与"示例1"对称,而对应的操做则分别是"rotate left", "link left"和"assemble"。
其它的状况,例如"查找15是b-1的状况,查找5是b-2的状况"等等,这些都比较简单,你们能够本身分析。

例子:

新建伸展树,而后向伸展树中依次插入10,50,40,30,20,60。插入完毕这些数据以后,伸展树的节点是60;此时,再旋转节点,使得30成为根节点。
依次插入10,50,40,30,20,60示意图以下:

将30旋转为根节点的示意图以下:

三、出处:http://www.cnblogs.com/vamei/archive/2013/03/24/2976545.html

树的搜索效率与树的深度有关。二叉搜索树的深度可能为n,这种状况下,每次搜索的复杂度为n的量级。AVL树经过动态平衡树的深度,单次搜索的复杂度为log(n) 。咱们下面看伸展树(splay tree),它对于m次连续搜索操做有很好的效率。 

伸展树会在一次搜索后,对树进行一些特殊的操做。这些操做的理念与AVL树有些相似,即经过旋转,来改变树节点的分布,并减少树的深度。但伸展树并无AVL的平衡要求,任意节点的左右子树能够相差任意深度。与二叉搜索树相似,伸展树的单次搜索也可能须要n次操做。但伸展树能够保证,m次的连续搜索操做的复杂度为mlog(n)的量级,而不是mn量级。 

具体来讲,在查询到目标节点后,伸展树会不断进行下面三种操做中的一个,直到目标节点成为根节点 (注意,祖父节点是指父节点的父节点)

1. zig: 当目标节点是根节点的左子节点或右子节点时,进行一次单旋转,将目标节点调整到根节点的位置。

zig

2. zig-zag: 当目标节点、父节点和祖父节点成"zig-zag"构型时,进行一次双旋转,将目标节点调整到祖父节点的位置。

zig-zag

3. zig-zig:当目标节点、父节点和祖父节点成"zig-zig"构型时,进行一次zig-zig操做,将目标节点调整到祖父节点的位置。

zig-zig

单旋转操做和双旋转操做见AVL树。下面是zig-zig操做的示意图:

zig-zig operation

在伸展树中,zig-zig操做(基本上)取代了AVL树中的单旋转。一般来讲,若是上面的树是失衡的,那么A、B子树极可能深度比较大。相对于单旋转(想一下单旋转的效果),zig-zig能够将A、B子树放在比较高的位置,从而减少树总的深度。

 

下面咱们用一个具体的例子示范。咱们将从树中搜索节点2:

Original

zig-zag (double rotation)

zig-zig

zig (single rotation at root)

上面的第一次查询须要n次操做。然而通过一次查询后,2节点成为了根节点,树的深度大减少。总体上看,树的大部分节点深度都减少。此后对各个节点的查询将更有效率。

伸展树的另外一个好处是将最近搜索的节点放在最容易搜索的根节点的位置。在许多应用环境中,好比网络应用中,某些固定内容会被大量重复访问(好比江南style的MV)。伸展树可让这种重复搜索以很高的效率完成。

四、出处:http://www.cnblogs.com/kernel_hcy/archive/2010/03/17/1688360.html

1、简介:
伸展树,或者叫自适应查找树,是一种用于保存有序集合的简单高效的数据结构。伸展树实质上是一个二叉查找树。容许查找,插入,删除,删除最小,删除最大,分割,合并等许多操做,这些操做的时间复杂度为O(logN)。因为伸展树能够适应需求序列,所以他们的性能在实际应用中更优秀。
伸展树支持全部的二叉树操做。伸展树不保证最坏状况下的时间复杂度为O(logN)。伸展树的时间复杂度边界是均摊的。尽管一个单独的操做可能很耗时,但对于一个任意的操做序列,时间复杂度能够保证为O(logN)。
2、自调整和均摊分析:
    平衡查找树的一些限制:
一、平衡查找树每一个节点都须要保存额外的信息。
二、难于实现,所以插入和删除操做复杂度高,且是潜在的错误点。
三、对于简单的输入,性能并无什么提升。
    平衡查找树能够考虑提升性能的地方:
一、平衡查找树在最差、平均和最坏状况下的时间复杂度在本质上是相同的。
二、对一个节点的访问,若是第二次访问的时间小于第一次访问,将是很是好的事情。
三、90-10法则。在实际状况中,90%的访问发生在10%的数据上。
四、处理好那90%的状况就很好了。
3、均摊时间边界:
在一颗二叉树中访问一个节点的时间复杂度是这个节点的深度。所以,咱们能够重构树的结构,使得被常常访问的节点朝树根的方向移动。尽管这会引入额外的操做,可是常常被访问的节点被移动到了靠近根的位置,所以,对于这部分节点,咱们能够很快的访问。根据上面的90-10法则,这样作能够提升性能。
为了达到上面的目的,咱们须要使用一种策略──旋转到根(rotate-to-root)。具体实现以下:
旋转分为左旋和右旋,这两个是对称的。图示:
 
为了叙述的方便,上图的右旋叫作X绕Y右旋,左旋叫作Y绕X左旋。
下图展现了将节点3旋转到根:
 
                            图1
首先节点3绕2左旋,而后3绕节点4右旋。
注意:所查找的数据必须符合上面的90-10法则,不然性能上不升反降!!
4、基本的自底向上伸展树:
    应用伸展(splaying)技术,能够获得对数均摊边界的时间复杂度。
    在旋转的时候,能够分为三种状况:
一、zig状况。
    X是查找路径上咱们须要旋转的一个非根节点。
    若是X的父节点是根,那么咱们用下图所示的方法旋转X到根:
     
                                图2
    这和一个普通的单旋转相同。
二、zig-zag状况。
在这种状况中,X有一个父节点P和祖父节点G(P的父节点)。X是右子节点,P是左子节点,或者反过来。这个就是双旋转。
先是X绕P左旋转,再接着X绕G右旋转。
如图所示:
 
                            图三
三、zig-zig状况。
    这和前一个旋转不一样。在这种状况中,X和P都是左子节点或右子节点。
    先是P绕G右旋转,接着X绕P右旋转。
    如图所示:
     
                                    图四
    下面是splay的伪代码:   

    P(X) : 得到X的父节点,G(X) : 得到X的祖父节点(=P(P(X)))。
    Function Buttom-up-splay:
        Do
            If X 是 P(X) 的左子结点 Then
                If G(X) 为空 Then
                    X 绕 P(X)右旋
                Else If P(X)是G(X)的左子结点
                    P(X) 绕G(X)右旋 X 绕P(X)右旋
                Else
                    X绕P(X)右旋 X绕P(X)左旋 (P(X)和上面一句的不一样,是原来的G(X))
                Endif
            Else If X 是 P(X) 的右子结点 Then
                If G(X) 为空 Then
                    X 绕 P(X)左旋
                Else If P(X)是G(X)的右子结点 P(X) 绕G(X)左旋 X 绕P(X)左旋
                Else
                    X绕P(X)左旋 X绕P(X)右旋 (P(X)和上面一句的不一样,是原来的G(X))
                Endif Endif While (P(X) != NULL)
    EndFunction

    仔细分析zig-zag,能够发现,其实zig-zag就是两次zig。所以上面的代码能够简化:
    

 Function Buttom-up-splay:
        Do
            If X 是 P(X) 的左子结点 Then
                If P(X)是G(X)的左子结点
                    P(X) 绕G(X)右旋
                Endif
                X 绕P(X)右旋
            Else If X 是 P(X) 的右子结点 Then
                If P(X)是G(X)的右子结点
                    P(X) 绕G(X)左旋
                Endif 
                X 绕P(X)左旋
            Endif While (P(X) != NULL)
    EndFunction

    下面是一个例子,旋转节点c到根上。 
 
                                    图五
5、基本伸展树操做:
一、插入
    当一个节点插入时,伸展操做将执行。所以,新插入的节点在根上。
二、查找
    若是查找成功(找到),那么因为伸展操做,被查找的节点成为树的新根。
若是查找失败(没有),那么在查找遇到NULL以前的那个节点成为新的根。也就是,若是查找的节点在树中,那么,此时根上的节点就是距离这个节点最近的节点。
三、查找最大最小
        查找以后执行伸展。
四、删除最大最小
a)删除最小:
    首先执行查找最小的操做。
这时,要删除的节点就在根上。根据二叉查找树的特色,根没有左子节点。
使用根的右子结点做为新的根,删除旧的包含最小值的根。
b)删除最大:
首先执行查找最大的操做。
删除根,并把被删除的根的左子结点做为新的根。
五、删除
        将要删除的节点移至根。
        删除根,剩下两个子树L(左子树)和R(右子树)。
        使用DeleteMax查找L的最大节点,此时,L的根没有右子树。
        使R成为L的根的右子树。
        以下图示:
         
                                图六
6、自顶向下的伸展树:
    在自底向上的伸展树中,咱们须要求一个节点的父节点和祖父节点,所以这种伸展树难以实现。所以,咱们能够构建自顶向下的伸展树。
    当咱们沿着树向下搜索某个节点X的时候,咱们将搜索路径上的节点及其子树移走。咱们构建两棵临时的树──左树和右树。没有被移走的节点构成的树称做中树。在伸展操做的过程当中:
   一、当前节点X是中树的根。
   二、左树L保存小于X的节点。
   三、右树R保存大于X的节点。
开始时候,X是树T的根,左右树L和R都是空的。和前面的自下而上相同,自上而下也分三种状况:
一、zig:
 
                                图七
    如上图,在搜索到X的时候,所查找的节点比X小,将Y旋转到中树的树根。旋转以后,X及其右子树被移动到右树上。很显然,右树上的节点都大于所要查找的节点。注意X被放置在右树的最小的位置,也就是X及其子树比原先的右树中全部的节点都要小。这是因为越是在路径前面被移动到右树的节点,其值越大。读者能够分析一下树的结构,缘由很简单。
二、zig-zig:
 
                                图八
    在这种状况下,所查找的节点在Z的子树中,也就是,所查找的节点比X和Y都小。因此要将X,Y及其右子树都移动到右树中。首先是Y绕X右旋,而后Z绕Y右旋,最后将Z的右子树(此时Z的右子节点为Y)移动到右树中。注意右树中挂载点的位置。
三、zig-zag:
 
                            图九
    在这种状况中,首先将Y右旋到根。这和Zig的状况是同样的。而后变成上图右边所示的形状。接着,对Z进行左旋,将Y及其左子树移动到左树上。这样,这种状况就被分红了两个Zig状况。这样,在编程的时候就会简化,可是操做的数目增长(至关于两次Zig状况)。
    最后,在查找到节点后,将三棵树合并。如图:
 
                                图十
    将中树的左右子树分别链接到左树的右子树和右树的左子树上。将左右树做为X的左右子树。从新最成了一所查找的节点为根的树。
    下面给出伪代码:    

 右链接:将当前根及其右子树链接到右树上。左子结点做为新根。 左链接:将当前根及其左子树链接到左树上。右子结点做为新根。 T : 当前的根节点。
    Function Top-Down-Splay Do 
            If X 小于 T Then 
               If X 等于 T 的左子结点 Then 右链接 
               ElseIf X 小于 T 的左子结点 Then 
                 T的左子节点绕T右旋 
                 右链接 
               Else X大于 T 的左子结点 Then 右链接 左链接 
               EndIf ElseIf X大于 T Then 
               IF X 等于 T 的右子结点 Then 左链接 
               ElseIf X 大于 T 的右子结点 Then T的右子节点绕T左旋 左链接 
               Else X小于 T 的右子结点 Then 左链接 右链接 
               EndIf EndIf While  (找到 X或遇到空节点) 组合左中右树 
 EndFunction 

    一样,上面的三种状况也能够简化:   

 Function Top-Down-Splay Do 
            If X 小于 T Then 
                If X 小于 T 的左孩子 Then T的左子节点绕T右旋 
                EndIf    
                右链接 Else If X大于 T Then 
                If X 大于 T 的右孩子 Then T的右子节点绕T左旋
                EndIf 
                左链接 
            EndIf While  !(找到 X或遇到空节点) 组合左中右树 
    EndFuntion

    下面是一个查找节点19的例子:
    在例子中,树中并无节点19,最后,距离节点最近的节点18被旋转到了根做为新的根。节点20也是距离节点19最近的节点,可是节点20没有成为新根,这和节点20在原来树中的位置有关系。
 
    这个例子是查找节点c:
 

相关文章
相关标签/搜索