《算法笔记》9.4 二叉查找树(BST)

9.4 二叉查找树(BST)

9.4.1 二叉查找树的定义

二叉查找树是一种特殊的二叉树,又称为排序二叉树、二叉搜索树、二叉排序树。二叉查找树的递归定义:

  • 要么二叉查找树是一颗空树
  • 要么二叉查找树由根结点、左子树和右子树组成,其中左子树和右子树都是二叉查找树,且左子树上所有结点的数据域均小于根结点数据域,右子树上所有节点数据域均大于根结点数据域。

9.4.2 二叉查找树基本操作

1.查找

查找将会是从树根到查找结点的一条路径,故最坏复杂度是O(h),其中h是二叉查找树的高度。

  • 如果当前根节点root为NULL , 说明查找失败,返回
  • 如果需要查找的值x等于当前节点数据域root->data,说明查找成功,访问之。
  • 如果需要查找的值x小于当前结点数据域root->data,说明应该往左子树查找,因此向root->lchild递归。
  • 如果需要查找的值x大于当前结点数据域root->data ,说明应该往右子树查找,因此向root->rchild递归。
void search( Node* root , int x ){
    if( root == NULL ){     //空树查找失败
        return ;
    }
    
    if( root->data == x ){   //查找成功,访问之
        cout << root->data << endl ;
    }
    else if( root->data > x ){      //x比当前节点小
        search( root->lchild , x ) ;     //往左子树递归
    }
    else{
        search( root->rchild , x ) ;
    }
}

2. 插入

对于一棵二叉查找树来说,查找某个数据域的结点一定是沿着确定的路径进行的。因此当对某个需要查找的值在二叉查找树中查找成功,说明结点已经存在;反之,如果这个需要查找的值在二叉查找树中查找失败,那么说明查找失败的地方一定是结点需要插入的地方。因此可以在上面查找的基础上,在root==NULL时新建需要插入的结点。显然插入的时间复杂度也是O(h),其中h为二叉查找树的高度。

//insert函数将在二叉树中插入一个数据域为x的新节点(注意参数root要加引用&)
void insert( Node* &root , int x ){
    if( root == NULL ){
        root = newNode(x) ;  //新建结点,权值为x(见9.1.3)
        return ;
    }
    
    if( root->data == x ){   //查找成功,说明结点已经存在
        return ;
    }
    else if( root->data > x ){      //x比当前节点小
        insert( root->lchild , x ) ;     //往左子树递归
    }
    else{
        insert( root->rchild , x ) ;
    }
    
}

3. 二叉查找树的建立

建立一颗二叉查找树,就是先后插入n个结点的过程,这和一般二叉树的建立完全是一样的。

//二叉查找树的建立
Node* Create( int data[] , int n ){
    Node* root = new Node ;
    for( int i = 0 ; i < n ; i++ ){
        insert(root , data[i]) ;
    }
    return root ;
}

4. 二叉查找树的删除

二叉查找树的删除操作一般有两种常见做法,复杂度都是O(h),h是二叉树的高度。
如果需要删掉根结点5,应该怎么做呢?为了保证删除操作之后仍然是一棵二叉查找树,一种办法是以树中比5小的最大结点(也就是结点4)覆盖结点5,然后删除原来的结点4;另一种办法是把树中比5大的最小结点(也就是结点6)覆盖结点5,然后删除原来的结点6。这两种做法都能保证删除操作之后仍然是一棵二叉查找树。
image.png
把以二叉查找树中比结点权值小的最大结点称为该结点的前驱,而把比结点权值大的最小结点称为该结点的后继。显然,结点的前驱是该结点左子树中的最右结点(也就是从左子树根结点开始不断沿着rchild往下直到rchild为NULL时的结点),而结点的后继则是该结点右子树中的最左结点(也就是从右子树根结点开始不断沿着lchild往下直到Ichild为NULL时的结点)。下面两个函数用来寻找以root为根的树中最大或最小权值的结点,用以辅助寻找结点的前驱和后继:

//寻找root为根节点的树中的最大权值结点
node* findMax( node* root ){
    while( root->rchild != NULL ){
        root = root->rchild ;
    }
    return root ;
}

//寻找root为根节点的树中的最小权值结点
node* findMin( node* root){
    while( root->lchild != NULL ){
        root = root->lchild ;
    }
    return root ;
}

假设决定用结点N的前驱P来替换N,于是就把问题转换为在N的左子树中删除结点P,就可以递归下去了,直到递归到一个叶子结点,就可以直接把它删除了。
因此删除操作的基本思路如下:

  1. 如果当前结点root为空,说明不存在权值为给定权值x的结点,直接返回。
  2. 如果当前结点root的权值恰为给定的权值x,说明找到了想要删除的结点,此时进入删除处理。
    a)如果当前结点root不存在左右孩子,说明是叶子结点,直接删除。
    b)如果当前结点root存在左孩子,那么在左子树中寻找结点前驱pre,然后让pre的数据覆盖root,接着在左子树中删除结点pre.
    c)如果当前结点root存在右孩子,那么在右子树中寻找结点后继next,然后让next的数据覆盖root,接着在右子树中删除结点next。
  3. 如果当前结点root的权值大于给定的权值x,则在左子树中递归删除权值为x的结点。
  4. 如果当前结点root的权值小于给定的权值x,则在右子树中递归删除权值为x的结点。
//删除以root为根结点的树中权值为x的结点
void deleteNode( node* &root , int x ){
    if( root == NULL ){     //不存在x结点,直接返回
        return ;
    }
    
    if( root->data == x ){
        if( root->lchild == NULL && root->rchild == NULL ){  //当前节点为叶子结点,直接删除
            root = NULL ;  //把root地址改为NULL,父节点引用不到了
        }
        else if( root->lchild != NULL ){
            node* pre = findMax( root->lchild ) ; //找root前驱
            root->data = pre->data ;
            deleteNode( root->lchild , pre->data ) ;  //在左子树删除pre
        }
        else{
            node* next = findMin( root->rchild  ) ;
            root->data = next->data ;
            deletNode( root->rchild , next->data ) ;
        }
    }
    else if( root->data > x ){
        deleteNode( root->lchid , x ) ;
    }
    else{
        deleteNode( root->rchild , x ) ;
    }
    
}

但是也要注意,总是优先删除前驱(或者后继)容易导致树的左右子树高度极度不平衡,使得二叉查找树退化成一条链。解决这一问题的办法有两种:一种是每次交替删除前驱或后继;另一种是记录子树高度,总是优先在高度较高的一棵子树里删除结点。

9.4.3 二叉查找树的性质

二叉查找树的中序遍历序列是有序的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值