图解双层伸展树

引言

伸展树是基于AVL树的,但它并没有AVL的平衡要求,任意节点的左右子树可以相差任意深度。与二叉搜索树类似,伸展树的单次搜索也可能需要n次操作。但伸展树可以保证,m次的连续搜索操作的复杂度为mlog(n)的量级,而不是mn量级。

伸展树的出发点是这样的:考虑到局部性原理(刚被访问过的数据,极有可能很快地再次被访问),因此将刚被访问过的节点移(旋转)到根节点。

如果没有了解过AVL树,建议先看下 图解AVL树

伸展策略

那么该如何旋转呢?

逐层伸展

联系刚刚学过的AVL树,我们可以从待访问的节点开始,逐层往上旋转。

我们使用的手段无非是,
在这里插入图片描述
当待访问节点v是左孩子,进行一次右旋操作。

在这里插入图片描述
若是右孩子,则进行一次左旋操作。

这种策略简单明了,但是存在一些不足之处,是什么呢?我们来看一种特例:

在这里插入图片描述

假设有这样一颗树,按照访问最深节点的方式进行访问。初始访问节点1:

在这里插入图片描述

经过若干次伸展(旋转)之后,1被送到了根节点,接下来访问最深节点2:

在这里插入图片描述

最后访问最深节点7,该伸展树会重新变成了类似链表的结构:
在这里插入图片描述

整个过程如下:

在这里插入图片描述
这样会有什么问题呢,显然树的高度会很高,旋转次数呈周期性的算术级数演变:每一周期累计 Ω ( n 2 ) \Omega(n^2) Ω(n2),分摊 Ω ( n ) \Omega(n) Ω(n)

好在Robert Endre Tarjan 提出了一种新的伸展策略:双层伸展

双层伸展

该策略的精髓是:向上伸展两层,而非一层。

在这里插入图片描述

我们考察节点v,父亲p以及祖父g。根据它们的相对位置,经过两次旋转,使得v为该子树的根。它们的相对位置,如上图所示。

首先考虑zag-zig的情况(对称的zig-zag是同理的):

在这里插入图片描述
节点v是左孩子,父亲p是右孩子。

首先围绕节点p做一次右旋,使v上升一层,接着围绕g做一次左旋,使v再上升一层。
如果你观察仔细的话,会发现其实与AVL树双旋完全等效,并且与逐层伸展也没有区别,唯一不同是,整个操作使v上升了两层。

没错,真正精髓的地方在于zig-zigzag-zag情形:

在这里插入图片描述

对于zig-zig的情况,要越级旋转,不是围绕节点p旋转,而是它的祖父节点g进行旋转。然后才围绕它的父亲节点p旋转。过程如上图所示。

我们看下与常规方法的区别:

在这里插入图片描述

上图中上半部分是常规的两次旋转,下半部分是双层旋转。可以看到旋转后的结构的区别。

我们同样考虑上面那种情况:
在这里插入图片描述

先访问最深节点1,此时不是围绕2进行旋转,而是1的祖父3。

在这里插入图片描述
然后是它的父亲节点2:

在这里插入图片描述

接下来是针对以5为根的子树,这里就不展开描述了。
最终形成的结构如下:

在这里插入图片描述

可以看到,访问了节点1之后,整颗树的高度基本缩减了一半

继续尝试访问最深节点3:

在这里插入图片描述

可以看到,树的高度又缩减了一半。

因此我们主要研究双层伸展树的实现,接下来到了给代码的时间。

实现

我们采用带有parent引用的实现,当然还有不带该引用的实现,有兴趣的同学可以去搜一下。

树节点结构

private static class Node<E> {
       E data;
       Node<E> left;
       Node<E> right;
       //指向父节点 维护起来要复杂一点, 但是很多方法不需要返回Node了
       Node<E> parent;

       Node(E data) {
           this.data = data;
       }

       /**
        * @return 当前节点是否为父节点的左孩子
        */
       boolean isLeft() {
           return parent != null && parent.left == this;
       }

       /**
        * @return 当前节点是否为父节点的右孩子
        */
       boolean isRight() {
           return parent != null && parent.right == this;
       }

       /**
        * 返回祖父节点,如果有的话
        *
        * @return
        */
       Node<E> grandparent() {
           if (parent != null) {
               return parent.parent;
           }
           return null;
       }

       /**
        * 为当前节点附加一个右孩子
        *
        * @param child
        */
       void attachRightChild(Node<E> child) {
           this.right = child;
           if (child != null) {
               child.parent = this;
           }
       }

       /**
        * 为当前节点附加一个左孩子
        *
        * @param child
        */
       void attachLeftChild(Node<E> child) {
           this.left = child;
           if (child != null) {
               child.parent = this;
           }
       }


   }

增加了一些便利方法。

因为代码中也有图解(啥?不敢相信是吧,往下看),其他方法就不一一分析了。有问题欢迎留言。

完整代码

package com.algorithms.tree;

import java.util.NoSuchElementException;
import java.util.stream.IntStream;

/**
 * 双层伸展树
 *
 * @author yjw
 * @date 2019/6/26/026
 */
public class SplayTree<E extends Comparable<? super E>> implements BinaryTree<E> {
    private Node<E> root;
    BinaryTreePrinter<Node<E>> printer;

    public SplayTree() {
        printer = new BinaryTreePrinter<>(node -> node.data.toString(), node -> node.left, eNode -> eNode.right);
        printer.setHspace(4);
        printer.setSquareBranches(true);
    }

    @Override
    public void insert(E x) {
        Node<E> node = new Node<>(x);
        Node<E> cur = root;
        Node<E> p = null;//保存cur的parent引用
        while (cur != null) {
            p = cur;
            if (node.data.compareTo(cur.data) < 0) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }

        node.parent = p;
        if (p == null) {
            root = node;
        } else if (node.data.compareTo(p.data) < 0) {
            //node < p
            p.left = node;
        } else {
            //node >= p
            p.right = node;
        }
        splay(node);
    }

    @Override
    public void remove(E x) {
        if (!contains(x)) {
            return;
        }
        Node<E> newTree;
        //代码执行到这里,说明找到了x,它已经被伸展到了树根root
        if (root.left == null) {
            //没有左孩子,指向右孩子即可
            newTree = root.right;
        } else {
            //连接root的左子树与右子树 合并成新树
            newTree = join(root.left, root.right);
        }
        //newTree即将成为新的root,当然不能有parent了,同时消除对旧root的引用
        if (newTree != null) {
            newTree.parent = null;
        }
        root = newTree;
    }

    @Override
    public boolean contains(E x) {
        if (isEmpty()) {
            return false;
        }
        Node<E> node = contains(x, root);
        if (node != null) {
            splay(node);
            return true;
        }
        return false;
    }

    private Node<E> contains(E x, Node<E> node) {
        if (node == null) {
            return null;
        }
        int cmp = x.compareTo(node.data);
        if (cmp < 0) {
            return contains(x, node.left);
        } else if (cmp > 0) {
            return contains(x, node.right);
        }
        return node;
    }

    @Override
    public boolean isEmpty() {
        return root == null;
    }

    @Override
    public void makeEmpty() {
        root = null;
    }

    @Override
    public void printTree() {
        if (isEmpty()) {
            System.out.println("Empty Tree.");
        } else {
            printer.printTree(root);
        }
    }

    private Node<E> join(Node<E> left, Node<E> right) {
        if (left == null) {
            return right;
        }
        if (right == null) {
            return left;
        }
        //找到left的最大节点
        Node<E> x = findMax(left);
        //伸展置left子树根
        splay(x);
        //将right作为x的右节点
        x.attachRightChild(right);
        return x;
    }

    public E findMax() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return findMax(root).data;
    }

    private Node<E> findMax(Node<E> node) {
        while (node.right != null) {
            node = node.right;
        }
        return node;
    }

    /**
     * 伸展
     * <p>
     * 对于zig-zag与zag-zig应用逐层伸展方法
     * 对于zig-zig与zag-zag应用双层伸展方式
     *
     * @param v
     */
    private void splay(Node<E> v) {
        //只要还没到点顶部
        while (v.parent != null) {
            //如果没有祖父节点
            if (v.grandparent() == null) {
                //如果v是左孩子
                if (v.isLeft()) {
                    //       p                      v
                    //      / \                    / \
                    //     v   Z     ->           X   p
                    //    / \                        / \
                    //   X   Y                      Y   Z
                    rotateRight(v.parent);
                } else {
                    //          p                v
                    //        /  \              / \
                    //       X    v    ->      p   Z
                    //          /  \          / \
                    //         Y    Z        X   Y
                    rotateLeft(v.parent);
                }
            } else if (v.isLeft() && v.parent.isLeft()) { //zig-zig
                //           g                              v
                //         /  \                            / \
                //        p    Z            p             W   p
                //      / \         ->    /   \   ->         / \
                //     v  Y              v     g            X   g
                //    / \               / \   / \              / \
                //   W   X             W   X Y  Z             Y   Z

                //双层伸展的精髓:先旋转g-p,再旋转p-v
                rotateRight(v.grandparent());
                rotateRight(v.parent);
            } else if (v.isRight() && v.parent.isRight()) { //zag-zag
                //        g                                    v
                //      /  \                                 /  \
                //     Z    p                 p             p    X
                //        /  \      ->      /   \    ->    / \
                //       Y    v            g     v        g   W
                //          /  \          / \   / \      / \
                //         W    X        Z   Y W   X    Z   Y

                rotateLeft(v.grandparent());
                rotateLeft(v.parent);
            } else if (v.isRight() && v.parent.isLeft()) {
                //       g                 g               v
                //      / \               / \            /   \
                //     p   Z      ->     v   Z    ->    p      g
                //   / \               / \             / \    / \
                //  Y   v             p   X           Y   W  X   Z
                //     / \          / \
                //    W   X        Y   W


                //zig-zag 采用的是逐层伸展的方式
                rotateLeft(v.parent);//先对p-v执行左旋
                rotateRight(v.parent);//再对g-v执行右旋
            } else { //v.isLeft() && v.parent.isRight()

                //       g                 g                    v
                //      / \               / \                 /   \
                //     W   p         ->  W   v       ->     g      p
                //        / \               / \            / \    / \
                //       v   Z             X   p          W   X  Y   Z
                //     / \                    / \
                //    X   Y                  Y   Z

                //对称的 zag-zig也采用逐层伸展
                rotateRight(v.parent);
                rotateLeft(v.parent);
            }
        }
    }

    /**
     * 以p的左孩子为中心,做一次右旋
     *
     * @param p
     */
    private void rotateRight(Node<E> p) {
        //       p                      v
        //      / \                    / \
        //     v   Z        ->        X   p
        //    / \                        / \
        //   X   Y                      Y   Z
        Node<E> v = p.left;
        //将Y作为p的左孩子
        p.attachLeftChild(v.right);
        //更新v的parent引用
        v.parent = p.parent;
        if (p.parent == null) {
            //v就是新根
            root = v;
        } else if (p.isRight()) {
            //p是其父节点的右孩子,更新parent的右孩子引用
            p.parent.attachRightChild(v);
        } else {
            p.parent.attachLeftChild(v);
        }
        v.attachRightChild(p);
    }

    private void rotateLeft(Node<E> p) {
        //          p                v
        //        /  \              / \
        //       X    v      ->    p   Z
        //          /  \          / \
        //         Y    Z        X   Y
        Node<E> v = p.right;
        //Y附加到p的右孩子上
        p.attachRightChild(v.left);
        v.parent = p.parent;
        if (p.parent == null) {
            root = v;
        } else if (p.isLeft()) {
            p.parent.attachLeftChild(v);
        } else {
            p.parent.attachRightChild(v);
        }
        v.attachLeftChild(p);
    }

    private static class Node<E> {
        E data;
        Node<E> left;
        Node<E> right;
        //指向父节点 维护起来要复杂一点, 但是很多方法不需要返回Node了
        Node<E> parent;

        Node(E data) {
            this.data = data;
        }

        /**
         * @return 当前节点是否为父节点的左孩子
         */
        boolean isLeft() {
            return parent != null && parent.left == this;
        }

        /**
         * @return 当前节点是否为父节点的右孩子
         */
        boolean isRight() {
            return parent != null && parent.right == this;
        }

        /**
         * 返回祖父节点,如果有的话
         *
         * @return
         */
        Node<E> grandparent() {
            if (parent != null) {
                return parent.parent;
            }
            return null;
        }

        /**
         * 为当前节点附加一个右孩子
         *
         * @param child
         */
        void attachRightChild(Node<E> child) {
            this.right = child;
            if (child != null) {
                child.parent = this;
            }
        }

        /**
         * 为当前节点附加一个左孩子
         *
         * @param child
         */
        void attachLeftChild(Node<E> child) {
            this.left = child;
            if (child != null) {
                child.parent = this;
            }
        }


    }

    public static void main(String[] args) {
        SplayTree<Integer> tree = new SplayTree<>();
        int[] values = IntStream.rangeClosed(1, 15).toArray();
        for (int value : values) {
            tree.insert(value);
        }
        System.out.println("-------------------------");
        System.out.println(tree.contains(1));
        tree.printTree();
    }
}

main中打印的树为:

                 1     
                 └┐    
                  14   
               ┌──┴──┐ 
               12    15
            ┌──┴──┐    
            10    13   
         ┌──┴──┐       
         8     11      
      ┌──┴──┐          
      6     9          
   ┌──┴──┐             
   4     7             
┌──┴──┐                
2     5                
└┐                     
 3   

优缺点

它的性能如下:

在这里插入图片描述
其中遍历(Traversal)操作的摊还(Amortized)时间复杂度为 O ( n ) O(n) O(n),整体还是不错的。

优点

  • 无需记录节点高度或平衡因子(我们的实现多了个parent引用,但是也不复杂对吧)
  • 编程实现比AVL树简单
  • 局部性强时,缓存命中率极高( k &lt; &lt; n &lt; &lt; m ) k &lt;&lt; n &lt;&lt; m) k<<n<<m)
    • 任何连续的m次查找,都在 o ( m l o g k + n l o g n ) o(mlogk + nlogn) o(mlogk+nlogn)时间内完成

缺点

  • 它有可能会变成一条链
  • 不适用于对效率敏感的场景

在以非降顺序访问n个元素之后,比如我们执行这样的代码:

SplayTree<Integer> tree = new SplayTree<>();
int[] values = IntStream.rangeClosed(1, 15).toArray();
for (int value : values) {
    tree.insert(value);
}
for (int value : values) {
    tree.contains(value);
}
tree.printTree();

打印树结构如下:

              15
             ┌┘ 
             14 
            ┌┘  
            13  
           ┌┘   
           12   
          ┌┘    
          11    
         ┌┘     
         10     
        ┌┘      
        9       
       ┌┘       
       8        
      ┌┘        
      7         
     ┌┘         
     6          
    ┌┘          
    5           
   ┌┘           
   4            
  ┌┘            
  3             
 ┌┘             
 2              
┌┘              
1               

哇,又变成一条链表了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值