伸展树c++ 实现

原创文章,转载请声明 :chance_yin  http://blog.csdn.net/chance_yin/article/details/35553747


一、为什么要有伸展树


    根据80-20黄金法则即80%的访问发生在20%的数据上,所以如果我们能够把经常访问的节点推到靠近根节点的位置,那么就可以极大的提高访问速度。


    根据这个思路,我们提出了 “ 旋转到根 ” 这一思路,具体的:每次查找、插入、删除一个节点,我们都使用旋转的方法把这个节点旋转到根节点的位置,并且因为旋转操作能够很好的把其他访问路径上的节点向上移动,所以最后这经常访问的20%的数据基本上都处于靠近根节点的位置。


二、普通AVL树的旋转和伸展树的旋转有什么不同?

    

    AVL树的旋转操作目的是缩小左右子树的高度差,它是全局调控即目的是缩小整棵树的高度,不会针对某一个节点做优化(例如将经常访问的节点移动到根的位置或靠近根的位置)

    相反,伸展树的旋转操作目的就是把经常访问的节点移动到根的位置,它不会考虑整棵树是否平衡,旋转完成后,伸展树可能成为一个糟糕的单链表,但伸展树保证在此单链表上查找经常访问的20%节点是最快的。


   鉴于基本AVL树和伸展树的旋转目的不同,他们的旋转操作也有一定的区别,具体如下:

  (下面的旋转操作看不懂可以继续往下看,只要知道他们的旋转操作有区别就好)

   1、处理ZIG-ZIG 形式的旋转操作

   (1)普通AVL数的旋转操作


   



   (2)伸展树(这步操作是最明显的区别)



    2、处理ZIG-ZAG形式的旋转操作

    (1)普通AVL树



     (2)伸展树(自顶向下)



三、伸展操作 splay

    伸展操作又分为“自底向上” 和 “自顶向下” 两种。其中“自底向上” 比较容易想得到,因为想要把深层的访问节点移动到树根,最直接的思路就是从要访问的节点开始向上慢慢的旋转。


    先要给出节点之间的几种连接形式:

    注1:

    (1) , 镜像 形式(2): 


    注2:

(1) , 镜像形式(2) 


    注3:

(1)   ,镜像形式(2)


1、自底向上伪码


for(;;)
{
    if( x 是 P 的左子节点)
   {
        if(  G == NULL )   // zig 形式,见注1(1)
        {
            x 绕 P 右旋;  // 见注4
        }
        else if( P 是 G 的左子节点 )  // zig-zig 形式见注2(1)
        {
            P 绕 G 右旋;  
            x 绕 P 右旋; //见注5
        }
        else if( P 是G的右子节点 ) //zig-zag ,形式见注3(1)
        {
           x 绕 P 右旋;
           x 绕 G 左旋; //见注6
        }
   }
   else if( x 是 P 右子节点)
   {
         “x 是 P 左子节点的镜像操作,和上面的if 操作大同小异,为了减少大家看代码的复杂度,我把它扔到下面的注释中了”
   }
}


  x 是 P 左子节点的镜像操作

else if (x 是 P 右子节点)
{
   if( G  == NULL) // 见注1(2)
   {
      x 绕 P 左旋;//见注4(2)
   }
   else if( P 是 G 右子节点 )//见注2(2)
   {
      P 绕 G 左旋;
      x 绕 P 左旋; //见注5(2)
   }
   else if( P 是 G 左子节点) //见注3(2)
   {
     x 绕 P 左旋;
     x 绕 G 右旋; //见注6(2)
   }
}



  自底向上操作:

  注4:

  (1)

   (2)镜像:

    

  注5:

  (1)

(2)

  


  注6:

  (1)

  

  (2)

  


  睡觉睡觉睡觉 终于把自底向上搞定了.....

   自底向上写了半天,但我要说实际使用中,一般都不用这种算法!!

   简单分析下不难发现,(1)首先要遍历访问路径上的所有节点,找到要访问的节点x (2)在自底向上调整结构的过程中,必须要记住x 的父节点P和祖父节点G,如果旋转G节点那么还需要记录曾祖父节点GG,如果你不想在struct BinaryNode 节点结构中添加额外的成员来记住父节点,那么就只能使用足够深的栈来记录父节点信息。


   所以重点来了,我们使用自顶向下来替代自底向上,在自顶向下算法中,只需要一次遍历访问路径上的节点,并且在访问过程中我们就要完成“旋转到顶的操作”。


  2、自顶向下伪码(1)


  在自顶向下伪码中,需要再引入两个辅助指针,“左树指针LeftTreeMax”  和 “右树指针RightTreeMin”

  从名字应该可以看出这两个指针的作用,LeftTreeMax 始终指向左树中的最右节点,即最左树的最大节点。RightTreeMax 始终指向右树中的最左节点,即最小节点。

  分析起来原因有点困难,但如果找出一个特殊情况就比较好看:

  先看为什么随着树的深度的加深,能够挂在左树上的节点的值会逐渐变大:

 

  


   如上图,能挂到左子树上的节点,一定处于访问路径的右分支上,显然随着树的深度的加深,又分支上的节点值越来越大,所以leftTreeMax 永远指向左树上的最大节点。

  同理不难分析,为什么RightTreeMin 永远指向右树上的最小值,这里只贴出一张分析图,其他文字分析就不再赘述:

  


  好知道了这些,自顶向下的代码就简单了,具体思路就一句话 “遍历访问路径,在遍历过程中执行适当的旋转和将节点挂载到左右子树上的工作”

for(;;)
{
   if(x < G) 
   {
       if(x == G 的左子节点 P )  //zig 形式
       {
            <span style="color:#ff0000;">将G及其右子树挂到右树上;</span>
       }
       else if( X < G 的左子节点P)  // zig-zig
       {
            P 绕G旋转;
            <span style="color:#ff0000;">将P和G挂到右树上</span>
       }
       else (x > G 的左子节点P) //zig-zag
       {
            <span style="color:#ff0000;">将G及其右子树挂到右树上;</span>
            将P及其左子数挂到左树上;
       }
   }
   else if(X > G)
   {
       if(x == G的右子节点P)
       {
            将G及其左子树挂到左树上;
       }
       else if( X > G 的右子节点P)
       {
            将P绕G左旋;
            将P和G挂到左树上;
       }
       else if(X < G右子节点P)
       {
            将G及其左子树挂到左树上;
            将P及其右子树挂到右树上;
       }
    }
    else
       break;
}

    观察自顶向下的伪代码发现,红色字部分是实际上是相同的语句,重复即罪恶,所以我们可以把他们合并或者说共用同一个语句。特别的zig-zag 形式中的 将"P及其左子树挂到左树上" 这句伪代码是可以丢到镜像 if(X > G) 中去的...... 自己考虑下,如此一来,我们就只剩下两种变换形式“zig(zag)” 和 “zig-zig(zag-zag)” ,而“zig-zag ”可以简化成一个zig和一个zag

    

for(;;)
{
    if( x  < G )
    {
        if(x < G 的左子节点P)  //zig-zig
        {
            P 绕 G 右旋;
        }

        将当前树的根和根的右子树挂到右树上;   //zig 
    }
    else if( x > G )
    {
        if(x > G 的右子节点P) // zag-zag
        {
             P 绕 G 左旋;
        }
        
        将当前树的根和根的左子树挂到左树上;  //zag
    }
    else
       break;
}


四、最后再将下具体代码实现(代码来自Mark Allen Weiss 的数据结构与算法分析c++描述)


template <typename Comparable>
class SplayTree
{
    public:
    SplayTree()
    {   
        nullnode = new BinaryNode(0,NULL,NULL);
        nullnode->left = nullnode->right = nullnode;

        root = nullnode; //不赋值为0,则root值是随机数
    }   

    ~SplayTree(){};

    void insert(const Comparable& x)
    {   
        insert(x,root);         
    }   

    void printTree()
    {   
        printTree(root);        
    }   

    void splay(const Comparable& x)
    {   
        splay(x,root);
    }   

    private:

    typedef struct BinaryNode
    {
        Comparable element;
        struct BinaryNode * left;
        struct BinaryNode * right;
    
        BinaryNode(const Comparable& theElement, BinaryNode * lt,BinaryNode * rt):
            element(theElement),left(lt),right(rt){};
    }BinaryNode;


    BinaryNode * root;
    BinaryNode * nullnode;


    /* 
     *注释说明:
     * T 表示当前的根节点
     * p 表示当前根节点的孩子节点
     * x 表示待查找元素
     * */
    void splay(const Comparable& x, BinaryNode *& root)
    {
        static BinaryNode header(x,nullnode,nullnode);
        BinaryNode * leftTreeMax,*rightTreeMin;
        header.right = header.left = nullnode;   //因为static 语句只会调用一次对象的构造函数,所以必须要在这里清空左右子树


        leftTreeMax = rightTreeMin = &header;   
        
        nullnode->element = x;


        for(;;)
        {
            if(x < root->element)
            {
                BinaryNode * leftChild = root->left;
                
                if(x < leftChild->element)
                {
                    rotateWithLeftChild(root);  // 该函数执行完成后,root 指向p 节点
                }


                if(root->left == nullnode)      // nullnode 和 这个判断节点为空的语句是否应该在上面的 <span style="font-family: Arial, Helvetica, sans-serif;">if(x < leftChild->element) 判断语句之前再放一个的解释见注释7</span>

                    break;


                rightTreeMin->left = root;
                rightTreeMin = root;

                root = root->left;
                    
            }
            else if(x > root->element)
            {
                BinaryNode * rightChild = root->right;
                if(x > rightChild->element)
                {
                    rotateWithRightChild(root);     
                }

                if(root->right == nullnode)
                    break;


                leftTreeMax->right = root;
                leftTreeMax = root;
                
                root = root->right; 
            }
            else
                break;
        }
        
        leftTreeMax->right = root->left;
        rightTreeMin->left = root->right;


        root->left = header.right;
        root->right = header.left;
    }

    /*
     *             root            leftChild
     *              /                /  \
     *        leftChild  ==>      other root
     *           /   \                  /
     *         left right            right
     **/
    void rotateWithLeftChild(BinaryNode *& root)
    {
        BinaryNode * leftChild = root->left;
        root->left = leftChild->right;
        leftChild->right = root;


        root = leftChild;   
    }


    /*
     *             root            rightChild
     *               \                /  \
     *            rightChild        root  right
     *              /   \             \ 
     *            left right         left 
     **/
    void rotateWithRightChild(BinaryNode *& root)
    {
        BinaryNode * rightChild = root->right;
        root->right = rightChild->left;
        rightChild->left = root;
        
        root = rightChild;
    }


    void printTree(BinaryNode * node)
    {
        if(node == nullnode)
        {
            return;
        }


        cout << node->element << endl;
        printTree(node->left);
        printTree(node->right);
    } <span style="white-space:pre">	</span>

    void remove(const Comparable& x, BinaryNode *& root)
    {
        BinaryNode * newTree;


        //删除一个节点也要重新调整树的结构
        //因为伸展树可能极度不平衡,如果使用递归的方法来删除一个
        //节点,则在大数据中可能会导致栈溢出
        splay(x,root);


        if(x == root->element)
        {
            /* 
             * 如果左子树为空则右子树的根就是新树的根
             * 如果左子树不为空,则使用左子树最大的节点(最右节点)作为新树的根
             * */
            if(root->left == nullnode)
            {
                newTree = root->right;
            }
            else
            {
                newTree = root->left;
                /* 再次重构整个伸展树 */
                splay(x,newTree);  // 找到左子树最大的节点
                newTree->right = root->right;  //让左子树最大的节点作为新树的根
            }
            delete root;
            root = newTree;
        }
    }

    void insert(const Comparable& x,BinaryNode *& root)
    {
        static BinaryNode * newNode = NULL;
        if(newNode == NULL)
        {
            newNode = new BinaryNode(x,nullnode,nullnode);  
        }
        newNode->element = x;


        if(root == nullnode)
        {
            root = newNode;
            root->left = root->right = nullnode;
        }
        else
        {
            splay(x,root);
            if(x < root->element)
            {
                newNode->left = root->left;  //splay 完成后,根据伸展规则root的左子树都小于关键字
                newNode->right = root;        // root 的右子树都大于关键字,所以要把左子树的元素挂在到
                                             //新节点的左子树上,而root和root的右子树都大于关键字  ,见注释8
                root->left = nullnode;       //保证所有的空节点都指向nullnode
                root = newNode;              //新插入的节点作为root节点
            }
            else if(x > root->element)
            {
                newNode->right = root->right;
                newNode->left = root;


                root->right = nullnode;
                root = newNode;
            }
            else
                return;  // 查到重复元素,则此次new的BinaryNode没有使用,所以下一次插入新的节点时,可以不用重新new
        }          
        newNode = NULL;
    }
};


注释7:

1、nullnode 的作用,为什么不用NULL??

   (1)分析如果不用nullnode 而是用我们熟悉的NULL来标表示节点为空

            那么我们就必须 在 “ if (  x < leftChild->element )   ”判断语句前后都要放一个if(leftChild == NULL)判断,原因是放在“ if (  x < leftChild->element )   ” 之前的if(leftChild == NULL)是为了避免访问空指针的->element元素,即防止出现 NULL->element 的语句的出现。

  而之后的if(leftChild == NULL) 是因为 rotateWihtXXXXChild() 语句会改变树的根节点。因为在伸展树中查找一个节点,如果这个节点不存在我们就要返回“访问路径上的最后一个元素”如图:


   

   (2)分析如果我们使用了nullnode,并且规定整棵树中所有空接点全指向nullnode,而不是NULL

     那么我们可以在splay 开始时,设置 nullnode == x (要查找的关键字值),这样就可以保证不会出先“NULL->elemetn”空引用的问题,也就省掉了(1)中提到的第一次 if(leftChild == NULL)的判断


    最后还要再提一下for(;;)循环出口的问题,如果找到了节点 那么应该从 if(X < root->element ) ....else (X > root->element)....else break; 结构中的break 退出。此时当前结点即要访问的节点;  如果没有找到想访问的节点,则从if(root->leftChild == nullnode) 返回。此时root表示访问路径上最后一个节点。


2、最后一个比较讲究的函数就是 insert 函数了

    (1) insert 函数需要保证我们在splay函数中假定的那个前提“整棵树中所有空接点均指向nullnode”
     (2)另外insert 开始的static 定义和对于if (newNode == NULL)的判断是考虑了这种情况:如果待插入的节点X 在树中已经存在,则新new 出来的节点可以给下次插入使用。
     (3)贴出一张图解释 newNode->left = root->left; :
  


最后一个图中,蓝色连接表示:newNode->left = root->left; 黄色连接表示:newNode->right = root; 红色连接表示:root->left = nullnode;


五、上面的所有图片就是我能想到的伸展树操作及其实现的所有“难点”了。有错误和不明白的地方欢迎留言交流。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值