Treap

  1. 参考

    1. 算法训练营第五章

  2.  

    1. 名称:Treap(Tree+heap,又叫做树堆)。

    2. 本质:同时满足二叉搜索树和堆两种数据结构的性质。

    3. 一些abstract:弥补了二叉搜索树输入时因为退化成一条链导致复杂度爆炸的弱点。如果以随机的顺序构造一个二叉搜索树,则期望是平衡的,即左右高度不会差很多。如果我们提前已知所有的输入值,随机打乱插入是可以的,但很多时候我们只能知道当前的值,那就需要别的指标作为堆插入的指标,而这个指标是随机数,即每一个节点包括两个属性,一个属性是用作搜索树的,是输入的值;另一个是用作堆的,是随机出来的,用来平衡的。

  3. 一些操作:

    1. 旋转:分为左旋(zag,逆时针)和右旋(zig,顺时针)。

      右旋如图:

       

      看图写代码(这里看起来没有修改父亲的信息,但是注意,每次旋转都是在插入或者删除操作中,调用的时候都是Insert(tr[p].lc)这种操作,然后再旋转,而旋转之后因为最后的赋值操作是对指针操作的,所以我们其实已经自动改变了tr[p].lc的值,所以不用担心父子关系错乱的事情~)

       void zig(int &p) {
         int tr[p].lc;
         tr[p].lc tr[q].rc;
         tr[q].rc p;
         tr[q].size tr[p].size;
         update(p);  // 见后
         q;  // q变为根
       }

      左旋如图:

       

      看图写代码:

       void zag(int &p) {
         int tr[p].rc;
         tr[p].rc tr[q].lc;
         tr[q].lc p;
         update(p);  // 见后
         q;
       }
    2. 插入:先根据有序性找到插入的位置,然后创建新节点,插入此值,然后随机一个数,当作优先级,自底向上检查是否满足堆性质,不满足就左旋或者右旋,使其满足堆的性质。

      算法步骤:

       

      所以Treap根本不会判断左右子树的高度差才决定是否旋转,而是一开始就选择相信这个玄学优先级。

      插入之后,沿着插入的路径回去看优先级大小,如果不满足堆,注意因为是沿着路径往回找,而此时矛盾的点就仅在于上面的p和q的优先级,也就是说q相当于新插入的点,递归回来到了这个位置,p是原来的点。而p的优先级显然大于除了q之外所有的点的优先级,因为只有q是新插入的点。然后我们发现旋转之后,新子树优先级满足大根堆的性质,也满足查找树的性质。

       int createNode(int val) {  // 创建新节点
         tr[++cnt].val val;
         tr[cnt].pri rand();
         tr[cnt].lc tr[cnt].rc 0;
         tr[cnt].count tr[cnt].size 1;  // 有1次,子树大小为1
         return cnt;
       }
       
       void update(int p) {
         tr[p].size tr[tr[p].lc].size tr[tr[p].rc].size tr[p].count;
       }
       
       void Insert(int &p, int val) {  // 在p的子树中插入val
         if (!p) {  // 空树
           createNode(val);
           return;
        }
         tr[p].size++;
         if (val == tr[p].val) {
           tr[p].count++;
           return;
        } else if (val tr[p].val) {
           Insert(tr[p].lc, val);
           if (tr[p].pri tr[tr[p].lc].pri) {  // 递归到这里,看和左儿子的优先级是否满足大根堆
             zig(p);  // 和左儿子的矛盾肯定要右旋
          }
        } else {
           Insert(tr[p].rc, val);
           if (tr[p].pri tr[tr[p].rc].pri) {  // 递归到这里,看和右儿子的优先级是否满足大根堆
             zag(p);  // 和右儿子的矛盾肯定要左旋
          }
        }
       }
    3. 删除:找到被删除的节点,将该节点向优先级较大的子节点旋转,递归到旋转到的位置执行删除,一直旋转到叶子,然后删除叶子的位置就行了。

       

      我们将这个树视为很平衡的树,所以旋转的次数大概是O(logn)(层数)的。

      板子:

       void Delete(int &p, int val) {  // 在p的子树中删除val
       if (!p) return;
         tr[p]--;
         if (val == tr[p].val) {
           if (tr[p].count 1) {
             tr[p].count 1;
             return;
          }
           if (!tr[p].lc || !tr[p].rc) {
             tr[p].lc tr[p].rc;  // 至少有一个为空,直接让不为空(有一个为空)的或者空(两个全是空)的儿子代替p。
          } else if (tr[tr[p].lc].pri tr[tr[p].rc].pri) {  // 左边的儿子上来,所以是右旋
             zig(p);
             // 右旋之后,p由于是指针,已经改变,原来的点是现在的p的右儿子
             Delete(tr[p].rc);
          } else {
             zag(p);
             // 同理
             Delete(tr[p].lc);
          }
        }
         else if (val tr[p].val) {
           // 小于显然要往左找
           Delete(tr[p].lc, val);
        } else {
           Delete(tr[p].rc, val);
        }
       }
    4. 前驱:

      从树根开始找,如果当前节点值小于val,则暂存这个节点的值,说明可以尝试变得更大,尝试去右子树找答案(暂存的原因是防止右子树的值都大于val,这样值得提前准备好);否则当前的值比较大,需要在左子树里找。

      板子:

       int GetPre(int val) {  // 找val在这棵树中的前驱
         int root;
         int res -inf;
         while(p) {
           if (tr[p].val val) {
             res tr[p].val;
             tr[p].rc;
          } else {
             tr[p].lc;
          }
        }
         return res;
       }
    5. 后继:

      显然和前驱正好反过来

      板子:

       int GetNxt(int val) {  // 找val在这棵树中的后继
         int root;
         int res inf;
         while(p) {
           if (tr[p].val val) {
             res tr[p].val;
             tr[p].lc;
          } else {
             tr[p].rc;
          }
        }
         return res;
       }
    6. 查找val在这棵树中的排名:

      显然是根据val和当前节点的大小,一直往下搜。

      板子:

       int GetRankByVal(int p, int val) {
         if (!p) return 0;
         if (tr[p].val == val) {  // 显然这个节点只算一个,而不是count个
           return tr[tr[p].lc].size 1;
        }
         else if (val tr[p].val) {
           return GetRankByVal(tr[p].lc, val);
        } else {
           return GetRankByVal(tr[p].rc, val) tr[tr[p].lc].size tr[p].count;  // 这个count就得都算了,因为已经到右子树了
        }
       }
    7. 拿到排名为rank的点的val

      板子:

       int GetValByRank(int p, int rank) {
         if (!p) return 0;
         if (rank tr[p].size) return inf;
         if (tr[tr[p].lc].size >= rank) {  // 在左节点就被截下了
           return GetValByRank(tr[p].lc, rank);
        } 
         if (tr[tr[p].lc].size tr[p].count >= rank) {  // 在当前节点被截下
           return tr[p].val;
        }
         return GetValByRank(tr[p].rc, rank tr[p].counttr[tr[p].lc].size);
       }

       

posted on 2022-05-04 20:58  小染子  阅读(207)  评论(0编辑  收藏  举报