高级搜索——伸展树Splay详解

文章目录

  • 伸展树Splay
    • 伸展树Splay的定义
    • 局部性原理
    • Splay的伸展操作
      • 逐层伸展
      • 双层伸展
        • zig-zig/zag-zag
        • zig-zag/zag-zig
        • zig/zag
        • 双层伸展的效果与效率
    • 伸展树的实现
      • 动态版本实现
        • 递增分配器
        • 节点定义
        • Splay类及其接口定义
        • 伸展操作
          • 左单旋
          • 右单旋
          • 右左/左右双旋
          • 伸展
        • 查找操作
        • 删除操作
        • 插入操作
        • 完整代码
      • 静态版本实现
        • 结点定义
        • 接口定义
        • rotate操作
        • splay操作
        • find操作
        • get_pre操作
        • get_suc操作
        • erase操作
        • get_rnk操作
        • get_val操作
        • insert操作
        • init操作与哨兵结点的设置
        • 完整代码
    • *伸展树的性能分析与证明

伸展树Splay

对于二叉搜索树的平衡性我们有很多的处理方式,AVL树中引入平衡因子,红黑树中引入颜色,Treap中引入堆的性质,今天要介绍的Splay最为特殊,其利用了局部性原理,实现了每次访问达到均摊O(logn)的时间复杂度。

伸展树Splay的定义

伸展树(splay tree) ,也是平衡二叉搜索树的一种形式,但并非严格的平衡。但是其实现相较于AVL树和红黑树更为简捷。伸展树无需时刻都严格地保持全树的平衡,但却能够在任何足够长的真实操作序列中,保持分摊意义上的高效率。伸展树也不需要对基本的二叉树节点结构,做任何附加的要求或改动,更不需要记录平衡因子或高度之类的额外信息,故适用范围更广。

局部性原理

局部性(locality)可以分为时间局部性(temporal locality)空间局部性(spatial locality)

假如你在书桌旁工作,需要查阅某本书籍,便将书架上的书拿过来,查阅后又放回书架,但是你发现你着手的工作需要频繁地查阅这本书,于是你将书放到了书桌上,因为很快你就要再次查阅。这就是时间局部性的体现

当你找到一本关于EDSAC的早期经典计算机的书籍时,也许紧挨着它的另一本关于早期工业计算机的书籍里同样有你所需的材料,这也是为什么图书馆通常将主题相同的书放在同一个书架.上以提高空间定位效率。这就是空间局部性的体现

于是我们对局部性原理有如下理解:

1)刚刚被访问过的元素,极有可能在不久之后再次被访问到
2)将被访问的下一元素,极有可能就处于不久之前被访问过的某个元素的附近

充分利用好此类特性,即可进一步地提高数据结构和算法的效率。

那么对于我们的二叉搜索树而言,局部性就体现为:

1)刚刚被访问过的节点,极有可能在不久之后再次被访问到
2)将被访问的下一节点,极有可能就处于不久之前被访问过的某个节点的附近

D.DSleator和R.E.Tarjan于1985年发现只需将刚被访问的节点,及时地“转移”至树根(附近),即可加速后续的操作,在此基础上对二叉搜索树进行优化,并命名为伸展树(Splay)

Splay的伸展操作

前面提到了,Splay利用局部性原理,遵循对于访问过的节点将其转移到根节点,保证了均摊效率。那么显然,实现一种操作,能够将某节点移动到根节点,这个关键操作也是我们Splay的名称的由来,伸展(splay)。

逐层伸展

我们AVL树、红黑树、Treap调整平衡性的共同操作都是旋转(Treap也有无旋式实现方式),我们实现伸展的第一个选择就是利用旋转将其逐层旋转到根节点。

我们以下图为例,通过两次右单旋和一次左单旋将E节点旋转到了根节点的位置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

随着节点E的逐层上升,两侧子树的结构也不断地调整,故这一过程也形象地称作伸展(splay),而采用这一调整策略的二叉搜索树也因此得名。不过,为实现真正意义上的伸展树,还须对以上策略做点微妙而本质的改进。之所以必须改进,是因为目前的策略仍存在致命
的缺陷一-对于很多访问序列,单次访问的分摊时间复杂度在极端情况下可能高达O(n)。

即然上面的示例过于理想,我们不妨来试试最坏情况下的伸展,如左下图中左倾斜的序列{1,2,3,4,5},我们假设已经实现了查找操作find,每次find都会把查找的节点放到根节点,我们不妨依次查找1,2,3,4,5

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可见,在各次访问之后,为将对应节点伸展调整至树根,分别需做4、4、3、2和1次旋转。

如果在一般情况下,节点总数为n,旋转的总次数就是(n - 1) + (n-1) + (n-2) + … + 1 = (n^2 + n - 2) / 2 = O(n^2)

如此以来,n次查找的均摊时间复杂度就是O(n),这相比于最坏情况下的二叉搜索树毫无优势。更糟糕的是,上图中5次查找后,树的结构又复原了!也就是说我们上述情况还会重复。

实际上,上述特例可以推广到规模任意的简易伸展树,如果按照关键字的大小顺序查找,我们每次访问的均摊时间复杂度都是O(n)!

那么这类最坏情况是否可以回避?如何回避?

双层伸展

当我们将单层伸展改为双层伸展后,上述问题就迎刃而解。

即然我们要经过若干次旋转将节点旋转到根的位置,那么我们就使其每次旋转两次(追溯两层),如果其父节点为根,则旋转一次(追溯一层)。

前导英语储备zig(右旋)zag(左旋)

我们的旋转分为三种类型

zig-zig/zag-zag

可见zig-zig/zag-zag就是两次右/左单旋

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

zig-zag/zag-zig

可见zig-zag/zag-zig就是右左/左右双旋

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

zig/zag

可见zig/zag就是单次次右/左单旋

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

双层伸展的效果与效率

由上面的各种情况, 不难发现每经过一次伸展操作 节点v都会上升两层,如果v的初始深度为偶数,那么最终v将上升至树根,如果depth为奇数,那么当v上升至树根时的孩子时,只需执行单次旋转即可。

那么在双层伸展下,我们上面的最坏情况又会是怎样的结果呢?

我们仍以单倾斜的二叉搜索树为例,我们发现执行单次的search(1)操作,二者的结果产生了很大差别

可见,最深节点(1)被访问之后再经过双层调整,不仅同样可将该节点伸展至树根,而且同时可使树的高度接近于减半。就树的形态而言,双层伸展策略可“智能”地“折叠”被访问的子树分支,从而有效地避免对长分支的连续访问。这就意味着,即便节点v的深度为O(n),双层伸展策略既可将v推至树根,亦可令对应分支的长度以几何级数(大致折半)的速度收缩。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下图则展现出了节点更多,更具一般性的样例,展现出了双层伸展的效果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

尽管伸展树不像AVL树和红黑树那么严格,随时都有可能出现很深的节点,但是一旦该节点被访问,那么就会像含羞草似的,经过双层伸展,其分支都会收缩至长度大致减半。于是,即便每次都“恶意地”试图访问最底层节点,最坏情况也不会持续发生。可见,伸展树虽不能杜绝最坏情况的发生,却能有效地控制最坏情况发生的频度,从而在分摊意义下保证整体的高效率。

伸展树的实现

伸展树的实现逻辑非常简单,就是在基础二叉搜索树的操作上,对于操作中访问的节点将其Splay到根节点的位置,不过具体实现上还是由些微差别的。

实现方式无非就是动态和静态两种方式,静态版本由于逻辑简单和代码简洁性而在OJ题目中广泛应用。

动态版本实现

递增分配器

关于递增分配器的作用,在[高级搜索-线段树C/C++]-CSDN博客已经有所介绍,其实就是一个用来充当缓存的链表,我们如果每次开节点都要new一个出来的话效率太低,所以我们可以提前开一部分出来,然后用的时候拿出来即可,这里不再过多赘述。

// 递增分配器
template <class T>
class CacheObj
{
public:
    void *operator new(size_t)
    {
        if (!_head)
        {
            T *a = new T[SIZE];
            for (size_t i = 0; i < SIZE; i++)
                add(a + i);
        }
        T *ret = _head;
        _head = _head->CacheObj<T>::_next;
        return ret;
    }
    void operator delete(void *p, size_t)
    {
        if (p)
            add(static_cast<T *>(p));
    }
    virtual ~CacheObj() {}

protected:
    T *_next;

private:
    static T *_head;
    static const size_t SIZE;
    static void add(T *p)
    {
        p->CacheObj<T>::_next = _head;
        _head = p;
    }
};
template <class T>
T *CacheObj<T>::_head = nullptr;
template <class T>
const size_t CacheObj<T>::SIZE = 10000;
节点定义

动态版本的我们先不对权值进行扩展,只是先实现一棵基本的Splay,所以节点定义和普通的二叉搜索树相同。

class Node : public CacheObj<Node>
{
public:
    Node(int v = 0) : _v(v), _left(nullptr), _right(nullptr), _parent(nullptr)
    {
    }
    int _v;
    Node *_left;
    Node *_right;
    Node *_parent;
};
Splay类及其接口定义
    class Splay
    {
    public:
        Splay() : _root(nullptr) {}
        Node *splay(Node *p, Node *parent = nullptr)//伸展
        {};
        Node *search(int x)//查找
        {};
        Node *insert(int x)//插入
        {};
        bool erase(int x)//删除
        {};
    public:
        Node *_root;
    };
伸展操作

伸展操作就是判断三种情况然后调用对应的旋转,旋转的实现是二叉搜索树中最基础的,如需详解,可见[AVL树详解C++]-CSDN博客

左单旋
    void RotateL(Node *parent)
    {
        Node *SubR = parent->_right;
        Node *SubRL = SubR->_left;
        Node *ppNode = parent->_parent;

        parent->_right = SubRL;
        if (SubRL)
            SubRL->_parent = parent;
        SubR->_left = parent;
        parent->_parent = SubR;

        if (_root == parent)
        {
            _root = SubR;
        }
        else
        {
            if (ppNode->_left == parent)
                ppNode->_left = SubR;
            else
                ppNode->_right = SubR;
        }
        SubR->_parent = ppNode;
    }

右单旋
    void RotateR(Node *parent)
    {
        Node *SubL = parent->_left;
        Node *SubLR = SubL->_right;
        Node *ppNode = parent->_parent;

        parent->_left = SubLR;
        if (SubLR)
            SubLR->_parent = parent;
        SubL->_right = parent;
        parent->_parent = SubL;

        if (_root == parent)
        {
            _root = SubL;
        }
        else
        {
            if (ppNode->_left == parent)
                ppNode->_left = SubL;
            else
                ppNode->_right = SubL;
        }
        SubL->_parent = ppNode;
    }
右左/左右双旋
    void RotateRL(Node *root)
    {
        RotateR(root->_right);
        RotateL(root);
    }
    void RotateLR(Node *root)
    {
        RotateL(root->_left);
        RotateR(root);
    }
伸展

对于伸展接口定义如下splay(Node *p, Node *parent = nullptr),意为将节点p伸展到parent下面,当parent为空自然是伸展至根节点

操作流程:

  • 获取节点p的父节点pNode和祖父节点ppNode
  • 根据三种情况执行对应的旋转
  • 当p的父节点为parent时,伸展结束
  • 返回p节点
    Node *splay(Node *p, Node *parent = nullptr)
    {
        if (!p)
            return p;
        while (p->_parent != parent)
        {
            Node *pNode = p->_parent;
            Node *ppNode = pNode->_parent;
            if (ppNode != parent)
            {
                if (ppNode->_left == pNode)
                {
                    if (pNode->_left == p)
                        RotateR(ppNode);
                    else
                    {
                        RotateLR(ppNode);
                        continue;
                    }
                }
                else if (ppNode->_right == pNode && pNode->_right == p)
                {
                    if (pNode->_right == p)
                        RotateL(ppNode);
                    else
                    {
                        RotateRL(ppNode);
                        continue;
                    }
                }
            }
            if (pNode->_left == p)
                RotateR(pNode);
            else
                RotateL(pNode);
        }
        return p;
    }
查找操作

按照二叉搜索树的查找规则去找对应键值的节点,找到就返回,否则返回前驱节点,对于返回节点要执行伸展操作来保证均摊时间复杂度。

操作流程:

  • 待查找结点键值x
  • 节点cur用于遍历,pre用于保存前驱节点(注意,pre键值可能小于x也可能大于x)
  • cur键值等于x,说明找到,break
  • cur键值小于x就往右走,大于x就往左走
  • 对最终的返回节点进行伸展操作(保证均摊时间复杂度的关键)
    Node *find(int x)
    {
        Node *cur = _root, *pre = nullptr;
        while (cur)
        {
            pre = cur;
            if (cur->_v == x)
                break;
            else if (cur->_v > x)
                cur = cur->_left;
            else
                cur = cur->_right;
        }
        return splay(cur ? cur : pre);
    }
删除操作

像AVL树和红黑树的删除操作可以说是十分繁琐,而对于我们的Splay来讲,却没有这般烦恼。

对于二叉搜索树的删除操作的策略都是替换删除法,Splay也不例外,我们可以按照二叉搜索树删除模式,删除后对其后继节点进行伸展,但是我们的Splay操作可以直接将待删除结点提升到根节点的位置,所以我们可以采用另一种策略来进行删除。

对于待删除关键字x,我们用search查询x,不妨假设找到了且其左右孩子不为空,那么我们将左子树断开,删除关键字结点,并将其右孩子作为根,再次查找x,这次会查找失败但是会将x结点的后继结点提升到根,这时再将左子树重新连接,我们就又得到了一棵Splay。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

操作流程:

  • 待删除关键字x,查找x
  • 查找失败返回false,成功则保存结点为del
  • 左为空,将右孩子作为根,右为空,左孩子作为根
  • 左右都不为空,断开左子树,右孩子作为根,再次查找x,再将左子树连接
  • 删除待删除结点del,返回true
        bool erase(int x)
        {
            if (!_root || search(x)->_v != x)
                return false;
            Node *del = _root;
            if (!_root->_left)
            {
                _root = _root->_right;
                if (_root)
                    _root->_parent = nullptr;
            }
            else if (!_root->_right)
            {
                _root = _root->_left;
                if (_root)
                    _root->_parent = nullptr;
            }
            else
            {
                Node *SubL = _root->_left;
                SubL->_parent = _root->_left = nullptr;
                _root = _root->_right;
                _root->_parent = nullptr;
                search(del->_v);
                _root->_left = SubL;
                SubL->_parent = _root;
            }
            delete del;
            return true;
        }
插入操作

插入操作同样利用了splay()伸展功能,对于待插入关键字x,查找返回后,树根节点要么等于查找目标(查找成功),要么就是拟插入节点的直接前驱或直接后继(查找失败)。因此,不妨改用如下方法实现插入功能。

为将关键字x插至伸展树T中,首先调用查找接口search(x),于是,其中最后被访问的节点pre,将通过伸展被提升为树根。
根据k与pre存储关键字的大小关系(不妨排除二者相等的情况),以pre为界将整棵树分裂为两棵子树。按照大小关系将pre插入到x的左节点或者右节点即可然后更新根节点为x(局部性原理)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

操作流程:

  • 待插入关键字x,查找x
  • 查找成功返回_root,成功则保存结点为pre
  • 根更新为关键字为x的新结点
  • pre->_v > x,则pre为_root右孩子,pre的左给root,同时维护pre的左孩子父节点为root(如果pre的左孩子非空)
  • pre->_v < x,则pre为_root左孩子,pre的右给root,同时维护pre的右孩子父节点为root(如果pre的右孩子非空)
  • 返回_root
        Node *insert(int x)
        {
            if (!_root)
            {
                return _root = new Node(x);
            }

            Node *pre = search(x);
            if (pre->_v == x)
                return _root;

            if (pre->_v > x)
            {
                pre->_parent = _root = new Node(x, pre->_left, pre);
                if (pre->_left)
                    pre->_left->_parent = _root;
                pre->_left = nullptr;
            }
            else if (pre->_v < x)
            {
                pre->_parent = _root = new Node(x, pre, pre->_right);
                if (pre->_right)
                    pre->_right->_parent = _root;
                pre->_right = nullptr;
            }
            return _root;
        }
完整代码
namespace D_Splay
{
    // 递增分配器
    template <class T>
    class CacheObj
    {
    public:
        void *operator new(size_t)
        {
            if (!_head)
            {
                T *a = new T[SIZE];
                for (size_t i = 0; i < SIZE; i++)
                    add(a + i);
            }
            T *ret = _head;
            _head = _head->CacheObj<T>::_next;
            return ret;
        }
        void operator delete(void *p, size_t)
        {
            if (p)
                add(static_cast<T *>(p));
        }
        virtual ~CacheObj() {}

    protected:
        T *_next;

    private:
        static T *_head;
        static const size_t SIZE;
        static void add(T *p)
        {
            p->CacheObj<T>::_next = _head;
            _head = p;
        }
    };
    template <class T>
    T *CacheObj<T>::_head = nullptr;
    template <class T>
    const size_t CacheObj<T>::SIZE = 10000;
    class Node : public CacheObj<Node>
    {
    public:
        Node(int v = 0, Node *left = nullptr, Node *right = nullptr) : _v(v), _left(left), _right(right), _parent(nullptr)
        {
        }
        int _v;
        Node *_left;
        Node *_right;
        Node *_parent;
    };
    class Splay
    {
    public:
        Splay() : _root(nullptr) {}
        Node *splay(Node *p, Node *parent = nullptr)
        {
            if (!p)
                return p;
            while (p->_parent != parent)
            {
                Node *pNode = p->_parent;
                Node *ppNode = pNode->_parent;
                if (ppNode != parent)
                {
                    if (ppNode->_left == pNode)
                    {
                        if (pNode->_left == p)
                            RotateR(ppNode);
                        else
                        {
                            RotateLR(ppNode);
                            continue;
                        }
                    }
                    else if (ppNode->_right == pNode && pNode->_right == p)
                    {
                        if (pNode->_right == p)
                            RotateL(ppNode);
                        else
                        {
                            RotateRL(ppNode);
                            continue;
                        }
                    }
                }
                if (pNode->_left == p)
                    RotateR(pNode);
                else
                    RotateL(pNode);
            }
            return p;
        }
        Node *search(int x)
        {
            Node *cur = _root, *pre = nullptr;
            while (cur)
            {
                pre = cur;
                if (cur->_v == x)
                    break;
                else if (cur->_v > x)
                    cur = cur->_left;
                else
                    cur = cur->_right;
            }
            return splay(cur ? cur : pre);
        }
        Node *insert(int x)
        {
            if (!_root)
            {
                return _root = new Node(x);
            }

            Node *pre = search(x);
            if (pre->_v == x)
                return _root;

            if (pre->_v > x)
            {
                pre->_parent = _root = new Node(x, pre->_left, pre);
                if (pre->_left)
                    pre->_left->_parent = _root;
                pre->_left = nullptr;
            }
            else if (pre->_v < x)
            {
                pre->_parent = _root = new Node(x, pre, pre->_right);
                if (pre->_right)
                    pre->_right->_parent = _root;
                pre->_right = nullptr;
            }
            return _root;
        }
        bool erase(int x)
        {
            if (!_root || search(x)->_v != x)
                return false;
            Node *del = _root;
            if (!_root->_left)
            {
                _root = _root->_right;
                if (_root)
                    _root->_parent = nullptr;
            }
            else if (!_root->_right)
            {
                _root = _root->_left;
                if (_root)
                    _root->_parent = nullptr;
            }
            else
            {
                Node *SubL = _root->_left;
                SubL->_parent = _root->_left = nullptr;
                _root = _root->_right;
                _root->_parent = nullptr;
                search(del->_v);
                _root->_left = SubL;
                SubL->_parent = _root;
            }
            delete del;
            return true;
        }
        void Inorder()
        {
            _InOrder(_root);
        }

    private:
        void _InOrder(Node *root)
        {
            if (!root)
                return;
            _InOrder(root->_left);
            cout << root->_v << " ";
            _InOrder(root->_right);
        }
        void RotateL(Node *parent)
        {
            Node *SubR = parent->_right;
            Node *SubRL = SubR->_left;
            Node *ppNode = parent->_parent;

            parent->_right = SubRL;
            if (SubRL)
                SubRL->_parent = parent;
            SubR->_left = parent;
            parent->_parent = SubR;

            if (_root == parent)
            {
                _root = SubR;
            }
            else
            {
                if (ppNode->_left == parent)
                    ppNode->_left = SubR;
                else
                    ppNode->_right = SubR;
            }
            SubR->_parent = ppNode;
        }
        void RotateR(Node *parent)
        {
            Node *SubL = parent->_left;
            Node *SubLR = SubL->_right;
            Node *ppNode = parent->_parent;

            parent->_left = SubLR;
            if (SubLR)
                SubLR->_parent = parent;
            SubL->_right = parent;
            parent->_parent = SubL;

            if (_root == parent)
            {
                _root = SubL;
            }
            else
            {
                if (ppNode->_left == parent)
                    ppNode->_left = SubL;
                else
                    ppNode->_right = SubL;
            }
            SubL->_parent = ppNode;
        }
        void RotateRL(Node *root)
        {
            RotateR(root->_right);
            RotateL(root);
        }
        void RotateLR(Node *root)
        {
            RotateL(root->_left);
            RotateR(root);
        }

    public:
        Node *_root;
    };
}

静态版本实现

动态版本虽然实现逻辑简单,但是还是有一定的代码量的,我们这里来介绍以下代码更为简洁的静态版本。、

静态版本下我们实现很多操作会更为方便,于是像treap一样,我们对结点引入权值w代表当前结点的数目,size代表以当前结点为根的子树结点数目

结点定义
const int N = 1e5 + 10;
struct Node
{
    Node() = default;
    int _c[2]; // 孩子节点
    int _p;    // 父节点
    int _v;    // 值域
    int _w;    // 权值
    int _size; // 子树节点数目
    void init(int p, int v)
    {
        _p = p, _v = v;
        _w = _size = 1;
    }
} tr[N];
int root = 0, idx = 0;
接口定义

接口看起来很多,但实现起来其实十分简洁,特别是旋转操作我们由三个旋转简化为了一个旋转

void pushup(int x);//更新size
void rotate(int x);//旋转
void splay(int x, int p);
void find(int v);//查找
int get_pre(int v);//获得前驱
int get_suc(int v);//获得后继
void erase(int v);//删除
int get_rnk(int v);//获取排名
int get_val(int k);//获取第k小元素
void insert(int v);//插入
void Init();//初始化
rotate操作

静态版本的rotate是根据待旋转结点与父节点以及当前结点的位置关系来判断进行左旋还是右旋 ,由于静态版本结点都是通过下标索引,所以我们可以大大简化代码量,具体流程看代码注释。

void pushup(int x)
{
    tr[x]._size = tr[tr[x]._c[0]]._size + tr[tr[x]._c[1]]._size + tr[x]._w;
}
void rotate(int x)
{
    int y = tr[x]._p, z = tr[y]._p;//y为父结点下标,z为祖先结点下标
    int k = tr[y]._c[1] == x;//k为判断右孩子是否为x,是为1,否则为0
    tr[y]._c[k] = tr[x]._c[k ^ 1];//k为1就把x的左孩子给y的右孩子,否则把x的右孩子给y的左孩子
    tr[tr[x]._c[k ^ 1]]._p = y;//把y的新孩子的父节点置为y
    tr[x]._c[k ^ 1] = y;//y置为x的孩子
    tr[y]._p = x;//y的父节点置为x
    tr[z]._c[tr[z]._c[1] == y] = x;//根据z和y的关系来更新z的新孩子
    tr[x]._p = z;//更新x的父节点
    pushup(y), pushup(x);//更新x和y的size
}
splay操作

我们的双层伸展也变得十分简单

操作流程:

  • 待伸展结点x伸展到p下面
  • y为x父节点,z为祖先结点
  • z不为p,则说明要进行双层伸展,根据直线型或者是折线型决定第一次旋转x还是y
  • 旋转x
  • x的父结点为p就退出循环
void splay(int x, int p)
{
    while (tr[x]._p != p)
    {
        int y = tr[x]._p, z = tr[y]._p;
        if (p != z)
        {
            (tr[y]._c[0] == x) ^ (tr[z]._c[0] == y) ? rotate(x) : rotate(y);
        }
        rotate(x);
    }
    if (!p)
        root = x;
}
find操作

同样是BST的查找结点流程,只不过静态版本的更为简洁,同样的,没找到v我们就返回最后一个不为空的结点

注意:这里while循环条件严格意义上其实是不对的,但是由于我们插入了-1e9和1e9两个哨兵结点保证了树不为空所以是对的,后面的init有讲

void find(int v)
{
    int x = root;
    while (tr[x]._c[tr[x]._v < v] && v != tr[x]._v)
        x = tr[x]._c[tr[x]._v < v];
    // 没找到时返回v的前驱或后继
    splay(x, 0);
}
get_pre操作

同样由于我们插入了-1e9和1e9两个哨兵结点保证了树不为空所以是对的

操作流程:

  • 查找v,如果此时查找返回结点被提升到根
  • 根的关键字小于v就返回根
  • 否则返回根左子树的最右结点,返回前进行伸展操作
int get_pre(int v)
{
    find(v); // v在根或者其前驱或后继在根
    int x = root;
    if (tr[x]._v < v)
        return x;
    x = tr[x]._c[0];
    while (tr[x]._c[1])
        x = tr[x]._c[1];
    splay(x, 0);
    return x;
}
get_suc操作

和获得前驱类似的流程,同样由于我们插入了-1e9和1e9两个哨兵结点保证了树不为空所以是对的

操作流程:

  • 查找v,如果此时查找返回结点被提升到根
  • 根的关键字大于v就返回根
  • 否则返回根右子树的最左结点,返回前进行伸展操作
int get_suc(int v)
{
    find(v);
    int x = root;
    if (tr[x]._v > v)
        return x;
    x = tr[x]._c[1];
    while (tr[x]._c[0])
        x = tr[x]._c[0];
    splay(x, 0);
    return x;
}
erase操作

erase的操作正确性的关键也基于我们插入了-1e9和1e9两个哨兵结点保证了树不为空,但是erase没有对待删除结点不在树里面进行特判!

  • 操作流程
  • 我们先获取前驱pre,再获取后继suc
  • 将pre伸展到根,将suc伸展到pre下面
  • 由于pre,v,suc三个结点为中序序列中连续的三个节点,所以此时位置关系为pre在根,suc在pre的右孩子,那么待删除v只能在pre的左孩子,所以我们删除pre的左孩子del
  • 如果del权值大于1,那么权值-1同时splaydel
  • 否则suc左孩子置空,伸展suc到根
void erase(int v)
{
    int pre = get_pre(v), suc = get_suc(v);
    splay(pre, 0), splay(suc, pre);
    int del = tr[suc]._c[0];
    if (tr[del]._w > 1)
        tr[del]._w--, splay(del, 0);
    else
        tr[suc]._c[0] = 0, splay(suc, 0);
}
get_rnk操作

注意我们树中-1e9和1e9两个哨兵的存在

查找v,此时v或者v的前驱或后继在根的位置,由于哨兵结点-1e9的存在,根结点左子树的size为根节点的排名

如果根小于v,左子树size为我们返回左子树size+1

否则根节点大于等于v,左子树size就是排名

int get_rnk(int v)
{
    find(v);
    return tr[root]._v < v ? tr[tr[root]._c[0]]._size + 1 : tr[tr[root]._c[0]]._size;
}
get_val操作

查找第k名元素,由于哨兵-1e9的存在,我们实际上要返回排名k + 1的结点

查找流程

  • 查找排名k,遍历结点x,x的左孩子y
  • 如果k + 2大于整棵树的结点数目我们就返回-1,否则k = k + 1
  • 否则,如果x的权值和y的size和小于k,我们就往右子树查找k - tr[y]._size + tr[x]._w的元素
  • 如果说左孩子y的size大于等于k,那么到左子树去找
  • 否则x就是目标结点
  • 将x伸展到根
int get_val(int k) // 获取第k名元素
{
    if (k + 2 > tr[root]._size)
        return -1;
    int x = root;
    ++k;
    while (1)
    {
        int y = tr[x]._c[0];
        if (tr[y]._size + tr[x]._w < k)
        {
            k -= tr[y]._size + tr[x]._w;
            x = tr[x]._c[1];
        }
        else
        {
            if (tr[y]._size >= k)
                x = y;
            else
                break;
        }
    }
    splay(x, 0);
    return tr[x]._v;
}
insert操作

我们按照BST的插入方式进行插入即可,结束时把新结点伸展到根

void insert(int v)
{
    int x = root, p = 0;
    while (x && tr[x]._v != v)
        p = x, x = tr[x]._c[tr[x]._v < v];
    if (x)
        tr[x]._w++;
    else
    {
        x = ++idx;
        tr[p]._c[tr[p]._v < v] = x;
        tr[x].init(p, v);
    }
    splay(x, 0);
}
init操作与哨兵结点的设置

初始化就是把idx和root置为0,tr数组置为0

为了减少各种操作对于空树的特判,我们选择插入两个哨兵结点,保证获取前驱后继都能找到,查找排名都能查到

void Init()
{
    idx = root = 0;
    memset(tr, 0, sizeof(tr));
    insert(-1e9), insert(1e9);
}
完整代码
#define int long long
const int N = 1e5 + 10;
struct Node
{
    Node() = default;
    int _c[2]; // 孩子节点
    int _p;    // 父节点
    int _v;    // 值域
    int _w;    // 权值
    int _size; // 子树节点数目
    void init(int p, int v)
    {
        _p = p, _v = v;
        _w = _size = 1;
    }
} tr[N];
int root = 0, idx = 0;
void pushup(int x)
{
    tr[x]._size = tr[tr[x]._c[0]]._size + tr[tr[x]._c[1]]._size + tr[x]._w;
}
void rotate(int x)
{
    int y = tr[x]._p, z = tr[y]._p;
    int k = tr[y]._c[1] == x;
    tr[y]._c[k] = tr[x]._c[k ^ 1];
    tr[tr[x]._c[k ^ 1]]._p = y;
    tr[x]._c[k ^ 1] = y;
    tr[y]._p = x;
    tr[z]._c[tr[z]._c[1] == y] = x;
    tr[x]._p = z;
    pushup(y), pushup(x);
}
void splay(int x, int p)
{
    while (tr[x]._p != p)
    {
        int y = tr[x]._p, z = tr[y]._p;
        if (p != z)
        {
            (tr[y]._c[0] == x) ^ (tr[z]._c[0] == y) ? rotate(x) : rotate(y);
        }
        rotate(x);
    }
    if (!p)
        root = x;
}
void find(int v)
{
    int x = root;
    while (tr[x]._c[tr[x]._v < v] && v != tr[x]._v)
        x = tr[x]._c[tr[x]._v < v];
    // 没找到时返回v的前驱或后继
    splay(x, 0);
}
int get_pre(int v)
{
    find(v); // v在根或者其前驱或后继在根
    int x = root;
    if (tr[x]._v < v)
        return x;
    x = tr[x]._c[0];
    while (tr[x]._c[1])
        x = tr[x]._c[1];
    splay(x, 0);
    return x;
}
int get_suc(int v)
{
    find(v);
    int x = root;
    if (tr[x]._v > v)
        return x;
    x = tr[x]._c[1];
    while (tr[x]._c[0])
        x = tr[x]._c[0];
    splay(x, 0);
    return x;
}
void erase(int v)
{
    int pre = get_pre(v), suc = get_suc(v);
    splay(pre, 0), splay(suc, pre);
    int del = tr[suc]._c[0];
    if (tr[del]._w > 1)
        tr[del]._w--, splay(del, 0);
    else
        tr[suc]._c[0] = 0, splay(suc, 0);
}
int get_rnk(int v)
{
    find(v);
    return tr[root]._v < v ? tr[tr[root]._c[0]]._size + 1 : tr[tr[root]._c[0]]._size;
}
int get_val(int k) // 获取第k名元素
{
    if (k + 2 > tr[root]._size)
        return -1;
    int x = root;
    ++k;
    while (1)
    {
        int y = tr[x]._c[0];
        if (tr[y]._size + tr[x]._w < k)
        {
            k -= tr[y]._size + tr[x]._w;
            x = tr[x]._c[1];
        }
        else
        {
            if (tr[y]._size >= k)
                x = y;
            else
                break;
        }
    }
    splay(x, 0);
    return tr[x]._v;
}
void insert(int v)
{
    int x = root, p = 0;
    while (x && tr[x]._v != v)
        p = x, x = tr[x]._c[tr[x]._v < v];
    if (x)
        tr[x]._w++;
    else
    {
        x = ++idx;
        tr[p]._c[tr[p]._v < v] = x;
        tr[x].init(p, v);
    }
    splay(x, 0);
}
void inorder(int x)
{
    if (!x)
        return;
    inorder(tr[x]._c[0]);
    cout << tr[x]._v << " ";
    inorder(tr[x]._c[1]);
}
void Init()
{
    idx = root = 0;
    memset(tr, 0, sizeof(tr));
    insert(-1e9), insert(1e9);
}
#include <iostream>
#include <ctime>
#include <cstring>
#include <vector>
using namespace std;
#define int long long
const int N = 1e5 + 10;
struct Node
{
    Node() = default;
    int _c[2]; // 孩子节点
    int _p;    // 父节点
    int _v;    // 值域
    int _w;    // 权值
    int _size; // 子树节点数目
    void init(int p, int v)
    {
        _p = p, _v = v;
        _w = _size = 1;
    }
} tr[N];
int root = 0, idx = 0;
void pushup(int x)
{
    tr[x]._size = tr[tr[x]._c[0]]._size + tr[tr[x]._c[1]]._size + tr[x]._w;
}
void rotate(int x)
{
    int y = tr[x]._p, z = tr[y]._p;
    int k = tr[y]._c[1] == x;
    tr[y]._c[k] = tr[x]._c[k ^ 1];
    tr[tr[x]._c[k ^ 1]]._p = y;
    tr[x]._c[k ^ 1] = y;
    tr[y]._p = x;
    tr[z]._c[tr[z]._c[1] == y] = x;
    tr[x]._p = z;
    pushup(y), pushup(x);
}
void splay(int x, int p)
{
    while (tr[x]._p != p)
    {
        int y = tr[x]._p, z = tr[y]._p;
        if (p != z)
        {
            (tr[y]._c[0] == x) ^ (tr[z]._c[0] == y) ? rotate(x) : rotate(y);
        }
        rotate(x);
    }
    if (!p)
        root = x;
}
void find(int v)
{
    int x = root;
    while (tr[x]._c[tr[x]._v < v] && v != tr[x]._v)
        x = tr[x]._c[tr[x]._v < v];
    // 没找到时返回v的前驱或后继
    splay(x, 0);
}
int get_pre(int v)
{
    find(v); // v在根或者其前驱或后继在根
    int x = root;
    if (tr[x]._v < v)
        return x;
    x = tr[x]._c[0];
    while (tr[x]._c[1])
        x = tr[x]._c[1];
    splay(x, 0);
    return x;
}
int get_suc(int v)
{
    find(v);
    int x = root;
    if (tr[x]._v > v)
        return x;
    x = tr[x]._c[1];
    while (tr[x]._c[0])
        x = tr[x]._c[0];
    splay(x, 0);
    return x;
}
void erase(int v)
{
    int pre = get_pre(v), suc = get_suc(v);
    splay(pre, 0), splay(suc, pre);
    int del = tr[suc]._c[0];
    if (tr[del]._w > 1)
        tr[del]._w--, splay(del, 0);
    else
        tr[suc]._c[0] = 0, splay(suc, 0);
}
int get_rnk(int v)
{
    find(v);
    return tr[root]._v < v ? tr[tr[root]._c[0]]._size + 1 : tr[tr[root]._c[0]]._size;
}
int get_val(int k) // 获取第k名元素
{
    if (k + 2 > tr[root]._size)
        return -1;
    int x = root;
    ++k;
    while (1)
    {
        int y = tr[x]._c[0];
        if (tr[y]._size + tr[x]._w < k)
        {
            k -= tr[y]._size + tr[x]._w;
            x = tr[x]._c[1];
        }
        else
        {
            if (tr[y]._size >= k)
                x = y;
            else
                break;
        }
    }
    splay(x, 0);
    return tr[x]._v;
}
void insert(int v)
{
    int x = root, p = 0;
    while (x && tr[x]._v != v)
        p = x, x = tr[x]._c[tr[x]._v < v];
    if (x)
        tr[x]._w++;
    else
    {
        x = ++idx;
        tr[p]._c[tr[p]._v < v] = x;
        tr[x].init(p, v);
    }
    splay(x, 0);
}
void inorder(int x)
{
    if (!x)
        return;
    inorder(tr[x]._c[0]);
    cout << tr[x]._v << " ";
    inorder(tr[x]._c[1]);
}
void Init()
{
    idx = root = 0;
    memset(tr, 0, sizeof(tr));
    insert(-1e9), insert(1e9);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    Init();
    int n, opt, x;
    cin >> n;
    while (n--)
    {
        cin >> opt >> x;
        switch (opt)
        {
        case 1:
            insert(x);
            break;
        case 2:
            erase(x);
            break;
        case 3:
            cout << get_rnk(x) << '\n';
            break;
        case 4:
            cout << get_val(x) << '\n';
            break;
        case 5:
            cout << tr[get_pre(x)]._v << '\n';
            break;
        case 6:
            cout << tr[get_suc(x)]._v << '\n';
            break;
        default:
            break;
        }
    }
    return 0;
}

*伸展树的性能分析与证明

伸展树的单次操作的时间不同情况下差异很大,并不能始终保证在log(n)内,故需从平摊的角度来计算其时间复杂度,我们采用平摊分析法来分析,但是伸展树的性能分析更为复杂,故而采用算法平摊分析中的势能分析法(Potential Method)。

仿照物理学的思想和概念,这里可假想式地认为,每棵伸展树S都具有一定量(非负)的势能(potential) ,记作中Φ(S)。于是,若经过某- -操作并相应地通过旋转完成伸展之后S演化为另一伸展树S’,则对应的势能变化为:
Δ Φ = Φ ( S ′ ) − Φ ( S ) \Delta \Phi = \Phi (S')-\Phi(S) ΔΦ=Φ(S)Φ(S)
那么对于一棵伸展树S0而言,连续进行m(m >> n)次操作的过程中,记第i次操作后的伸展树为Si,则有

Δ Φ = Φ ( S i ) − Φ ( S i − 1 ) , 1 < = i < = m \Delta \Phi = \Phi (Si)-\Phi(Si-1),1 <= i <= m ΔΦ=Φ(Si)Φ(Si1)1<=i<=m
如果从整体来看就有
Δ Φ = ∑ i = 1 m [ Φ ( S i ) − Φ ( S i − 1 ) ] = Φ ( S m ) − Φ ( S 0 ) \Delta \Phi = {\textstyle \sum_{i=1}^{m}} [\Phi (Si)-\Phi(Si-1)] = \Phi(Sm)-\Phi(S0) ΔΦ=i=1m[Φ(Si)Φ(Si1)]=Φ(Sm)Φ(S0)
也就是说,整体的势能变化量仅取决于最初和最终状态一这与物理学中势能场的规律吻合。势能函数与物理学中势能的另一相似之处在于,它也可以被看作是能量(计算成本)的一种存在形式。比如,当某一步计算实际所需的时间小于分摊复杂度时,则可理解为通过势能的增加将提前支出的计算成本存储起来;反之,在前者大于后者时,则可从此前积累的势能中支取相应量用于支付超出的计算成本。

我们记第i次操作的分摊复杂度为实际复杂度和势能变化量之和:
A = T i + Δ Φ i A = Ti + \Delta\Phi i A=Ti+ΔΦi

∑ i = 1 m A i = ∑ i = 1 m T i + [ Φ ( S m ) − Φ ( S 0 ) ] {\textstyle \sum_{i=1}^{m}}Ai = {\textstyle \sum_{i=1}^{m}}Ti + [\Phi(Sm)-\Phi(S0)] i=1mAi=i=1mTi+[Φ(Sm)Φ(S0)]

这样一来,我们实际运行时间 ∑ i = 1 m T i 不会超过总体的分摊运行时间 ∑ i = 1 m A i ,所以后者是前者的上界 这样一来,我们实际运行时间{\textstyle \sum_{i=1}^{m}}Ti不会超过总体的分摊运行时间{\textstyle \sum_{i=1}^{m}}Ai,所以后者是前者的上界 这样一来,我们实际运行时间i=1mTi不会超过总体的分摊运行时间i=1mAi,所以后者是前者的上界
我们的算法界大牛R.E.Tarjan采用如下势能函数
Φ ( S ) = ∑ v ∈ S l o g ∣ v ∣ ,其中 ∣ v ∣ = 结点 v 的后代数目 \Phi(S) ={\textstyle \sum_{v\in S}^{}}log\left| v \right|,其中\left| v\right|=结点v的后代数目 Φ(S)=vSlogv,其中v=结点v的后代数目

证明了伸展树单次操作的分摊时间复杂度为O(logn)。为此,以下将分三种情况(其余情况不过是它们的对称形式)证明:

在对节点v的伸展过程中,每一步调整所需时间均不超过v的势能变化的3倍,即:3[Φ`(v) - Φ(v)]*

情况一:zig

该情况在双层伸展中最多出现一次,即最后一步伸展,所以这一步的实际复杂度T为O(1),但是我们势能定义为后代数目,所以势能其实是变化了的,那么我们的分摊复杂度为:
A = T + Δ Φ = 1 + Δ Φ ( p ) + Δ Φ ( v ) = 1 + [ Φ ′ ( v ) − Φ ( v ) ] A = T + \Delta\Phi = 1 + \Delta\Phi(p) + \Delta\Phi(v) = 1 + [\Phi '(v) - \Phi(v)] A=T+ΔΦ=1+ΔΦ(p)+ΔΦ(v)=1+[Φ(v)Φ(v)]
情况二:zig-zag

对于双旋而言,实际调整时间为 T= O(2),而我们的pushdown操作也揭示了势能变化量,则有
A = T + Δ Φ = 2 + Δ Φ ( v ) + Δ Φ ( p ) + Δ Φ ( g ) = 2 + Φ ′ ( v ) − Φ ( v ) + Φ ′ ( p ) − Φ ( p ) + Φ ′ ( g ) − Φ ( g ) = 2 + Φ ′ ( p ) + Φ ′ ( g ) − Φ ( v ) − Φ ( p ) , ( Φ ′ ( v ) = Φ ( g ) v 伸展后的 s i z e 和 g 初始 s i z e 相同 ) < = 2 + Φ ′ ( p ) + Φ ′ ( g ) − 2 ∗ Φ ( v ) , ( Φ ( v ) < Φ ( p ) ) < = 2 + 2 ∗ Φ ′ ( v ) − 2 − 2 ∗ Φ ( v ) , ( Φ ′ ( p ) + Φ ′ ( g ) < = 2 ∗ Φ ′ ( v ) − 2 ) = 2 ∗ [ Φ ′ ( v ) − Φ ( v ) ] \begin{align} A &= T + \Delta\Phi \\ &= 2 + \Delta\Phi(v) + \Delta\Phi(p) + \Delta\Phi(g) \\ &= 2 + \Phi '(v) - \Phi(v) + \Phi '(p) - \Phi(p) + \Phi '(g) - \Phi(g)\\ &= 2 + \Phi '(p) + \Phi '(g) - \Phi(v) - \Phi(p) ,(\Phi '(v) = \Phi(g)v伸展后的size和g初始size相同)\\ &<= 2 + \Phi '(p) + \Phi '(g) - 2*\Phi(v),(\Phi(v) < \Phi(p))\\ &<= 2 + 2*\Phi '(v) - 2 - 2*\Phi(v),(\Phi '(p) + \Phi '(g)<=2*\Phi '(v) - 2)\\ &=2*[\Phi'(v) - \Phi(v)] \end{align} A=T+ΔΦ=2+ΔΦ(v)+ΔΦ(p)+ΔΦ(g)=2+Φ(v)Φ(v)+Φ(p)Φ(p)+Φ(g)Φ(g)=2+Φ(p)+Φ(g)Φ(v)Φ(p)(Φ(v)=Φ(g)v伸展后的sizeg初始size相同)<=2+Φ(p)+Φ(g)2Φ(v)(Φ(v)<Φ(p))<=2+2Φ(v)22Φ(v)(Φ(p)+Φ(g)<=2Φ(v)2)=2[Φ(v)Φ(v)]
倒数第二行那里用了基本不等式,因为log为上凸函数,所以函数中值大于两点中值,(log2 a + log2 b) / 2<= log2 ((a + b) / 2)

log2 a + log2 b <= 2 * (log2 (a + b) / 2) = 2 * log2 (a + b) - 2 < 2 * log2 c - 2(c > a + b,也就对应v的size大于g,p的size之和)

情况三:zig-zig

和情况二证明类似,最后一步放缩即可
A = T + Δ Φ = 2 + Δ Φ ( v ) + Δ Φ ( p ) + Δ Φ ( g ) = 2 + Φ ′ ( v ) − Φ ( v ) + Φ ′ ( p ) − Φ ( p ) + Φ ′ ( g ) − Φ ( g ) = 2 + Φ ′ ( p ) + Φ ′ ( g ) − Φ ( v ) − Φ ( p ) , ( Φ ′ ( v ) = Φ ( g ) v 伸展后的 s i z e 和 g 初始 s i z e 相同 ) < = 2 + Φ ′ ( p ) + Φ ′ ( g ) − 2 ∗ Φ ( v ) , ( Φ ( v ) < Φ ( p ) ) < = 2 + Φ ′ ( g ) + Φ ′ ( v ) − 2 ∗ Φ ( v ) , ( Φ ′ ( p ) < Φ ′ ( v ) ) < = 3 ∗ [ Φ ′ ( v ) − Φ ( v ) ] , ( 最后一步放缩是用 Φ ′ ( g ) + Φ ( v ) < = 2 Φ ′ ( v ) − 2 ) \begin{align} A &= T + \Delta\Phi \\ &= 2 + \Delta\Phi(v) + \Delta\Phi(p) + \Delta\Phi(g) \\ &= 2 + \Phi '(v) - \Phi(v) + \Phi '(p) - \Phi(p) + \Phi '(g) - \Phi(g)\\ &= 2 + \Phi '(p) + \Phi '(g) - \Phi(v) - \Phi(p) ,(\Phi '(v) = \Phi(g)v伸展后的size和g初始size相同)\\ &<= 2 + \Phi '(p) + \Phi '(g) - 2*\Phi(v),(\Phi(v) < \Phi(p))\\ &<= 2 + \Phi '(g) + \Phi '(v) - 2*\Phi(v),(\Phi'(p) < \Phi'(v)) \\ &<=3*[\Phi'(v) - \Phi(v)],(最后一步放缩是用\Phi'(g) + \Phi(v) <= 2\Phi'(v)-2) \end{align} A=T+ΔΦ=2+ΔΦ(v)+ΔΦ(p)+ΔΦ(g)=2+Φ(v)Φ(v)+Φ(p)Φ(p)+Φ(g)Φ(g)=2+Φ(p)+Φ(g)Φ(v)Φ(p)(Φ(v)=Φ(g)v伸展后的sizeg初始size相同)<=2+Φ(p)+Φ(g)2Φ(v)(Φ(v)<Φ(p))<=2+Φ(g)+Φ(v)2Φ(v)(Φ(p)<Φ(v))<=3[Φ(v)Φ(v)],(最后一步放缩是用Φ(g)+Φ(v)<=2Φ(v)2)
综上,我们伸展操作的每一步至多需要3*[Φ’[v] - Φ[v]]的时间,那么我们某次操作中,把一个结点v伸展到根r的分摊时间复杂度

A <= 1+ 3 * [Φ® - Φ(v)] <= 1 + 3 * Φ® = O(1 + logn) = O(logn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/220566.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring 保姆级带你认识,让你如何轻松应对面试官

Spring 保姆级带你认识&#xff0c;让你如何轻松应对面试官 1.Spring是什么&#xff1f;作用是什么&#xff1f; Spring是一个轻量级的JavaEE框架&#xff0c;它主要解决企业应用中的复杂性问题。Spring框架有三个核心部分&#xff1a;IoC容器、AOP和数据访问/集成层。Spring…

java--抽象类

1.什么是抽象类 ①在java中有一个关键字叫&#xff1a;abstract&#xff0c;它就是抽象的意思&#xff0c;可以用它修饰类、成员方法。 ②abstract修饰类&#xff0c;这个类就是抽象类&#xff1b;修饰方法&#xff0c;这个方法就是抽象方法。 2.抽象类的注意事项、特点 ①抽…

Vue3-ElementPlus按需导入

1.安装 pnpm add element-plus 2.配置按需导入&#xff1a; 官方文档&#xff1a;快速开始 | Element Plus 按照官网按需导入中的自动导入步骤来进行 pnpm add -D unplugin-vue-components unplugin-auto-import 观察Vite代码与原vite文件的差别&#xff0c;将原vite文件中没…

[数据结构]-map和set

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、键值对…

【数据结构】二叉树的实现

目录 1. 前言2. 二叉树的实现2.1 创建一棵树2.2 前序遍历2.2.1 分析2.2.2 代码实现2.2.3 递归展开图 2.3 中序遍历2.3.1 分析2.3.2 代码实现2.3.3 递归展开图 2.4 后序遍历2.4.1 分析2.4.2 代码实现2.4.3 递归展开图 2.5 求节点个数2.5.1 分析2.5.2 代码实现 2.6 求叶子节点个数…

Linux 环境下的性能测试——top与stress

对于Linux 环境&#xff0c;top命令是使用频繁且信息较全的命令&#xff0c; 它对于所有正在运行的进行和系统负荷提供实时更新的概览信息。stress是个简单且全面的性能测试工具。通过它可以模拟各种高负载情况。 通过top与stress这两个命令的结合使用&#xff0c;基本可以达到…

解决:ModuleNotFoundError: No module named ‘exceptions’

解决&#xff1a;ModuleNotFoundError: No module named ‘exceptions’ 文章目录 解决&#xff1a;ModuleNotFoundError: No module named exceptions背景报错问题翻译&#xff1a;报错位置代码报错原因解决方法今天的分享就到此结束了 背景 在使用之前的代码时&#xff0c;报…

报表控件Stimulsoft 操作演示:访问编译的报告

使用编译计算模式的报告能够执行使用报告脚本语言实现的各种脚本。然而&#xff0c;这些场景并不总是安全的&#xff0c;从网络安全的角度来看&#xff0c;可能会导致负面情况。经过分析情况&#xff0c;我们决定加强有关编译模式报告的安全策略。但让我们一步一步来。顺便说一…

IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -Mybatis核心配置详解

第一章 Mybatis核心配置详解【mybatis-config.xml】 1.1 核心配置文件概述 MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 1.2 核心配置文件根标签 没有实际语义&#xff0c;主要作用&#xff1a;所有子标签均需要设置在跟标签内部 1.3 核心配置文件…

【数电笔记】17-具体函数的卡诺图填入

目录 说明&#xff1a; 用卡诺图表示逻辑函数 1. 基本步骤 2. 例题 2.1 例1-真值表转换卡诺图 2.2 例2-标准与或式画卡诺图 2.3 例3-非标准与或式画卡诺图&#xff08;常见,重点掌握&#xff09; 说明&#xff1a; 笔记配套视频来源&#xff1a;B站&#xff1b;本系列笔…

学生档案管理系统研究

摘 要 学生档案管理系统是一个教育单位不可缺少的部分,它的内容对于学校的决策者和管理者来说都至关重要,所以学生档案管理系统应该能够为用户提供充足的信息和快捷的查询手段。但一直以来人们使用传统人工的方式管理文件档案&#xff0c;这种管理方式存在着许多缺点,如:效率低…

gpt阅读论文利器

1. txyz.ai 读论文 严伯钧 3. consensus 两亿科学论文的资源库. 用英文. 中国经济发展, 美国加州没有,减肥没有. 2. chrome插件 gpt sidebar 3. gpt academic 论文润色和学术翻译 ,一键输出公式. 英语口语8000句. 托福备考计划表. 百词斩托福. 薄荷外刊. 分区笔记精读法.…

java--接口的其他细节

1.jdk8开始&#xff0c;接口新增了三种形式的方法 ①默认方法(实例方法)&#xff1a;使用用default修饰&#xff0c;默认会被加上public修饰。注意&#xff1a;只能使用接口的实现类对象调用 ②私有方法&#xff1a;必须用private修饰(jdk9开始才支持) ③类方法(静态方法)&a…

数字化车间|用可视化技术提升车间工作效率

数字化车间正在成为现代制造业的重要组成部分。随着科技的不断进步&#xff0c;传统的车间生产方式逐渐地被数字化和自动化取代。数字化车间将机器和软件进行整合&#xff0c;实现了生产过程的高效、精确和可追溯。在数字化车间中&#xff0c;机器之间可以进行无缝的通信和协作…

vue: 线上项目element-ui的icon偶尔乱码问题

线上环境偶尔会复现&#xff0c; 具体&#xff1a; 一般使用不会出现这个问题&#xff0c;因为一般引入的是element-ui的css文件&#xff0c;问题出在于为了主题色变化啊&#xff0c;需要用到scss变量引入了scss文件。 import “~element-ui/packages/theme-chalk/src/index”…

XCharts——Unity上最好用的免费开源图表插件!(一)基本介绍

只讲实用干货&#xff01;&#xff01;&#xff01;&#xff08;过于细节的或是未提及到的可直接问&#xff09; 目录 XCharts介绍 插件简介 插件下载 XCharts基本使用 类型介绍 1.折线图&#xff08;LineChart&#xff09; 2.柱形图&#xff08;BarChart&#xff09; …

Spring---事务

事务 学习了MySQL的朋友应该大致了解事务的含义。所谓事务就是一系列操作&#xff0c;要么都执行&#xff0c;要么都不执行。在spring中事务有两种形式&#xff1a;声明式事务和编程式事务。一般使用声明式事务&#xff0c;用的比较多的就是注解Transactional。如下图所示&…

深入理解指针3

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起继续深入学习指针&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 如果本篇文章对你有帮助&#xff0c;还请各位点点赞&#xff01;&#xff01;&#xff01; 话不多说&am…

Unity环境配置并解决visual studio 不能智能代码提示Unity代码问题(一)

1、请先安装好unity和Visual Studio 2019 2、Visual Studio需要安装如图&#xff08;2019才会有那个移动的可以勾选&#xff09; 3、Unity配置 file->build setting windows->package manager 安装如下图 edit->preferences 3、创建c#脚本 如果还是没能智能提…

计算机基础知识65

cookie和session的使用 # 概念&#xff1a;cookie 是客户端浏览器上的键值对 # 目的&#xff1a;为了做会话保持 # 来源&#xff1a;服务端写入的&#xff0c;服务端再返回的响应头中写入&#xff0c;浏览器会自动取出来 存起来是以key value 形式&#xff0c;有过期时间、path…