treap(tree+heap就是二叉搜索树+堆)

1、Treap的定义
Treap是一棵二叉搜索树,只是每个节点多了一个优先级fix,对于每个节点,该节点的优先级小于等于其所有孩子的优先级。当然,引入优先级fix的目的就是防止退化成一条链,从而影响查找效率。
所以,这样看来就是:Treap中对于节点的关键字key来说,它是一棵二叉搜索树,而对于fix来说,它是一个最小堆,所以Treap可以看成是Tree+Heap,只是这里的Heap不一定是完全二叉树。
Treap的平均时间复杂度为log(n).
如下图: treap(tree+heap就是二叉搜索树+堆)_第1张图片
修正值是节点在插入到TreapTreap中时随机生成的一个值,它与节点的值无关。下述代码给出了TreapTreap的一般定义。
struct Treap_Node {
Treap_Node *left, *right; //节点的左右子树的指针
int value, fix; //节点的值和修正值
};

基本操作:插入x数、删除x数、查询x数的排名、查询排名为x的数、求x的前驱、求x的后继。
treap同时满足两个性质的方法,首先令其满足二叉排序树性质;通过左旋或者右旋,令其同时满足堆的性质。
左旋操作
treap(tree+heap就是二叉搜索树+堆)_第2张图片
右旋操作
treap(tree+heap就是二叉搜索树+堆)_第3张图片
左旋、右旋代码

void Treap_Left_Rotate(Treap_Node *&a) {//左旋 节点指针一定要传递引用
    Treap_Node *b = a->right;
    a->right = b->left;
    b->left = a;
    a = b;
}
void Treap_Right_Rotate(Treap_Node *&a) {//右旋 节点指针一定要传递引用
    Treap_Node *b = a->left;
    a->left = b->right;
    b->right = a;
    a = b;
}

初学者对左旋、右旋总记不住,其实很简单,就以右旋举例,左儿子B转到父A的位置,父A当然就成了B的右子树了,A以前是B的父节点,A比B大,旋转后为了保持BST结构,肯定A是B的右子树了,如果B以前左右子树都有,再加上A成为右子树,这样B就有三个子树了,破坏了BST结构,只能把B的子树给A一个,当然只能给B以前的右子树了。
Treap的插入操作
给节点随机分配一个优先级,先和二叉排序树(又叫二叉搜索树)的插入一样,按key插入到相应地方,然后再按优先级fix维护堆的性质。
插入写成递归形式的话,只需要在递归调用完成后判断是否满足堆性质,如果不满足就旋转,实现相对简单。其插入过程示例图如下:
在已知的Treap插入值为4的元素
找到插入的位置后,随机生成的修正值为15
treap(tree+heap就是二叉搜索树+堆)_第4张图片
新建的节点与他的父节点3不满足堆序3为根的子树左旋,如图9
treap(tree+heap就是二叉搜索树+堆)_第5张图片
节点4与其父节点5仍不满足最小堆序,对以节点55为根的子树右旋,如图10
treap(tree+heap就是二叉搜索树+堆)_第6张图片
至此,节点4与其父亲2满足堆序,调整结束。

时间复杂度:
由于旋转是O(1)的,最多进行h次(h是树的高度),插入的复杂度是O(h)的,在期望情况下h=O(log n),所以它的期望复杂度是O(log n)。

Treap_Node *root;
void Treap_Insert(Treap_Node *&P, int value) {//节点指针一定要传递引用
    if (!P) {//找到位置,建立节点
        P = new Treap_Node;
        P->value = value;
        P->fix=rand();//生成随机的修正值
    }
    else if (value <= P->value) {
        Treap_Insert(P->left, value);
        if (P->left->fix < P->fix)
        Treap_Right_Rotate(P);//左子节点修正值小于当前节点修正值,右旋当前节点
    }
    else {
        Treap_Insert(P->right, value);
        if (P->right->fix < P->fix)
        Treap_Left_Rotate(P);//右子节点修正值小于当前节点修正值,左旋当前节点
    }
}
int main() {
    Treap_Insert(root, 7);//在 Treap 中插入值为 7 的元素
    return 0;
}

Treap的删除操作
与BST一样,在Treap中删除元素要考虑多种情况
我们可以按照在BST中删除元素同样的方法来删除Treap中的元素
即用它的后继(或前驱)节点的值代替它,然后删除它的后继(或前驱)节点。
为了不使Treap向一边偏沉,我们需要随机地选取是用后继还是前驱代替它,并保证两种选择的概率均等。
上述方法期望时间复杂度为O(logN)。
但是上述方法并没有充分利用Treap随机性质,每次删除一个节点,就破坏了fix堆的结构,我们左旋或右旋操作后,就不一定用后继(或前驱)替换删除的节点。

我们给出一种更为通用的删除方法,这种方法是基于旋转调整的
首先要在Treap树中找到待删除节点的位置,然后分情况讨论:
情况一,该节点为叶节点或链节点,则该节点是可以直接删除的节点,若该节点有非空子节点,用非空子节点代替该节点的,否则用空节点代替该节点,然后删除该节点。
情况二,该节点有两个非空子节点
我们的策略是通过旋转,使该节点变为可以直接删除的节点。如果该节点的左子节点的修正值小于右子节点的修正值,右旋该节点,使该节点降为右子树的根节点,然后访问右子树的根节点,继续讨论;反之,左旋该节点,使该节点降为左子树根节点,然后访问左子树的根节点,继续讨论,知道变成可以直接删除的节点。

下面给一个删除例子:在Treap中删除是6的元素
如图11
先在Treap中找到6的位置
treap(tree+heap就是二叉搜索树+堆)_第7张图片
发现节点6有两个子节点,且左子节点的修正值小于右子节点的修正值,需要右旋节点6,如图12。(记住往大的那边转,右给右)
treap(tree+heap就是二叉搜索树+堆)_第8张图片

旋转后,节点6仍有两个节点,右子节点修正值较小,于是左旋节点6,如图 13。(往大的那边转)
treap(tree+heap就是二叉搜索树+堆)_第9张图片
此时,节点6只有一个子节点,可以直接删除,用它的左子节点代替它,删除本身,如图14。我们发现这次是用他的前驱代替
treap(tree+heap就是二叉搜索树+堆)_第10张图片
我们发现在删除的过程中根本就不需要用到删除节点的父节点,只是删除节点和左右子树打交道,不像BST的删除用到父节点。

删除的复杂度稍高,但是期望时间仍为O(logN)
但是在程序中更容易实现
下述代码给出了后一种(即上述图例中)的删除方法
在给定的Treap中删除值为6的节点。

BST_Node *root;
void Treap_Delete(Treap_Node *&P, int value) {//节点指针要传递引用
    if (value == P->value) {//找到要删除的节点 对其删除
        if (!P->right or !P->left) {//情况一,该节点可以直接被删除
            Treap_Node *t = P;
            if (!P->right) P = P->left; //用左子节点代替它
            else P = P->right; //用右子节点代替它
            delete t; //删除该节点
        }
        else {//情况二
            if (P->left->fix < P->right->fix) {//左子节点修正值较小,右旋
                Treap_Right_Rotate(P);
                Treap_Delete(P->right, value);
            }
            else {//左子节点修正值较小,左旋
                Treap_Left_Rotate(P);
                Treap_Delete(P->left, value);
            }
        }
    }
    else if (value < P->value) Treap_Delete(P->left, value); //在左子树查找要删除的节点
    else Treap_Delete(P->right, value); //在右子树查找要删除的节点
}
int main() {
    Treap_Delete(root, 6);//在Treap中删除值为6的元素
    return 0;
}

Treap的特点

1、
Treap简明易懂

Treap只有两种调整方式,左旋和右旋
,而且即使没有严密的数学证明和分析,Treap的构造方法、平衡原理也是不难理解的,
只要能够理解BST和堆的思想,理解Treap就不在话下

2、Treap易于编写,Treap只需维护一个满足堆序的修正值,修正值一经生成无需修改
,相比较其他各种平衡树,Treap拥有最少的调整方式,仅仅两种相互对称的旋转
所以Treap当之无愧是最易于编码调试的一种平衡树。
3、Treap稳定性佳

Treap的平衡性虽不如AVL、红黑树、SBT等平衡树
,但是Treap也不会退化,可以保证期望O(logN)的深度,Treap的稳定性取决于随机数发生器

4、Treap具有严密的数学证明

Treap期望O(logN的深度,是有严密的数学证明的
但这不是本文介绍的重点,略去。
5、Treap具有良好的实践效果
各种实际应用中,Treap的稳定性表现得相当出色,没有因为任何的构造出的数据而退化,
于是在信息学竞赛中,不少选手习惯于使用Treap,均取得了不俗的表现。

你可能感兴趣的:(数据结构)