OJ中常用平衡树,Treap树堆详解

文章目录

    • Treap定义
    • Treap的可行性
    • Treap的构建
      • 节点定义
      • 旋转
        • 左单旋
        • 右单旋
        • 旋转的代码实现
      • 插入
        • 插入的代码实现
      • 删除
      • 遍历
      • 查找
      • Treap对权值的扩展
      • Treap对size的扩展
        • 扩展size域后的节点定义和旋转,插入,删除操作
        • 查询第k小的元素
        • 求元素的排名
      • 查询后继、前驱
      • Treap类的代码实现
      • Treap的特点
    • 无旋式Treap
      • 无旋式Treap 定义
      • 无旋式Treap 的特点
      • 无旋式Treap 的节点定义
      • 无旋式Treap 的操作
        • 分裂(split)
        • 合并(merge)
        • 插入(insert)
        • 删除(erase)

Treap定义

T r e a p = T r e e + H e a p Treap = Tree + Heap Treap=Tree+Heap

顾名思义,Treap其实就是树和堆的结合,其本质是一种平衡树。

我们最基础的BST在插入有序或接近有序的数据时,会退化成单链的结构,所以我们基于BST有着很多优化方案如AVL树引入平衡因子,红黑树引入颜色等等。

而我们的Treap引入堆其实就是为了维护平衡。其维护平衡的方法就是在BST的基础上加入修正值,修正值满足最小堆的性质(也可以是最大堆,本文基于最小堆实现),即根的修正值小于其子树的修正值。

Treap其实就是满足如下性质的二叉树:

  1. 如果左子树非空,那么左子树节点的值都小于根节点的值,并且根节点的修正值都小于左子树节点的修正值
  2. 如果右子树非空,那么右子树节点的值都小于根节点的值,并且根节点的修正值都小于右子树节点的修正值
  3. 它的左右子树也都是一棵Treap

下图例为一棵Treap

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

修正值的获取方法其实是一个随即生成的数字,没有规律。

Treap的可行性

前面提到了BST退化为单链是因为插入有序或者接近有序的数据,但我们修正值是一个随机数,获得有序随机数的概率很小,所以我们的Treap是趋于平衡并不是AVL树或者红黑树那种严格平衡,

Treap的构建

节点定义

struct TreapNode
{
    TreapNode *_left, *_right;//节点左右孩子指针
    int val, fix;//节点值以及修正值
    //int _weight;
};

旋转

我们常用的几种平衡树像是AVL树,RB树,为了维护其平衡性都要用到旋转操作,Treap也一样,但是Treap的只用到了左单旋和右单旋,调整比较简单,没有AVL树或是RB树那样繁琐的双旋。

我们记根节点为root,其左右孩子分别为SubL,SubR,左右孩子的左右孩子记为SubLL/SubLR和SubRL/SubRR

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

左单旋

左单旋一棵子树:SubRL作为root的右孩子,root作为SubR的左孩子
在这里插入图片描述

右单旋

右单旋一棵子树:SubLR作为root的左孩子,root作为SubL的右孩子

在这里插入图片描述

旋转操作的本质就是在不改变这棵树的中序遍历的前提下让这颗树的层数可能减小,不改变中序遍历自然维护了BST的性质,不过我们Treap中旋转还可以用来维护修正值的堆序。如:

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

旋转后既维护了BST的性质又修正了修正值的堆序。

旋转的代码实现
void RotateL(TreapNode *&root)//左单旋
{
    TreapNode *SubR = root->_right;
    root->_right = SubR->_left;
    SubR->_left = root;
    root = SubR;
}
void RotateR(TreapNode *&root)//右单旋
{
    TreapNode *SubL = root->_left;
    root->_left = SubL->_right;
    SubL->_right = root;
    root = SubL;
}

插入

Treap的插入也BST相似(这里的插入暂时允许重复元素插入)

找到插入位置,建立新的节点,如果修正值异常那么就旋转维护

插入过程如下:

  1. 从根节点开始遍历
  2. 如果插入值小于当前节点值,那么在左子树插入,插入完成后,如果当前节点修正值大于左孩子修正值,那么右单旋
  3. 如果插入值大于当前节点值,那么在右子树插入,插入完成后,如果当前节点修正值大于右孩子修正值,那么左单旋
  4. 当前节点为空,那么找到了插入位置,在该位置进行插入

以下图为例

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

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

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

插入的代码实现
void insert(TreapNode *&cur, int val)
{
    if (!cur)
    {
        cur = new TreapNode(val, rand());
    }
    else if (cur->val > val)
    {
        insert(cur->_left, val);
        if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点
            RotateR(cur);
    }
    else
    {
        insert(cur->_right, val);
        if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点
            RotateL(cur);
    }
}

删除

Treap中删除元素相对于AVL树和RB树简直是简单爆了,一共只有三种情况(其实是成两种)

情况一:被删除节点是叶子节点

没有孩子没有牵挂,生不带来死不带去,直接删除即可

情况二:被删除节点有一个孩子节点非空

子承父业,用孩子节点代替当前节点

情况三:两个孩子节点都非空

不同于BST的替代删除法,我们是通过旋转将待删除结点调整到叶子节点或者只有一个孩子节点非空的位置,然后按照情况一二进行删除。

旋转调整的过程类似于堆调整算法中的向下调整(AdjustDown)算法

如果左孩子修正值小于右孩子修正值,那么右单旋,否则左单旋。如此往下调整直到调整为情况一二。

我们以下图删除结点15为例

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

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

我们删除的期望时间复杂度为O(logN)

代码实现:

void erase(TreapNode *&root, int val)
{
    if (root->val > val)
    {
        erase(root->_left, val);
    }
    else if (root->val < val)
    {
        erase(root->_right, val);
    }
    else // 找到待删除节点
    {
        if (!root->_right || !root->_left)
        { // 情况一二,该节点可以直接被删除
            TreapNode *tmp = root;
            if (!root->_right)
                root = root->_left; // 用左子节点代替它
            else
                root = root->_right; // 用右子节点代替它
            delete tmp;              // 删除该节点
        }
        else
        { // 情况三
            if (root->_left->fix < root->_right->fix)
            { // 左子节点修正值较小,右旋
                RotateR(root);
                erase(root->_right, val);
            }
            else
            { // 左子节点修正值较小,左旋
                RotateL(root);
                erase(root->_left, val);
            }
        }
    }
}

遍历

和普通的二叉树遍历方式一样

    void _InOrder(TreapNode *root)
    {
        if (!root)
            return;
        _InOrder(root->_left);
        cout << root->val << ":" << root->fix << " ";
        _InOrder(root->_right);
    }
    void _PrevOrder(TreapNode *root)
    {
        if (!root)
            return;
        cout << root->val << ":" << root->fix << " ";
        _PrevOrder(root->_left);
        _PrevOrder(root->_right);
    }

查找

和BST一样

    bool Find(int val)
    {
        TreapNode *cur = p;
        while (cur)
        {
            if (cur->val > val)
                cur = cur->_left;
            else if (cur->val < val)
                cur = cur->_right;
            else
                return true;
        }
        return false;
    }

Treap对权值的扩展

我们的Treap其实还存在一个问题,那就是它允许了重复元素的插入,对此我们可以再进一步的优化,那就是把重复的节点合并,即在节点定义里面增加一个权域weight,它存储的是当前值节点的数目

这样我们插入时如果当前值已存在,那么把对应节点的weight+1即可,相应的,删除时只要把对应节点的weight-1即可,只有当weight为0才真正删除。

虽然增加了节点的大小,但是这样避免了一定程度的旋转,是一笔划算的买卖。

扩展了weight域后的节点定义和插入删除操作

struct TreapNode
{
    TreapNode(TreapNode *l, TreapNode *r, int v, int f) : _left(l), _right(r), val(v), fix(f), w(1)
    {
    }
    TreapNode *_left, *_right;
    int val, fix, w;
};
    void insert(TreapNode *&cur, int val)
    {
        if (!cur)
        {
            cur = new TreapNode(nullptr, nullptr, val, rand());
        }
        else if (cur->val > val)
        {
            insert(cur->_left, val);
            if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点
                RotateR(cur);
        }
        else if (cur->val < val)
        {
            insert(cur->_right, val);
            if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点
                RotateL(cur);
        }
        else//已经存在,w++
        {
            cur->w++;
        }
    }
    void erase(TreapNode *&root, int val)
    {
        if (root->val > val)
        {
            erase(root->_left, val);
        }
        else if (root->val < val)
        {
            erase(root->_right, val);
        }
        else // 找到待删除节点
        {
            if (!(--root->w))//如果w为0,在真正删除
            {
                if (!root->_right || !root->_left)
                { // 情况一二,该节点可以直接被删除
                    TreapNode *tmp = root;
                    if (!root->_right)
                        root = root->_left; // 用左子节点代替它
                    else
                        root = root->_right; // 用右子节点代替它
                    delete tmp;              // 删除该节点
                }
                else
                { // 情况三
                    if (root->_left->fix < root->_right->fix)
                    { // 左子节点修正值较小,右旋
                        RotateR(root);
                        erase(root->_right, val);
                    }
                    else
                    { // 左子节点修正值较小,左旋
                        RotateL(root);
                        erase(root->_left, val);
                    }
                }
            }
        }
    }

Treap对size的扩展

除了对权域weight的扩展外,我们还可以再增加一个size域,size存储的是以当前节点为根的子树的大小。(我们称一棵子树中节点的数目为子树的大小)

为什么要记录当前子树大小呢?

这是为了满足对于第k大元素的查询。

在root所在子树里面找第k大节点,可以根据左右子树的size转化为在左/右子树中找第j大节点

增加了域自然要在相应的操作中对域进行维护。这里涉及三种操作旋转、插入、删除。

旋转:在旋转后对子节点和根节点分别重新计算其子树大小。

插入:新插入节点的size自然是1,对于插入路径上的每一个节点相应的size+1即可。

删除:对于待删除节点,对于删除路径上的每一个节点相应的size-1即可。

对于插入和删除的size维护都比较无脑,主要是保证旋转的维护逻辑无误。

扩展size域后的节点定义和旋转,插入,删除操作
struct TreapNode
{
    TreapNode(TreapNode *l, TreapNode *r, int v, int f) : _left(l), _right(r), val(v), fix(f), w(1), size(1)
    {
    }
    int lsize()//左子树size
    {
        return _left ? _left->size : 0;
    }
    int rsize()//右子树size
    {
        return _right ? _right->size : 0;
    }
    TreapNode *_left, *_right;
    int val, fix, w, size;
};
    void erase(TreapNode *&root, int val)
    {
        if (root->val > val)
        {
            erase(root->_left, val);
            root->size--;
        }
        else if (root->val < val)
        {
            erase(root->_right, val);
            root->size--;
        }
        else // 找到待删除节点
        {
            if (!(--root->w))
            {
                if (!root->_right || !root->_left)
                { // 情况一二,该节点可以直接被删除
                    TreapNode *tmp = root;
                    if (!root->_right)
                        root = root->_left; // 用左子节点代替它
                    else
                        root = root->_right; // 用右子节点代替它
                    delete tmp;              // 删除该节点
                }
                else
                { // 情况三
                    if (root->_left->fix < root->_right->fix)
                    { // 左子节点修正值较小,右旋
                        RotateR(root);
                        erase(root->_right, val);
                    }
                    else
                    { // 左子节点修正值较小,左旋
                        RotateL(root);
                        erase(root->_left, val);
                    }
                }
            }
            else
                root->size--;
        }
    }
   void RotateL(TreapNode *&root)
    {
        TreapNode *SubR = root->_right;
        root->_right = SubR->_left;
        SubR->_left = root;
        root = SubR;
        // 此时root为新根,显然要先调节子节点,再调节root
        SubR = root->_left; // 命名有点迷惑性了,但是节省空间就这样了
        SubR->size = SubR->lsize() + SubR->rsize() + SubR->w;
        root->size = root->lsize() + root->rsize() + root->w;
    }
    void RotateR(TreapNode *&root)
    {
        TreapNode *SubL = root->_left;
        root->_left = SubL->_right;
        SubL->_right = root;
        root = SubL;
        // 此时root为新根,显然要先调节子节点,再调节root
        SubL = root->_right; // 命名有点迷惑性了,但是节省空间就这样了
        SubL->size = SubL->lsize() + SubL->rsize() + SubL->w;
        root->size = root->lsize() + root->rsize() + root->w;
    }
    void insert(TreapNode *&cur, int val)
    {
        if (!cur)
        {
            cur = new TreapNode(nullptr, nullptr, val, rand());
        }
        else if (cur->val > val)
        {
            insert(cur->_left, val);
            cur->size++;
            if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点
                RotateR(cur);
        }
        else if (cur->val < val)
        {
            insert(cur->_right, val);
            cur->size++;
            if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点
                RotateL(cur);
        }
        else
        {
            cur->size++;
            cur->w++;
        }
    }
查询第k小的元素

有了size域之后,我们访问第k小元素就很方便了,其实就是一个简单的分治问题。

对于一个节点root,它在当前子树内的排名显然是[lsize + 1 , lsize + w]
当k在区间左侧,说明要到左子树去找第k小元素
当k在区间右侧,说明要到右子树去找第k - lsize - w小元素
否则当前节点对应的值就是第k小元素

这里也能看出来我们对于weight域的扩展其实是很有必要的~

  1. 从节点root开始访问、
  2. 如果root.lsize + 1<= k <= root.lsize + w,则返回当前节点对应值
  3. 如果k < root.lsize + 1,则分治到左子树查找第k小
  4. 如果k > root.lsize + w,则分治到右子树查找第k - root.lsize - w小

代码如下:

    T kth(TreapNode *root, int k)
    {
        if (!root)
        {
            assert(false);
        }
        if (k < root->lsize() + 1)
        {
            return kth(root->_left, k);
        }
        else if (k > root->lsize() + root->w)
        {
            return kth(root->_right, k - root->lsize() - root->w);
        }
        else
        {
            return root->val;
        }
    }
求元素的排名

既然能够查询第k小的元素,自然能够查询元素的排名,同样是利用分治的思想。

对于一个元素val,它的元素排名的贡献来自于值比它小的节点个数,而对于这个个数我们是可以在查找的路径上实现的。

过程为:

  1. 当前访问节点为root,路径前面的节点对于排名的贡献总和为prev,初始从根节点开始查询,prev初始为0

  2. 如果root的值为val,那么它的排名区间为[lsize + prev + 1 , lsize + prev + w] (说是区间是因为val的节点有w个)

  3. 如果root的值大于val,那么就分治到左子树,prev向下传递

  4. 如果root的值小于val,那么就分治到右子树,prev + lsize + w向下传递

代码如下:

    int getKth(TreapNode *root, const T &val, int prev)
    {
        if (!root)
        {
            assert(false);
        }
        if (root->val > val)
        {
            return getKth(root->_left, val, prev);
        }
        else if (root->val < val)
        {
            return getKth(root->_right, val, prev + root->lsize() + root->w);
        }
        else
        {
            return root->lsize() + prev + 1;
        }
    }

查询后继、前驱

很常规的一个操作,查前驱就一直记录小于val的节点,同时访问右孩子

查后继就一直记录大于val的节点,同时访问左孩子

遇到空就return

    TreapNode *ans;
    void prev(const T &val)
    {
        prev(p, val);
    }
    void sub(const T &val)
    {
        sub(p, val);
    }
    void prev(TreapNode *root, const T &val)
    {
        if (!root)
            return;
        if (root->val < val)
            ans = root, prev(root->_right, val);
        else
            prev(root->_left, val);
    }

    void sub(TreapNode *root, const T &val)
    {
        if (!root)
            return;
        if (root->val > val)
            ans = root, sub(root->_left, val);
        else
            sub(root->_right, val);
    }

Treap类的代码实现

这里给出Treap的完整代码

template <class T>
struct TreapNode
{
    TreapNode(const T &v, int f) : _left(nullptr), _right(nullptr), val(v), fix(f), w(1), size(1)
    {
    }
    int lsize()
    {
        return _left ? _left->size : 0;
    }
    int rsize()
    {
        return _right ? _right->size : 0;
    }
    TreapNode *_left, *_right;
    T val;
    int fix, w, size;
};
template <class T>
class Treap
{
private:
    typedef TreapNode<T> TreapNode;
    TreapNode *p = nullptr;

public:
    void insert(const T &val)
    {
        insert(p, val);
    }
    bool Find(const T &val)
    {
        TreapNode *cur = p;
        while (cur)
        {
            if (cur->val > val)
                cur = cur->_left;
            else if (cur->val < val)
                cur = cur->_right;
            else
                return true;
        }
        return false;
    }
    void erase(const T &val)
    {
        erase(p, val);
    }

    void InOrder()
    {
        _InOrder(p);
    }
    void PrevOrder()
    {
        _PrevOrder(p);
    }
    T kth(int k)
    {

        return kth(p, k);
    }
    int getKth(const T &val)
    {
        return getKth(p, val, 0);
    }

private:
    int getKth(TreapNode *root, const T &val, int prev)
    {
        if (!root)
        {
            assert(false);
        }
        if (root->val > val)
        {
            return getKth(root->_left, val, prev);
        }
        else if (root->val < val)
        {
            return getKth(root->_right, val, prev + root->lsize() + root->w);
        }
        else
        {
            return root->lsize() + prev + 1;
        }
    }
    T kth(TreapNode *root, int k)
    {
        if (!root)
        {
            assert(false);
        }
        if (k < root->lsize() + 1)
        {
            return kth(root->_left, k);
        }
        else if (k > root->lsize() + root->w)
        {
            return kth(root->_right, k - root->lsize() - root->w);
        }
        else
        {
            return root->val;
        }
    }
    void erase(TreapNode *&root, const T &val)
    {
        if (root->val > val)
        {
            erase(root->_left, val);
            root->size--;
        }
        else if (root->val < val)
        {
            erase(root->_right, val);
            root->size--;
        }
        else // 找到待删除节点
        {
            if (!(--root->w))
            {
                if (!root->_right || !root->_left)
                { // 情况一二,该节点可以直接被删除
                    TreapNode *tmp = root;
                    if (!root->_right)
                        root = root->_left; // 用左子节点代替它
                    else
                        root = root->_right; // 用右子节点代替它
                    delete tmp;              // 删除该节点
                }
                else
                { // 情况三
                    if (root->_left->fix < root->_right->fix)
                    { // 左子节点修正值较小,右旋
                        RotateR(root);
                        erase(root->_right, val);
                    }
                    else
                    { // 左子节点修正值较小,左旋
                        RotateL(root);
                        erase(root->_left, val);
                    }
                }
            }
            else
                root->size--;
        }
    }
    void _InOrder(TreapNode *root)
    {
        if (!root)
            return;
        _InOrder(root->_left);
        cout << root->val << ":" << root->fix << ":" << root->size << " ";
        _InOrder(root->_right);
    }
    void _PrevOrder(TreapNode *root)
    {
        if (!root)
            return;
        cout << root->val << ":" << root->fix << ":" << root->size << " ";
        _PrevOrder(root->_left);
        _PrevOrder(root->_right);
    }
    void RotateL(TreapNode *&root)
    {
        TreapNode *SubR = root->_right;
        root->_right = SubR->_left;
        SubR->_left = root;
        root = SubR;
        // 此时root为新根,显然要先调节子节点,再调节root
        SubR = root->_left; // 命名有点迷惑性了,但是节省空间就这样了
        SubR->size = SubR->lsize() + SubR->rsize() + SubR->w;
        root->size = root->lsize() + root->rsize() + root->w;
    }
    void RotateR(TreapNode *&root)
    {
        TreapNode *SubL = root->_left;
        root->_left = SubL->_right;
        SubL->_right = root;
        root = SubL;
        // 此时root为新根,显然要先调节子节点,再调节root
        SubL = root->_right; // 命名有点迷惑性了,但是节省空间就这样了
        SubL->size = SubL->lsize() + SubL->rsize() + SubL->w;
        root->size = root->lsize() + root->rsize() + root->w;
    }
    void insert(TreapNode *&cur, const T &val)
    {
        if (!cur)
        {
            cur = new TreapNode(val, rand());
        }
        else if (cur->val > val)
        {
            insert(cur->_left, val);
            cur->size++;
            if (cur->fix > cur->_left->fix) // 左孩子修正值小于当前节点修正值,右旋当前节点
                RotateR(cur);
        }
        else if (cur->val < val)
        {
            insert(cur->_right, val);
            cur->size++;
            if (cur->fix > cur->_right->fix) // 右孩子修正值小于当前节点修正值,左旋当前节点
                RotateL(cur);
        }
        else
        {
            cur->size++;
            cur->w++;
        }
    }
};

Treap的特点

  • 期望深度为O(logn)有着严格的数学证明
  • 实现简单,实际OJ中的代码编写十分简洁。
  • 原理简单,只有两种单旋,而且只需维护两种基础数据结构的规则
  • 稳定性佳,虽然不是严格平衡,但是其简单的代码加上期望平衡性能够很好的应用于算法题目中

无旋式Treap

无旋式Treap 定义

无旋式Treap,即无旋转操作的Treap,不同于我们其他的平衡树,他独有的调整操作是:分裂、合并。也正是其操作方式的特性使其具有支持维护序列和可持久化等特性。因此我们可以用封装的无旋式Treap实现类似C++STL中set的效果。

无旋式Treap 的特点

优点:支持可持久化,操作种类多(支持区间操作)

缺点:比众多平衡树效率低,且相比于旋转式Treap也要慢一些。

无旋式Treap 的节点定义

无旋式Treap 的节点定义和有旋式Treap一样

template <class T>
struct TreapNode
{
    TreapNode(const T &v, int f) : _left(nullptr), _right(nullptr), val(v), fix(f), w(1), size(1)
    {
    }
    TreapNode *_left, *_right;
    T val;
    int fix, w, size;
};

无旋式Treap 的操作

分裂(split)

Treap的分裂有两种:按权值分裂和按排名分裂。思想一样,会一种另一种也就会了。这里介绍按权值分裂。

按照权值分裂

将指定根节点root分裂为两个Treap,第一个Treap所有节点的权值小于等于给定val值,第二个Treap所有节点的权值大于给定val值。(注意前者是小于等于,后者是严格大于)

    using PII = pair<TreapNode* , TreapNode*>;
    PII split(TreapNode *root, const T &val)
    {
    \\...
    }

操作流程

  1. 判断root所指向的节点是否为空;为空就直接返回nullptr对

  2. 将当前root节点所指向的值与给定的key值进行比较:

    • 如果root → val > val ,则说明root所指向的节点及其右子树全部属于第二个Treap,同时向左子树继续分裂;

    • 如果root → val <= val,则说明root所指向的节点及其左子树全部属于第一个Treap,同时向右子树继续分裂。

  3. 将root和子树递归分裂后得到的两个子树中的一个连接,和另一个子树组成pair返回

  4. root分裂完要更新size值(update)

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

    代码如下:

    /*
    void update(TreapNode *root)
    {
        if (!root)
            return;
        root->size = root->rsize() + root->lsize() + 1;
    }
   */
    PII split(TreapNode *root, const T &val)
    {
        if (!root)
            return make_pair(nullptr, nullptr);
        if (root->val > val)
        {
            PII lp = split(root->_left, val);
            root->_left = lp.second;
            update(root);
            return make_pair(lp.first, root);
        }
        else
        {
            PII rp = split(root->_right, val);
            root->_right = rp.first;
            update(root);
            return make_pair(root, rp.second);
        }
    }
合并(merge)

有分裂自然有合并。由于我们合并一定是为了修补分裂,而分裂是由权值划分为了两个子树,所以一定满足一个子树的val全都小于另一个子树。

所以我们的merge的两个参数为两个根节点指针u,v,要求u树的val全都小于v树的val

然后按照u和v的修正值进行合并,u->fix < v->fix的话,把u的右子树和v合并,否则把v的左子树和u合并

每次合并完都要更新size值(update操作)

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

TreapNode *merge(TreapNode *u, TreapNode *v) // u全都小于v
    {
        if (!u)
            return v;
        if (!v)
            return u;
        if (u->fix < v->fix)
        {
            u->_right = merge(u->_right, v);
            update(u);

            return u;
        }
        else
        {
            v->_left = merge(u, v->_left);
            update(v);

            return v;
        }
    }
插入(insert)

从插入和删除就能看出无旋式Treap的优越之处了,代码十分简单。

先按照val分裂,再把新结点和左子树合并,再合并两个子树

    void insert(const T &val)
    {
        PII pp = split(p, val);
        if (pp.first && pp.first->val == val)
            pp.first->w++, pp.first->size++;
        else
            pp.first = merge(pp.first, new TreapNode(val, rand()));
        p = merge(pp.first, pp.second);
    }
删除(erase)

先按照val分成三段,中间的一段就是val,把两边的两段合并即可

    void erase(const T &val)
    {
        PII oo = split(p, val - 1);
        PII pp = split(p, val);
        if (pp.first && pp.first->val == val)
            delete pp.first;
        p = merge(oo.first, pp.second);
    }

查询区间排名等操作与旋转式Treap一样

完整代码

#include <iostream>
#include <string>
#include <algorithm>
#include <cassert>
#include <vector>
using namespace std;
#define int long long
template <class T>
struct TreapNode
{
	TreapNode(const T& v, int f) : _left(nullptr), _right(nullptr), val(v), fix(f), w(1), size(1)
	{
	}
	int lsize()
	{
		return _left ? _left->size : 0;
	}
	int rsize()
	{
		return _right ? _right->size : 0;
	}
	TreapNode* _left, * _right;
	T val;
	int fix, w, size;
};
template <class T>
class Treap
{

private:
	typedef TreapNode<T> Node;
	Node* p = nullptr;
	using PII = pair<Node*, Node*>;

public:
	Node* ans;
	void prev(const T& val)
	{
		prev(p, val);
	}
	void sub(const T& val)
	{
		sub(p, val);
	}
	void insert(const T& val)
	{
		PII pp = split(p, val);
		if (pp.first && pp.first->val == val)
			pp.first->w++, pp.first->size++;
		else
			pp.first = merge(pp.first, new Node(val, rand()));
		p = merge(pp.first, pp.second);
	}
	void erase(const T& val)
	{
		PII oo = split(p, val - 1);
		PII pp = split(p, val);
		if (pp.first && pp.first->val == val)
			delete pp.first;
		p = merge(oo.first, pp.second);
	}
	bool Find(const T& val)
	{
		Node* cur = p;
		while (cur)
		{
			if (cur->val > val)
				cur = cur->_left;
			else if (cur->val < val)
				cur = cur->_right;
			else
				return true;
		}
		return false;
	}

	void InOrder()
	{
		_InOrder(p);
	}
	void PrevOrder()
	{
		_PrevOrder(p);
	}
	T kth(int k)
	{
		return kth(p, k);
	}
	int getKth(const T& val)
	{
		PII pp = split(p, val - 1);
		int ret = (pp.first ? pp.first->size : 0) + 1;
		p = merge(pp.first, pp.second);
		return ret;
	}

private:
	void update(Node* root)
	{
		if (!root)
			return;
		root->size = root->rsize() + root->lsize() + 1;
	}
	PII split(Node* root, const T& val)
	{
		if (!root)
			return make_pair(nullptr, nullptr);
		if (root->val > val)
		{
			PII lp = split(root->_left, val);
			root->_left = lp.second;
			update(root);
			return make_pair(lp.first, root);
		}
		else
		{
			PII rp = split(root->_right, val);
			root->_right = rp.first;
			update(root);
			return make_pair(root, rp.second);
		}
	}
	Node* merge(Node* u, Node* v) // u全都小于v
	{
		if (!u)
			return v;
		if (!v)
			return u;
		if (u->fix < v->fix)
		{
			u->_right = merge(u->_right, v);
			update(u);

			return u;
		}
		else
		{
			v->_left = merge(u, v->_left);
			update(v);

			return v;
		}
	}
	void prev(Node* root, const T& val)
	{
		if (!root)
			return;
		if (root->val < val)
			ans = root, prev(root->_right, val);
		else
			prev(root->_left, val);
	}

	void sub(Node* root, const T& val)
	{
		if (!root)
			return;
		if (root->val > val)
			ans = root, sub(root->_left, val);
		else
			sub(root->_right, val);
	}

	T kth(Node* root, int k)
	{
		if (root->lsize() + 1 == k)
			return root->val;
		else if (root->lsize() + 1 > k)
		{
			return kth(root->_left, k);
		}
		else
		{
			return kth(root->_right, k - root->lsize() - root->w);
		}
	}

	void _InOrder(Node* root)
	{
		if (!root)
			return;
		_InOrder(root->_left);
		cout << root->val << ":" << root->fix << ":" << root->size << " ";
		_InOrder(root->_right);
	}
	void _PrevOrder(Node* root)
	{
		if (!root)
			return;
		cout << root->val << ":" << root->fix << ":" << root->size << " ";
		_PrevOrder(root->_left);
		_PrevOrder(root->_right);
	}
};

OJ练习

P3369 【模板】普通平衡树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这里直接用静态版无旋Treap

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long

int root, idx, lc[100010] = {0}, rc[100010] = {0}, sz[100010] = {0}, rnd[100010] = {0}, key[100010] = {0};

int newnode(int val)
{
    idx++;
    key[idx] = val, rnd[idx] = rand(), sz[idx] = 1;
    return idx;
}

void update(int x)
{
    sz[x] = sz[lc[x]] + sz[rc[x]] + 1;
}

void split(int cur, int k, int &l, int &r) // l树权值小于等于k r大于k
{
    if (!cur)
        l = r = 0;
    else
    {
        if (key[cur] <= k)
        {
            l = cur;
            split(rc[cur], k, rc[cur], r);
        }
        else
        {
            r = cur;
            split(lc[cur], k, l, lc[cur]);
        }
        update(cur);
    }
}

int merge(int l, int r)
{
    if (!l || !r)
        return l + r;
    if (rnd[l] < rnd[r])
    {
        rc[l] = merge(rc[l], r);
        update(l);
        return l;
    }
    else
    {
        lc[r] = merge(l, lc[r]);
        update(r);
        return r;
    }
}

void insert(int k)
{
    int l, r;
    split(root, k, l, r);
    root = merge(merge(l, newnode(k)), r);
}

void erase(int k)
{
    int x, y, z;
    split(root, k, x, z);
    split(x, k - 1, x, y);
    y = merge(lc[y], rc[y]);
    root = merge(merge(x, y), z);
}

int kth(int k) // 排名
{
    int l, r;
    split(root, k - 1, l, r);
    int ret = sz[l] + 1;
    root = merge(l, r);
    return ret;
}

int getkth(int p, int k)
{
    if (sz[lc[p]] + 1 == k)
        return key[p];
    else if (sz[lc[p]] + 1 > k)
    {
        return getkth(lc[p], k);
    }
    else
    {
        return getkth(rc[p], k - sz[lc[p]] - 1);
    }
}
int ans;
void prev(int p, int val)
{
    if (!p)
        return;
    if (key[p] < val)
        ans = p, prev(rc[p], val);
    else
        prev(lc[p], val);
}

void sub(int p, int val)
{
    if (!p)
        return;
    if (key[p] > val)
        ans = p, sub(lc[p], val);
    else
        sub(rc[p], val);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    srand(time(0));
    int n;
    cin >> n;
    root = idx = 0;
    while (n--)
    {
        int s, x;
        cin >> s >> x;

        if (s == 1)
            insert(x);
        else if (s == 2)
            erase(x);
        else if (s == 3)
            cout << kth(x) << '\n';
        else if (s == 4)
            cout << getkth(root, x) << '\n';
        else if (s == 5)
        {
            prev(root, x);
            cout << key[ans] << '\n';
        }
        else
        {
            sub(root, x);
            cout << key[ans] << '\n';
        }
    }
    return 0;
}

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

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

相关文章

虚幻引擎:RPC:远端调用

1.如何区当前是服务器还是在客服端 2.如何修改一个actor的所有权 修改所有权必须 在服务器上进行修改,不允许在客户端进行修改

大数据商城人流数据分析与可视化 - python 大数据分析 计算机竞赛

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于大数据的基站数据分析与可视化 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度…

220v插座led指示灯维修

由于220v是交流电&#xff0c;有反向电压的情况&#xff0c;而led反向通电的时候电阻无穷大&#xff0c;所以分压也无穷大&#xff0c;220v一导通就击穿&#xff0c;即使加了很大的电阻也没用&#xff0c;串联电阻只能作用于二极管正向的时候。 目前有两种方案&#xff1a; 方…

远程运维用什么软件?可以保障更安全?

远程运维顾名思义就是通过远程的方式IT设备等运行、维护。远程运维适用场景包含因疫情居家办公&#xff0c;包含放假期间出现运维故障远程解决&#xff0c;包含项目太远需要远程操作等等。但远程运维过程存在一定风险&#xff0c;安全性无法保障&#xff0c;所以一定要选择靠谱…

【深度学习】pytorch——神经网络工具箱nn

笔记为自我总结整理的学习笔记&#xff0c;若有错误欢迎指出哟~ 深度学习专栏链接&#xff1a; http://t.csdnimg.cn/dscW7 pytorch——神经网络工具箱nn 简介nn.Modulenn.Module实现全连接层nn.Module实现多层感知机 常用神经网络层图像相关层卷积层&#xff08;Conv&#xff…

MPLAB X IDE 仿真打断点提示已中断的断点?

这种中间带裂缝的是无效断点。 原因可能与XC编译器的优化有关&#xff0c;最后生成的汇编与C语言并不是一一对应的(官方给的解释是效率高)。所以这一行C语言转换的汇编代码可能并不在这个位置&#xff0c;也可能与其它汇编合并后根本就没有 我的解决方法是把优化等级调到最低&a…

MapReduce:大数据处理的范式

一、介绍 在当今的数字时代&#xff0c;生成和收集的数据量正以前所未有的速度增长。这种数据的爆炸式增长催生了大数据领域&#xff0c;传统的数据处理方法往往不足。MapReduce是一个编程模型和相关框架&#xff0c;已成为应对大数据处理挑战的强大解决方案。本文探讨了MapRed…

wpf添加Halcon的窗口控件报错:下列控件已成功添加到工具箱中,但未在活动设计器中启用

报错截图如下&#xff1a; 注意一下新建工程的时候选择wpf应用而不是wpf应用程序。 添加成功的控件&#xff1a;

python 之 正则表达式模块re

文章目录 findall例子&#xff1a;特点和注意事项&#xff1a; match示例&#xff1a;match 对象的方法和属性&#xff1a;注意事项&#xff1a; search示例&#xff1a;match 对象的方法和属性&#xff1a;注意事项&#xff1a; split示例&#xff1a;参数说明&#xff1a;注意…

尚硅谷大数据项目《在线教育之实时数仓》笔记006

视频地址&#xff1a;尚硅谷大数据项目《在线教育之实时数仓》_哔哩哔哩_bilibili 目录 第9章 数仓开发之DWD层 P041 P042 P043 P044 P045 P046 P047 P048 P049 P050 P051 P052 第9章 数仓开发之DWD层 P041 9.3 流量域用户跳出事务事实表 P042 DwdTrafficUserJum…

初步利用Ansible实现批量服务器自动化管理

1.Ansible介绍 Ansible是一款开源的自动化运维工具, 在2012年由Michael DeHaan创建, 现在由Red Hat维护。Ansible是基于Python开发的,采用YAML语言编写自动化脚本playbook, 可以在Linux、Unix等系统上运行, 通过SSH协议管理节点, 无需在被管理节点安装agent。Ansible以其简单、…

【计算机网络 - 自顶向下方法】第一章习题答案

P2 Question&#xff1a;   式 (1-1) 给出了经传输速率为 R 的 N 段链路发送长度为 L 的一个分组的端到端时延。 对于经过 N 段链路一个接一个地发送 P 个这样的分组&#xff0c;一般化地表示出这个公式。 Answer&#xff1a;    N ∗ L R \frac{N*L}{R} RN∗L​时&#x…

Amazon MSK 基于 S3 的数据导出、导入、备份、还原、迁移方案

Amazon MSK&#xff08;Amazon Managed Streaming for Apache Kafka&#xff09;是 Amazon 云平台提供的托管 Kafka 服务。在系统升级或迁移时&#xff0c;用户常常需要将一个 Amazon MSK 集群中的数据导出&#xff08;备份&#xff09;&#xff0c;然后在新集群或另一个集群中…

04、SpringBoot + 微信支付 --- 内网穿透ngrok(安装、使用)

Native 下单 1、内网穿透 ngrok 1-1&#xff1a;注册下载 下载 2-2&#xff1a;使用方式 直接在该目录cmd打开 第一次时候这个ngrok时&#xff0c;需要为计算机做授权 授权命令&#xff1a; ngrok config add-authtoken 2XmL8EfYQe6uVAjM9Iami0pWogd_5ztKmSxHs6UeAQn9RQB…

python 之异常处理结构

文章目录 常见的异常处理表现形式1. SyntaxError2. NameError3. TypeError4. IndexError5. KeyError6. ZeroDivisionError7. FileNotFoundErrortry……except …… 结构1. try 块2. except 块示例&#xff1a;多个except块try……except ……else 结构结构说明&#xff1a;示例…

AVS3:双向梯度修正BGC

双向梯度修正&#xff08;Bi-directional Gradient Correction&#xff0c;BGC&#xff09;是利用双向参考块间的差值对预测值进行修正的技术。 BGC仅用于双向预测CU&#xff0c;设两个方向得到的单向预测值分别为pred0和pred1&#xff0c;修正前的双向预测值为predBI&#xf…

Elasticsearch:搜索架构

Elasticsearch 全文检索的复杂性 为了理解为什么全文搜索是一个很难解决的问题&#xff0c;让我们想一个例子。 假设你正在托管一个博客发布网站&#xff0c;其中包含数亿甚至数十亿的博客文章&#xff0c;每个博客文章包含数百个单词&#xff0c;类似于 CSDN。 执行全文搜索…

win版redis详细安装教程

一、下载 github下载地址 https://github.com/MicrosoftArchive/redis/releases 可选择&#xff1a;下载msi包或zip压缩包 这里我选择的是zip压缩包&#xff0c;直接通过cmd命令窗口操作即可。 二、安装步骤 1、解压Redis压缩包 选中压缩包&#xff0c;右键选择解压&#…

【计算机网络】数据链路层-MAC和ARP协议

文章目录 1. 认识以太网2. MAC协议MAC帧的格式MAC地址和IP地址的区别MTU 3. 局域网通信原理碰撞检测和避免 4. ARP协议ARP数据报的格式ARP缓存 1. 认识以太网 网络层解决的是跨网络点到点传输的问题&#xff0c;数据链路层解决的是同一网络中的通信。 数据链路层负责在同一局域…

ARMday01(计算机理论、ARM理论)

计算机理论 计算机组成 输入设备、输出设备、运算器、控制器、存储器 1.输入设备&#xff1a;将编写好的软件代码以及相关的数据输送到计算机中&#xff0c;转换成计算机能够识别、处理和存储的数据形式 键盘、鼠标、手柄、扫描仪、 2.输出设备&#xff1a;将计算机处理好的数…