引入

基本性质:

二叉排序树(又叫二叉搜索、查找树) 是一种特殊的二叉树,定义如下:

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 左、右子树也分别为二叉排序树。
  4. 不允许有键值相同结点。【如果真的出现了,那么放在左子树,右子树是无所谓的】

二分查找与二叉排序树

​ 二分查找也称为折半查找,要求原线性表有序,它是一种效率很高的查找方法。如果在需要进行频繁修改的表中采用二分查找,其效率也是非常低下的,因为顺序表的修改操作效率低。如果只考虑频繁的修改,我们可以采用链表。然而,链表的查找效率又非常低。综合这两种数据结构的优势,二叉查找树(Binary Sort/Search Tree)登场了。

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

给定一串数\((65,32,87,46,71,98,39)\),使用插入构造法,步骤如下:

数据结构篇——二叉排序(查找,搜索)树 算法 第1张

二叉排序树的基本操作

结构体定义

typedef struct BTNode
{
    char data;
    struct BTNode* Left;
    struct BTNode* Right;
}*BTree;

typedef BTNode BSTNode, *BSTree;

查找节点

如果是普通二叉树的查找操作,因为无法确定二叉树的具体特性,因此只能对其左右子树都进行遍历。但二叉排序树的特性,则会有一条确定的路线。

//找不到返回NULL,找到返回该节点。
//非递归
BSTNode* BSTreeFind(BSTree t, int x) {
    if (!t)return NULL;
    if (t->data == x) return t;
    if (x < t->data) return BSTreeFind(t->Left, x);
    if (x > t->data) return BSTreeFind(t->Right, x);
}
//非递归
BSTNode* BSTFind(BSTree T,int x) {
    BSTree p = T;
    while (p) {
        if (x == p->data)
            return p;
        p = x > p->data ? p->Right : p->Left;
    }
    return NULL;
}

插入节点

对于一颗二叉排序树来说,如果查找某个元素成功,说明该结点存在;如果查找失败,则查找失败的地方一定就是该结点应该插入的地方。

BSTree InsertBStree(BSTree BST, int x) {
    if (BST == NULL) {
        BST = new BSTNode;
        BST->data = x;
        return BST;
    }
    if (x < BST->data)
        BST->Left = InsertBStree(BST->Left, x);
    else
        BST->Right = InsertBStree(BST->Right, x);
    return BST;
}

二叉排序树的建立

建立一颗二叉排序树,就是先后插入\(n\)个结点的过程,与一般二叉树的建立是完全一样的。

BSTree BuildBSTree(int* a, int length) {
    BSTree BST = NULL;
    for (int i = 0; i < length; i++)
        BST = InsertBStree(BST, a[i]);
    return BST;
}

二叉排序树的删除

​ 二叉查找树的删除操作一般有两种常见做法,复杂度都是\(O(h)\)\(O(log(n))\),其中 \(h\) 为树的高度,\(n\) 为结点个数。

​ 如下图的二叉排序树,如果要删除结点 \(5\),则有两种办法,一种办法是以树中比 \(5\) 小的最大结点(结点 \(4\) )覆盖结点 \(5\) ,另一种是用树中比 \(5\) 大的最小结点(结点 \(6\) )覆盖结点 \(5\)

数据结构篇——二叉排序(查找,搜索)树 算法 第2张

​ 在二叉排序树中把比该结点权值小的最大结点称为该结点的前驱,把比该结点权值大的最小结点称为该结点的后继。结点的前驱是左子树的最右结点,结点的后继是右子树的最左结点。用下面两个函数用来寻找以root为根的树中最大、最小权值的结点,后面会使用到。

//获得以root为根结点的树中的最大值结点
int GetMax(BSTree root) {
    while (root->Right != NULL)
        root = root->Right;
    return root->data;
}

//获得以root为根结点的树中的最小值
int GetMin(BSTree root) {
    while (root->Left != NULL) {
        root = root->Left;
    }
    return root->data;
}

假设决定使用结点\(N\)的前驱 \(P\) 来替换 \(N\) ,于是就把问题转换为,先用 \(P\) 的值去覆盖 \(N\) 的权值,再删除结点\(P\),于是删除操作的实现如下:(写法1好理解,写法2更简洁)

写法1:

使用 BSTree root,如果 root 为要删除的结点, \(P\) 为父节点,删除情况有以下3种:

  1. root 结点是叶子,将 \(P\) 节点的 leftright 指针域置为NULL

  2. root 结点只有左子树,将 root 节点的 left 重新到结点 \(P\) 上。和删除单链表节点类似。(只有右子树时情况相似。第一种情况,写的时候可以和第二种合并起来)

  3. root 结点既有左子树,也有右子树,寻找结点的前驱 pre(或者后继),用 pre 的值去覆盖 root 的权值,再删除结点 pre​,而这个 pre 的删除一定属于情况 1 或者 2 。

​ 这个写法更适合JAVA,代码里的那些 delete 还可以省去,但是必须要注意的是:函数的返回值不是多余的,删除只有一个(或没有)子树的根节点的时候,必须用到这个返回值。

实现方法1:

//删除结点左子树的最大值结点
BSTree DelMax(BSTree root) {
    if (root->Right == NULL) {
        BSTNode *t= root->Left;
        delete root;
        return t;
    }
    root->Right = DelMax(root->Right);
    return root;
}

BSTree BSTDel(BSTree root,int x) {
    if (root == NULL) 
        return NULL;
    if (root->data == x) {
        //情况1和2,被删除的结点只有左子树或右子树,或没有子树
        if (! root->Right || !root->Left) {
            BSTNode *t = !root->Right ? root->Left : root->Right;
            delete root;
            return t;
        }
        //删除既有左子树,也有右子树的结点
        else{
            root->data = GetMax(root->Left);
            root->Left = DelMax(root->Left);
        }
    }
    else if (x > root->data) 
        root->Right = BSTDel(root->Right, x);
    else  
        root->Left = BSTDel(root->Left, x);
    return root;
}

实现方法2:

//删除子树中的最大值
void DelMax(BSTree &root,int &value) {
    if (!root)return;
    if (root->Right == NULL) {
        BSTNode *t= root;
        value = root->data;
        root = root->Left;
        delete t;
    }
    else
        DelMax(root->Right, value);
}

void BSTDel(BSTree &root,int x) {
    if (root == NULL) 
        return;
    BSTree p = root;
    if (p->data == x) {
        if (! p->Right || !p->Left) {
            root = !p->Right ? p->Left : p->Right;
            delete p;
        }
        else
            //因为使用的方法是值替换,并没有把原来的root覆盖掉,所以delete p不能像和下面方法2一样写在整个大if里
            DelMax(root->Left, root->data);
    }
    else if (x > p->data) BSTDel(p->Right, x);
    else  BSTDel(p->Left, x);
}

写法2:

使用 BSTree *root,和上面的BSTree &root是等价的,如果 root 为要删除的结点,删除情况也是以下3种:

  1. 当前 root 结点是叶子,将 root 置空。
  2. root 结点只有左子树,将 root 置为 root->Right(只有右子树时情况相似。第一种情况,写的时候可以和第二种合并起来)
  3. 被删除的结点既有左子树,也有右子树,寻找结点的前驱 pre(或者后继),用 root 的指针域覆盖 pre 的指针域,再把结点 root 删除。(形象化来说就是删除 root,把 pre 移动到 root 的位置)

void BSTDel(BSTree *root, int x){
    if (!*root) return; 
    BSTree p = *root;
    if (p->data == x) {
        if (!p->Right || !p->Left)
            *root = !p->Right ? p->Left : p->Right;
        else{
                BSTNode *parent = p->Left, *q = p->Left;
                if (!q->Right) q->Right = p->Right;
                else {
                    while (q->Right) {
                        parent = q;
                        q = q->Right;
                    }
                    parent->Right = q->Left;
                    q->Left = p->Left;
                    q->Right = p->Right;
                }
                *root = q;
        }
        delete p;
    }
    else if (x > p->data) //向右找
        BSTDel(&(p->Right), x);
    else if (x < p->data) //向左找
        BSTDel(&(p->Left), x);
}

二叉排序树的性质

二叉排序树最基本的性质:二叉排序树的中序序列是有序的。

如果合理调整二叉排序树的形态,使得树上的每个结点都尽量有两个子结点,这样整个二叉树的高度就会大约在\(log(n)\) 左右,其中 \(n\) 为结点个数。实现这个要求的一种树就是下一篇平衡二叉树(AVL Tree)。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄