天天看点

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

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

定义

二叉查找树( )或者是一棵空树,或者是具有下列性质的二叉树:

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

上述性质被称为性质

可以看出,二叉查找树是递归定义的

如图是一个二叉查找树。

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

下述代码给出了一个节点的定义。

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

遍历

对于一个已知的二叉查找树,从小到大输出其节点的值,只需对其进行二叉树的中序遍历,即递归地先输出其左子树,再输出其本身,然后输出其右子树

遍历的时间复杂度为

下述代码为把一个以为根的所有节点的值从小到大输出到标准输出流。

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. 如果当前节点为空节点,查找失败,二叉查找树中没有要查找的值。

查找的期望时间复杂度为

下述代码为在一个已知的二叉查找树中查找值。

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. 如果当前节点为空节点,在此建立新的节点,该节点的值为要插入的值,左右子树为空,插入成功。

对于相同的元素,一种方法我们规定把它插入左边,另一种方法是我们在节点上再加一个域,记录重复节点的个数

上述方法为前者

插入的期望时间复杂度为。

下述代码为在一个二叉查找树中插入值。

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. 情况二,该节点是链节点(只有一个非空子节点的节点),为了删除这个节点而不影响它的子树,需要把它的子节点代替它的位置,然后把它删除。如图所示,删除节点时,需要把它的左子节点代替它的位置。
  3. 二叉查找树——清华大学计算机系 郭家宝
  4. 情况三,该节点有两个非空子节点。由于情况比较复杂,一般的策略是用它右子树的最小值来代替它,然后把它删除。如图所示,删除节点时,在它的右子树中找到最小的节点,该节点一定为待删除节点的后继。删除节点(它可能是叶节点或者链节点),然后把节点的值改为。
  5. 二叉查找树——清华大学计算机系 郭家宝

实际上在实现的时候,情况一和情况二是可以一样对待的,因为用叶节点的子节点代替它本身,就是用空节点代替了它,等同于删除

对待情况三除了可以用后继代替本身以外,我们还可以使用它的前驱(左子树的最大值)代替它本身

这就牵涉到了查找最小值的问题,基本方法就是从子树的根节点开始,如果左子节点不为空,那么就访问左子节点,直到左子节点为空,当前节点就是该子树的最小值节点,删除它只需用它的右子节点代替它本身。

下述代码是在一个给定的二叉查找树中删除值。

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;
}      

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

二叉查找树的效果究竟如何?

事实上,随机的进行次插入和删除之后,二叉查找树会趋向于向左偏沉

为什么会出现这种情况,原因是在于删除时,我们总是选择将待删除节点的后继代替它本身

这样就会造成总是右边的节点数目减少,以至于树向左偏沉

已经被证明,随机插入或删除次以后,树的期望深度为

我们可以在删除时随机地选择用前驱还是后继代替本身,以消除向一边偏沉的问题

这种神奇地做法消除了偏沉的不平衡,效果十分明显

对待随机的数据二叉查找树已经做得很不错了,但是如果有像这样有序的数据插入树中时,会有什么后果出现?

如图

二叉查找树退化成为了一条链

这个时候,无论是查找、插入还是删除,都退化成了的时间

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

事实上,在实际的应用中,这种情况会经常出现,而简单的二叉查找树却没有办法变得更快

我们需要使二叉查找树变得尽量平衡,才能保证各种操作在的期望时间内完成,于是各种自动平衡二叉查找树( )因而诞生

在常见的自动平衡二叉查找树(以下简称平衡树)中,有近乎完美平衡的树( & ),红黑树( )和( )等,以及期望平衡的伸展树( )和( & )等数据结构。

继续阅读