二叉查找树——清华大学计算机系 郭家宝

版权声明:欢迎转载 https://blog.csdn.net/yandaoqiusheng/article/details/85012327

二叉查找树的定义、遍历与查找

定义

二叉查找树( B i n a r y Binary S e a r c h Search T r e e Tree )或者是一棵空树,或者是具有下列性质的二叉树:

  1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 它的左、右子树也分别为二叉查找树。

上述性质被称为 B S T BST 性质
可以看出,二叉查找树是递归定义的
如图 1 1 是一个二叉查找树。
在这里插入图片描述
下述代码给出了一个 B S T BST 节点的定义。

struct BST_Node {
    BST_Node *left, *right;//节点的左右子树的指针
    int value; //节点的值
};
遍历

对于一个已知的二叉查找树,从小到大输出其节点的值,只需对其进行二叉树的中序遍历,即递归地先输出其左子树,再输出其本身,然后输出其右子树
遍历的时间复杂度为 Θ ( N ) Θ(N)
下述代码为把一个以 r o o t root 为根的 B S T BST 所有节点的值从小到大输出到标准输出流。

BST_Node *root;
void BST_Print(BST_Node *P) {
    if (P) {
        BST_Print(P->left);
        printf(%d\n”, P->value);
        BST_Print(P->right);
    }
}
int main() {
    BST_Print(root);
    return 0;
}
查找

对于一个已知的二叉查找树,在其中查找特定的值,方法如下。

  1. 从根节点开始查找;
  2. 如果当前节点的值就是要查找的值,查找成功;
  3. 如果要查找的值小于当前节点的值,在当前节点的左子树中查找该值;
  4. 如果要查找的值大于当前节点的值,在当前节点的右子树中查找该值;
  5. 如果当前节点为空节点,查找失败,二叉查找树中没有要查找的值。

查找的期望时间复杂度为 Θ ( l o g N ) Θ(logN)
下述代码为在一个已知的二叉查找树中查找值 5 5

BST_Node *root;
BST_Node *BST_Find(BST_Node *P, int value) {
    if (!P) return 0; //查找失败
    if (P->value == value) return P; //查找成功
    else if (value < P->value) return BST_Find(P->left, value); //在左子树中查找
    else return BST_Find(P->right, value); //在右子树中查找
}
int main() {
    BST_Node *result;
    result = BST_Find(root, 5);//在BST中查找值为5的节点
    if (result) printf("Found!");
    else printf("Not Found!");
    return 0;
}

二叉查找树的插入与删除

插入

在二叉查找树中插入元素,要建立在查找的基础上
基本方法是类似于线性表中的二分查找,不断地在树中缩小范围定位,最终找到一个合适的位置插入
具体方法如下所述。

  1. 从根节点开始插入;
  2. 如果要插入的值小于等于当前节点的值,在当前节点的左子树中插入;
  3. 如果要插入的值大于当前节点的值,在当前节点的右子树中插入;
  4. 如果当前节点为空节点,在此建立新的节点,该节点的值为要插入的值,左右子树为空,插入成功。

对于相同的元素,一种方法我们规定把它插入左边,另一种方法是我们在节点上再加一个域,记录重复节点的个数
上述方法为前者
插入的期望时间复杂度为 Θ ( l o g N ) Θ(logN)
下述代码为在一个二叉查找树中插入值 5 5

BST_Node *root;
BST_Node *BST_Insert(BST_Node *&P,int value) {//节点指针要传递引用
    if (!P) {
        P = new BST_Node; //插入成功
        P->value = value;
    }
    else if (value <= value) return BST_Insert(P->left, value); //在左子树中插入
    else return BST_Insert(P->right, value); //在右子树中插入
}
int main() {
    BST_Insert(root, 5);//在 BST 中插入值 5
    return 0;
}
删除

二叉查找树的删除稍有些复杂,要分三种情况分别讨论
基本方法是要先在二叉查找树中找到要删除的节点的位置,然后根据节点分以下情况:

  1. 情况一,该节点是叶节点(没有非空子节点的节点),直接把节点删除即可。
  2. 情况二,该节点是链节点(只有一个非空子节点的节点),为了删除这个节点而不影响它的子树,需要把它的子节点代替它的位置,然后把它删除。如图 2 2 所示,删除节点 2 2 时,需要把它的左子节点代替它的位置。
    在这里插入图片描述
  3. 情况三,该节点有两个非空子节点。由于情况比较复杂,一般的策略是用它右子树的最小值来代替它,然后把它删除。如图 3 3 所示,删除节点 2 2 时,在它的右子树中找到最小的节点 3 3 ,该节点一定为待删除节点的后继。删除节点 3 3 (它可能是叶节点或者链节点),然后把节点 2 2 的值改为 3 3
    在这里插入图片描述

实际上在实现的时候,情况一和情况二是可以一样对待的,因为用叶节点的子节点代替它本身,就是用空节点代替了它,等同于删除
对待情况三除了可以用后继代替本身以外,我们还可以使用它的前驱(左子树的最大值)代替它本身
这就牵涉到了查找最小值的问题,基本方法就是从子树的根节点开始,如果左子节点不为空,那么就访问左子节点,直到左子节点为空,当前节点就是该子树的最小值节点,删除它只需用它的右子节点代替它本身。
下述代码是在一个给定的二叉查找树中删除值 2 2

BST_Node *root;
int Find_DeleteMin(BST_Node *&P) {//返回子树的最小值并删除 节点指针要传递引用
    if (!P->left) {//如果已经没有左子节点,删除它
        BST_Node *t = P;
        int r = P->value;
        P = P->right;
        delete t;
        return r;
    }
    else return Find_DeleteMin(P->left); //如果还有左子节点,访问左子节点
}
void BST_Delete(BST_Node *&P, int value)//节点指针要传递引用
{
    if (!P) return ; //查找失败,BST 中不存在要删除的节点
    if (P->value == value) {//查找成功,对其删除
        if (P->left and P->right) P->value = Find_DeleteMin(P->right);
        else {
            BST_Node *t = P;
            if (P->left) P = P->left;
            else P = P->right;
            delete t;
        }
    }
    else if (value < P->value) BST_Delete(P->left, value); //在左子树中查找
    else BST_Delete(P->right, value); //在右子树中查找
}
int main() {
    BST_Delete(root, 2);//在 BST 中删除值 2
    return 0;
}

二叉查找树的平衡性问题讨论

二叉查找树的效果究竟如何?
事实上,随机的进行 N 2 ( N &gt; = 1000 ) N^2(N&gt;=1000) 次插入和删除之后,二叉查找树会趋向于向左偏沉
为什么会出现这种情况,原因是在于删除时,我们总是选择将待删除节点的后继代替它本身
这样就会造成总是右边的节点数目减少,以至于树向左偏沉
已经被证明,随机插入或删除 N 2 N^2 次以后,树的期望深度为 Θ ( N 1 2 ) Θ(N^{\frac{1}{2}})
我们可以在删除时随机地选择用前驱还是后继代替本身,以消除向一边偏沉的问题
这种神奇地做法消除了偏沉的不平衡,效果十分明显
对待随机的数据二叉查找树已经做得很不错了,但是如果有像这样 6 , 5 , 4 , 3 , 2 , 1 6,5,4,3,2,1 有序的数据插入树中时,会有什么后果出现?
如图 4 4
二叉查找树退化成为了一条链
这个时候,无论是查找、插入还是删除,都退化成了 Θ ( N ) Θ(N) 的时间
在这里插入图片描述
事实上,在实际的应用中,这种情况会经常出现,而简单的二叉查找树却没有办法变得更快
我们需要使二叉查找树变得尽量平衡,才能保证各种操作在 Θ ( l o g N ) Θ(logN) 的期望时间内完成,于是各种自动平衡二叉查找树 S e l f B a l a n c i n g Self-Balancing B i n a r y Binary S e a r c h Search T r e e Tree )因而诞生
在常见的自动平衡二叉查找树(以下简称平衡树)中,有近乎完美平衡的 A V L AVL 树( A d e l s o n V e l s k i i Adelson-Velskii & L a n d i s Landis T r e e Tree ),红黑树( R e d Red B l a c k Black T r e e Tree )和 S B T SBT S i z e Size B a l a n c e d Balanced T r e e Tree )等,以及期望平衡的伸展树( S p l a y Splay T r e e Tree )和 T r e a p Treap T r e e Tree & H e a p Heap )等数据结构。
T r e a p Treap 具有可观的平衡性,且最易于编码调试等特点,因此在信息学竞赛中被广泛地使用。
相应的,这里还有关于 T r e a p Treap 的讲解,有兴趣的同学可以来这里看一下——传送门

猜你喜欢

转载自blog.csdn.net/yandaoqiusheng/article/details/85012327
今日推荐