6天通吃树结构—— 第四天 伸展树

 

      我们知道AVL树为了保持严格的平衡,所以在数据插入上会呈现过多的旋转,影响了插入和删除的性能,此时AVL的一个变种

伸展树(Splay)就应运而生了,我们知道万事万物都遵循一个“八二原则“,也就是说80%的人只会用到20%的数据,比如说我们

的“QQ输入法”,平常打的字也就那么多,或许还没有20%呢。

 

一:伸展树

 1:思想

    伸展树的原理就是这样的一个”八二原则”,比如我要查询树中的“节点7”,如果我们是AVL的思路,每次都查询“节点7”,那么当这

棵树中的节点越来越多的情况下就会呈现下旋,所以复杂度只会递增,伸展树的想法就是在第一次查询时树里面会经过一阵痉挛把

“节点7”顶成“根节点”,操作类似AVL的双旋转,比如下图:

当我们再次查询同样的”数字7“时,直接在根节点处O(1)取出,当然这算是一个最理想的情况,有时痉挛过度,会出现糟糕的”链表“,

也就退化了到O(N),所以伸展树讲究的是”摊还时间“,意思就是说在”连续的一系列操作中的平均时间“,当然可以保证是log(N)。

 

2:伸展方式

    不知道大家可否记得,在AVL中的旋转要分4个情况,同样伸展树中的伸展需要考虑6种情况,当然不考虑镜像的话也就是3种情况,

从树的伸展方向上来说有“自下而上”和“自上而下"的两种方式,考虑到代码实现简洁,我还是说下后者。

 

<1> 自上而下的伸展

      这种伸展方式会把树切成三份,L树,M树,R树,考虑的情况有:单旋转,“一字型”旋转,“之字形”旋转。

①: 单旋转

从图中我们可以看到,要将“节点2”插入到根上,需要将接近于“节点2”的数插入到根上,也就是这里的“节点7”,首先树被分成了3份,

初始情况,L和R树是“空节点”,M是整棵树,现在需要我们一步一步拆分,当我们将“节点2”试插入到“节点7”的左孩子时,发现“节点7”

就是父节点,满足“单旋转”情况,然后我们将整棵树放到“R树”中的left节点上,M此时是一个逻辑上的空节点,然后我们将R树追加到

M树中。L树追加到M的左子树中,最后我们将“节点2”插入到根节点上。说这么多有点拗口,伸展树比较难懂,需要大家仔细品味一下。

 

②: 一字型

一字型旋转方式与我们AVL中的“单旋转”类似,首先同样我们切成了三份,当我们"预插入20时”,发现20的“父节点”是根的右孩子,

而我们要插入的数字又在父节点的右边,此时满足”一字型“旋转,我们将7,10两个节点按照”右右情况”旋转,旋转后“节点10"的

左孩子放入到L树的right节点,"节点10”作为中间树M,最后将20插入根节点。

③: 之字形

 

之字形有点类似AVL中的“双旋转”,不过人家采取的策略是不一样的,当我们试插入“节点9”,同样发现“父节点”是根的右儿子,并且

“节点9”要插入到父节点的内侧,根据规则,需要将“父节点10”作为M树中的根节点,“节点7”作为L树中的right节点,然后M拼接L和R,

最后将节点9插入到根上。

 

3:基本操作

①:节点定义

我们还是采用普通二叉树中的节点定义,也就没有了AVL那么烦人的高度信息。

public class BinaryNode<T>
    {
        // Constructors
        public BinaryNode(T theElement) : this(theElement, null, null) { }

        public BinaryNode(T theElement, BinaryNode<T> lt, BinaryNode<T> rt)
        {
            element = theElement;
            left = lt;
            right = rt;
        }

        public T element;

        public BinaryNode<T> left;

        public BinaryNode<T> right;
    }

②:伸展

     这里为了编写代码方便,我采用的是逻辑nullNode节点,具体伸展逻辑大家可以看上面的图。

#region 伸展
        /// <summary>
        /// 伸展
        /// </summary>
        /// <param name="Key"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public BinaryNode<T> Splay(T Key, BinaryNode<T> tree)
        {
            BinaryNode<T> leftTreeMax, rightTreeMin;

            header.left = header.right = nullNode;

            leftTreeMax = rightTreeMin = header;

            nullNode.element = Key;

            while (true)
            {
                int compareResult = Key.CompareTo(tree.element);

                if (compareResult < 0)
                {
                    //如果成立,说明是”一字型“旋转
                    if (Key.CompareTo(tree.left.element) < 0)
                        tree = rotateWithLeftChild(tree);

                    if (tree.left == nullNode)
                        break;

                    //动态的将中间树的”当前节点“追加到 R 树中,同时备份在header中
                    rightTreeMin.left = tree;

                    rightTreeMin = tree;

                    tree = tree.left;
                }
                else if (compareResult > 0)
                {
                    //如果成立,说明是”一字型“旋转
                    if (Key.CompareTo(tree.right.element) > 0)
                        tree = rotateWithRightChild(tree);

                    if (tree.right == nullNode)
                        break;

                    //动态的将中间树的”当前节点“追加到 L 树中,同时备份在header中
                    leftTreeMax.right = tree;

                    leftTreeMax = tree;

                    tree = tree.right;
                }
                else
                {
                    break;
                }
            }

            /* 剥到最后一层,来最后一次切分 */
            //把中间树的左孩子给“左树”
            leftTreeMax.right = tree.left;

            //把中间树的右孩子给“右树”
            rightTreeMin.left = tree.right;

            /* 合并操作 */
            //将头节点的左树作为中间树的左孩子
            tree.left = header.right;

            //将头结点的右树作为中间树的右孩子
            tree.right = header.left;

            return tree;
        }
        #endregion

③:插入

插入操作关键在于我们要找到接近于”要插入点“的节点,然后顶成“根节点”,也就是上面三分图中的最后一分。

#region 插入
        /// <summary>
        /// 插入
        /// </summary>
        /// <param name="Key"></param>
        public void Insert(T Key)
        {
            if (newNode == null)
                newNode = new BinaryNode<T>(default(T));

            newNode.element = Key;

            if (root == nullNode)
            {
                newNode.left = newNode.right = nullNode;

                root = newNode;
            }
            else
            {
                root = Splay(Key, root);

                int compareResult = Key.CompareTo(root.element);

                if (compareResult < 0)
                {
                    newNode.left = root.left;

                    newNode.right = root;

                    root.left = nullNode;

                    root = newNode;
                }
                else
                    if (compareResult > 0)
                    {
                        newNode.right = root.right;

                        newNode.left = root;

                        root.right = nullNode;

                        root = newNode;
                    }
                    else
                        return;
            }

            newNode = null;
        }
        #endregion

④:删除

  删除操作也要将节点伸展到根上,然后进行删除,逻辑很简单。

#region 删除
        /// <summary>
        /// 删除
        /// </summary>
        /// <param name="Key"></param>
        public void Remove(T Key)
        {
            BinaryNode<T> newTree;

            //将删除结点顶到根节点
            root = Splay(Key, root);

            //不等于说明没有找到
            if (root.element.CompareTo(Key) != 0)
                return;

            //如果左边为空,则直接用root的右孩子接上去
            if (root.left == nullNode)
            {
                newTree = root.right;
            }
            else
            {
                newTree = root.left;

                newTree = Splay(Key, newTree);

                newTree.right = root.right;
            }
            root = newTree;
        }
        #endregion

总的运行代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DataStructSplay
{
    public class BinaryNode<T>
    {
        public BinaryNode(T theElement) : this(theElement, null, null) { }

        public BinaryNode(T theElement, BinaryNode<T> lt, BinaryNode<T> rt)
        {
            element = theElement;
            left = lt;
            right = rt;
        }

        public T element;

        public BinaryNode<T> left;

        public BinaryNode<T> right;
    }

    public class SplayTree<T> where T : IComparable
    {
        public BinaryNode<T> root;

        public BinaryNode<T> nullNode;

        public BinaryNode<T> header = new BinaryNode<T>(default(T));

        public BinaryNode<T> newNode;

        public SplayTree()
        {
            nullNode = new BinaryNode<T>(default(T));

            nullNode.left = nullNode.right = nullNode;

            root = nullNode;
        }

        #region 插入
        /// <summary>
        /// 插入
        /// </summary>
        /// <param name="Key"></param>
        public void Insert(T Key)
        {
            if (newNode == null)
                newNode = new BinaryNode<T>(default(T));

            newNode.element = Key;

            if (root == nullNode)
            {
                newNode.left = newNode.right = nullNode;

                root = newNode;
            }
            else
            {
                root = Splay(Key, root);

                int compareResult = Key.CompareTo(root.element);

                if (compareResult < 0)
                {
                    newNode.left = root.left;

                    newNode.right = root;

                    root.left = nullNode;

                    root = newNode;
                }
                else
                    if (compareResult > 0)
                    {
                        newNode.right = root.right;

                        newNode.left = root;

                        root.right = nullNode;

                        root = newNode;
                    }
                    else
                        return;
            }

            newNode = null;
        }
        #endregion

        #region 是否包含
        /// <summary>
        /// 是否包含
        /// </summary>
        /// <param name="Key"></param>
        /// <returns></returns>
        public bool Contains(T Key)
        {
            if (isEmpty())
                return false;

            root = Splay(Key, root);

            return root.element.CompareTo(Key) == 0;
        }
        #endregion

        #region 判断是否为空
        /// <summary>
        /// 判断是否为空
        /// </summary>
        /// <returns></returns>
        public bool isEmpty()
        {
            return root == nullNode;
        }
        #endregion

        #region 伸展
        /// <summary>
        /// 伸展
        /// </summary>
        /// <param name="Key"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public BinaryNode<T> Splay(T Key, BinaryNode<T> tree)
        {
            BinaryNode<T> leftTreeMax, rightTreeMin;

            header.left = header.right = nullNode;

            leftTreeMax = rightTreeMin = header;

            nullNode.element = Key;

            while (true)
            {
                int compareResult = Key.CompareTo(tree.element);

                if (compareResult < 0)
                {
                    //如果成立,说明是”一字型“旋转
                    if (Key.CompareTo(tree.left.element) < 0)
                        tree = rotateWithLeftChild(tree);

                    if (tree.left == nullNode)
                        break;

                    //动态的将中间树的”当前节点“追加到 R 树中,同时备份在header中
                    rightTreeMin.left = tree;

                    rightTreeMin = tree;

                    tree = tree.left;
                }
                else if (compareResult > 0)
                {
                    //如果成立,说明是”一字型“旋转
                    if (Key.CompareTo(tree.right.element) > 0)
                        tree = rotateWithRightChild(tree);

                    if (tree.right == nullNode)
                        break;

                    //动态的将中间树的”当前节点“追加到 L 树中,同时备份在header中
                    leftTreeMax.right = tree;

                    leftTreeMax = tree;

                    tree = tree.right;
                }
                else
                {
                    break;
                }
            }

            /* 剥到最后一层,来最后一次切分 */
            //把中间树的左孩子给“左树”
            leftTreeMax.right = tree.left;

            //把中间树的右孩子给“右树”
            rightTreeMin.left = tree.right;

            /* 合并操作 */
            //将头节点的左树作为中间树的左孩子
            tree.left = header.right;

            //将头结点的右树作为中间树的右孩子
            tree.right = header.left;

            return tree;
        }
        #endregion

        #region 删除
        /// <summary>
        /// 删除
        /// </summary>
        /// <param name="Key"></param>
        public void Remove(T Key)
        {
            BinaryNode<T> newTree;

            //将删除结点顶到根节点
            root = Splay(Key, root);

            //不等于说明没有找到
            if (root.element.CompareTo(Key) != 0)
                return;

            //如果左边为空,则直接用root的右孩子接上去
            if (root.left == nullNode)
            {
                newTree = root.right;
            }
            else
            {
                newTree = root.left;

                newTree = Splay(Key, newTree);

                newTree.right = root.right;
            }
            root = newTree;
        }
        #endregion

        #region 右旋转
        /// <summary>
        /// 右旋转
        /// </summary>
        /// <param name="k1"></param>
        /// <returns></returns>
        public BinaryNode<T> rotateWithRightChild(BinaryNode<T> k1)
        {
            BinaryNode<T> k2 = k1.right;
            k1.right = k2.left;
            k2.left = k1;
            return k2;
        }
        #endregion

        #region 左旋转
        /// <summary>
        /// 左旋转
        /// </summary>
        /// <param name="k2"></param>
        /// <returns></returns>
        public BinaryNode<T> rotateWithLeftChild(BinaryNode<T> k2)
        {
            BinaryNode<T> k1 = k2.left;
            k2.left = k1.right;
            k1.right = k2;
            return k1;
        }
        #endregion
    }
}

伸展树可以总结成一幅图:

时间: 2025-01-27 00:49:29

6天通吃树结构—— 第四天 伸展树的相关文章

6天通吃树结构—— 第三天 Treap树

原文:6天通吃树结构-- 第三天 Treap树                我们知道,二叉查找树相对来说比较容易形成最坏的链表情况,所以前辈们想尽了各种优化策略,包括AVL,红黑,以及今天 要讲的Treap树.        Treap树算是一种简单的优化策略,这名字大家也能猜到,树和堆的合体,其实原理比较简单,在树中维护一个"优先级","优先级" 采用随机数的方法,但是"优先级"必须满足根堆的性质,当然是"大根堆"或者&q

6天通吃树结构—— 第五天 Trie树

原文:6天通吃树结构-- 第五天 Trie树      很有段时间没写此系列了,今天我们来说Trie树,Trie树的名字有很多,比如字典树,前缀树等等. 一:概念      下面我们有and,as,at,cn,com这些关键词,那么如何构建trie树呢? 从上面的图中,我们或多或少的可以发现一些好玩的特性.       第一:根节点不包含字符,除根节点外的每一个子节点都包含一个字符.       第二:从根节点到某一节点,路径上经过的字符连接起来,就是该节点对应的字符串.       第三:每个

6天通吃树结构—— 第一天 二叉查找树

        一直很想写一个关于树结构的专题,再一个就是很多初级点的码农会认为树结构无用论,其实归根到底还是不清楚树的实际用途.   一:场景: 1:现状     前几天我的一个大学同学负责的网站出现了严重的性能瓶颈,由于业务是写入和读取都是密集型,如果做缓存,时间间隔也只能在30s左 右,否则就会引起客户纠纷,所以同学也就没有做缓存,通过测试发现慢就慢在数据读取上面,总共需要10s,天啊...原来首页的加载关联 到了4张表,而且表数据中最多的在10w条以上,可以想象4张巨大表的关联,然后就是

浅述优化网站结构的四个好处

网站的结构优化非常重要,是做SEO过程中一个重要步骤.网站不仅仅需要外部做些调整,同时必须配合合理的内部SEO才能实现完美的SEO效果,所以今天我和大家总结优化网站结构的四个好处,希望对大家有所帮助. 首先是用户体验 在SEO角度来说,排名和流量是最重要的,但是用户体验同样重要.很多人访问你的网站第一眼是感观,好的观感能影响用户继续去点击你的其他页面或者链接,从而找到自己想要的信息.但是想要用户快速找到结果,必须依靠良好的导航,准确的内部链接和锚文字.网站好比一个大厦,里边的结构和功能设计十分完

伸展树

引用:http://digital.cs.usu.edu/~allan/DS/Notes/Ch22.pdf 一.简介:伸展树,或者叫自适应查找树,是一种用于保存有序集合的简单高效的数据结构.伸展树实质上是一个二叉查找树.允许查找,插入,删除,删除最小,删除最大,分割,合并等许多操作,这些操作的时间复杂度为O(logN).由于伸展树可以适应需求序列,因此他们的性能在实际应用中更优秀.伸展树支持所有的二叉树操作.伸展树不保证最坏情况下的时间复杂度为O(logN).伸展树的时间复杂度边界是均摊的.尽管

数据结构之伸展树详解_C 语言

1. 概述 二叉查找树(Binary Search Tree,也叫二叉排序树,即Binary Sort Tree)能够支持多种动态集合操作,它可以用来表示有序集合.建立索引等,因而在实际应用中,二叉排序树是一种非常重要的数据结构. 从算法复杂度角度考虑,我们知道,作用于二叉查找树上的基本操作(如查找,插入等)的时间复杂度与树的高度成正比.对一个含n个节点的完全二叉树,这些操作的最坏情况运行时间为O(log n).但如果因为频繁的删除和插入操作,导致树退化成一个n个节点的线性链(此时即为一个单链表

Java数据结构与算法解析(八)——伸展树

伸展树简介 伸展树(Splay Tree)是特殊的二叉查找树. 它的特殊是指,它除了本身是棵二叉查找树之外,它还具备一个特点: 当某个节点被访问时,伸展树会通过旋转使该节点成为树根.这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点. 特性 和普通的二叉查找树相比,具有任何情况下.任何操作的平摊O(log2n)的复杂度,时间性能上更好 和一般的平衡二叉树比如 红黑树.AVL树相比,维护更少的节点额外信息,空间性能更优,同时编程复杂度更低 在很多情况下,对于查找操作,后面的查询和之前的查询

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

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢!    我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每次搜索的复杂度为n的量级.AVL树通过动态平衡树的深度,单次搜索的复杂度为log(n) (以上参考纸上谈兵 AVL树).我们下面看伸展树(splay tree),它对于m次连续搜索操作有很好的效率.   伸展树会在一次搜索后,对树进行一些特殊的操作.这些操作的理念与AVL树有些类似,即通过旋转,

【BBST 之伸展树 (Splay Tree)】

最近"hiho一下"出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codeforces.com/problemset/problem/675/D,运行效果居然还挺好的,时间快了大概10%,内存少了大概30%. 1 #include <cstdio> 2 #include <cstring> 3 #include <string> 4