【大话数据结构C语言】56 二叉排序树的查找、插入和删除

本文介绍了二叉排序树的基本操作,包括查找关键字、插入关键字和删除关键字。在查找过程中,如果关键字等于根节点关键字则查找成功,否则根据大小关系在左右子树中继续查找。插入操作涉及到在合适位置新增节点,而删除操作则需要考虑多种情况来正确移除节点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

欢迎关注我的公众号是【CodeAllen】,关注回复【1024】获取精品学习资源
程序员技术交流①群:736386324 ,程序员技术交流②群:371394777    

目录

二叉排序树查找关键字

二叉排序树中插入关键字

二叉排序树中删除关键字

总结


二叉排序树查找关键字

 
二叉排序树中查找某关键字时,查找过程类似于次优二叉树,在二叉排序树不为空树的前提下,首先将被查找值同树的根结点进行比较,会有 3 种不同的结果:
  • 如果相等,查找成功;
  • 如果比较结果为根结点的关键字值较大,则说明该关键字可能存在其左子树中;
  • 如果比较结果为根结点的关键字值较小,则说明该关键字可能存在其右子树中;
实现函数为:(运用递归的方法)
BiTree SearchBST(BiTree T,KeyType key){
    //如果递归过程中 T 为空,则查找结果,返回NULL;或者查找成功,返回指向该关键字的指针
    if (!T || key==T->data) {
        return T;
    }else if(key<T->data){
        //递归遍历其左孩子
        return SearchBST(T->lchild, key);
    }else{
        //递归遍历其右孩子
        return SearchBST(T->rchild, key);
    }
}

 

二叉排序树中插入关键字

二叉排序树本身是动态查找表的一种表示形式,有时会在查找过程中插入或者删除表中元素,当因为查找失败而需要插入数据元素时,该数据元素的插入位置一定位于二叉排序树的叶子结点,并且一定是查找失败时访问的最后一个结点的左孩子或者右孩子
 
例如,在下图 的二叉排序树中做查找关键字 1 的操作,当查找到关键字 3 所在的叶子结点时,判断出表中没有该关键字,此时关键字 1 的插入位置为关键字 3 的左孩子。
 
所以,二叉排序树表示动态查找表做插入操作,只需要稍微更改一下上面的代码就可以实现,具体实现代码为:
BOOL SearchBST(BiTree T,KeyType key,BiTree f,BiTree *p){
    //如果 T 指针为空,说明查找失败,令 p 指针指向查找过程中最后一个叶子结点,并返回查找失败的信息
    if (!T){
        *p=f;
        return false;
    }
    //如果相等,令 p 指针指向该关键字,并返回查找成功信息
    else if(key==T->data){
        *p=T;
        return true;
    }
    //如果 key 值比 T 根结点的值小,则查找其左子树;反之,查找其右子树
    else if(key<T->data){
        return SearchBST(T->lchild,key,T,p);
    }else{
        return SearchBST(T->rchild,key,T,p);
    }
}
//插入函数
BOOL InsertBST(BiTree T,ElemType e){
    BiTree p=NULL;
    //如果查找不成功,需做插入操作
    if (!SearchBST(T, e,NULL,&p)) {
        //初始化插入结点
        BiTree s=(BiTree)malloc(sizeof(BiTree));
        s->data=e;
        s->lchild=s->rchild=NULL;
        //如果 p 为NULL,说明该二叉排序树为空树,此时插入的结点为整棵树的根结点
        if (!p) {
            T=s;
        }
        //如果 p 不为 NULL,则 p 指向的为查找失败的最后一个叶子结点,只需要通过比较 p 和 e 的值确定 s 到底是 p 的左孩子还是右孩子
        else if(e<p->data){
            p->lchild=s;
        }else{
            p->rchild=s;
        }
        return true;
    }
    //如果查找成功,不需要做插入操作,插入失败
    return false;
}

 

 
通过使用二叉排序树对动态查找表做查找和插入的操作,同时在中序遍历二叉排序树时,可以得到有关所有关键字的一个有序的序列。
 
例如,假设原二叉排序树为空树,在对动态查找表  {3,5,7,2,1}  做查找以及插入操作时,可以构建出一个含有表中所有关键字的二叉排序树,过程如下图所示:
 
 
通过不断的查找和插入操作,最终构建的二叉排序树如图 2(5) 所示。当使用中序遍历算法遍历二叉排序树时,得到的序列为: 1 2 3 5 7  ,为有序序列。
一个无序序列可以通过构建一棵二叉排序树,从而变成一个有序序列。
 
 

二叉排序树中删除关键字

在查找过程中,如果在使用二叉排序树表示的动态查找表中删除某个数据元素时,需要在成功删除该结点的同时,依旧使这棵树为二叉排序树。
 
假设要删除的为结点 p,则对于二叉排序树来说,需要根据结点 p 所在不同的位置作不同的操作,有以下 3 种可能:
 
1、结点 p 为叶子结点,此时只需要删除该结点,并修改其双亲结点的指针即可;
2、结点 p 只有左子树或者只有右子树,如果 p 是其双亲节点的左孩子,则直接将 p 节点的左子树或右子树作为其双亲节点的左子树;反之也是如此,如果 p 是其双亲节点的右孩子,则直接将 p 节点的左子树或右子树作为其双亲节点的右子树;
3、结点 p 左右子树都有,此时有两种处理方式:
1)令结点 p 的左子树为其双亲结点的左子树;结点 p 的右子树为其自身直接前驱结点的右子树,如下图所示;
 
2)用结点 p 的直接前驱(或直接后继)来代替结点 p,同时在二叉排序树中对其直接前驱(或直接后继)做删除操作。如下图为使用直接前驱代替结点 p:
 
上图中,在对左图进行中序遍历时,得到的结点 p 的直接前驱结点为结点 s,所以直接用结点 s 覆盖结点 p,由于结点 s 还有左孩子,根据第 2 条规则,直接将其变为双亲结点的右孩子。
 
#include<stdio.h>
#include<stdlib.h>
#define TRUE 1
#define FALSE 0
#define ElemType int
#define  KeyType int
/* 二叉排序树的节点结构定义 */
typedef struct BiTNode
{
    int data;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

//二叉排序树查找算法
int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p) {
    //如果 T 指针为空,说明查找失败,令 p 指针指向查找过程中最后一个叶子结点,并返回查找失败的信息
    if (!T) {
        *p = f;
        return FALSE;
    }
    //如果相等,令 p 指针指向该关键字,并返回查找成功信息
    else if (key == T->data) {
        *p = T;
        return TRUE;
    }
    //如果 key 值比 T 根结点的值小,则查找其左子树;反之,查找其右子树
    else if (key < T->data) {
        return SearchBST(T->lchild, key, T, p);
    }
    else {
        return SearchBST(T->rchild, key, T, p);
    }
}
int InsertBST(BiTree *T, ElemType e) {
    BiTree p = NULL;
    //如果查找不成功,需做插入操作
    if (!SearchBST((*T), e, NULL, &p)) {
        //初始化插入结点
        BiTree s = (BiTree)malloc(sizeof(BiTNode));
        s->data = e;
        s->lchild = s->rchild = NULL;
        //如果 p 为NULL,说明该二叉排序树为空树,此时插入的结点为整棵树的根结点
        if (!p) {
            *T = s;
        }
        //如果 p 不为 NULL,则 p 指向的为查找失败的最后一个叶子结点,只需要通过比较 p 和 e 的值确定 s 到底是 p 的左孩子还是右孩子
        else if (e < p->data) {
            p->lchild = s;
        }
        else {
            p->rchild = s;
        }
        return TRUE;
    }
    //如果查找成功,不需要做插入操作,插入失败
    return FALSE;
}
//删除函数
int Delete(BiTree *p)
{
    BiTree q, s;
    //情况 1,结点 p 本身为叶子结点,直接删除即可
    if (!(*p)->lchild && !(*p)->rchild) {
        *p = NULL;
    }
    else if (!(*p)->lchild) { //左子树为空,只需用结点 p 的右子树根结点代替结点 p 即可;
        q = *p;
        *p = (*p)->rchild;
        free(q);
    }
    else if (!(*p)->rchild) {//右子树为空,只需用结点 p 的左子树根结点代替结点 p 即可;
        q = *p;
        *p = (*p)->lchild;//这里不是指针 *p 指向左子树,而是将左子树存储的结点的地址赋值给指针变量 p
        free(q);
    }
    else {//左右子树均不为空,采用第 2 种方式
        q = *p;
        s = (*p)->lchild;
        //遍历,找到结点 p 的直接前驱
        while (s->rchild)
        {
            q = s;
            s = s->rchild;
        }
        //直接改变结点 p 的值
        (*p)->data = s->data;
        //判断结点 p 的左子树 s 是否有右子树,分为两种情况讨论
        if (q != *p) {
            q->rchild = s->lchild;//若有,则在删除直接前驱结点的同时,令前驱的左孩子结点改为 q 指向结点的孩子结点
        }
        else {
            q->lchild = s->lchild;//否则,直接将左子树上移即可
        }
        free(s);
    }
    return TRUE;
}
int DeleteBST(BiTree *T, int key)
{
    if (!(*T)) {//不存在关键字等于key的数据元素
        return FALSE;
    }
    else
    {
        if (key == (*T)->data) {
            Delete(T);
            return TRUE;
        }
        else if (key < (*T)->data) {
            //使用递归的方式
            return DeleteBST(&(*T)->lchild, key);
        }
        else {
            return DeleteBST(&(*T)->rchild, key);
        }
    }
}
void order(BiTree t)//中序输出
{
    if (t == NULL) {
        return;
    }
    order(t->lchild);
    printf("%d ", t->data);
    order(t->rchild);
}
int main()
{
    int i;
    int a[5] = { 3,4,2,5,9 };
    BiTree T = NULL;
    for (i = 0; i < 5; i++) {
        InsertBST(&T, a[i]);
    }
    printf("中序遍历二叉排序树:\n");
    order(T);
    printf("\n");
    printf("删除3后,中序遍历二叉排序树:\n");
    DeleteBST(&T, 3);
    order(T);
}

 

 
运行结果:
中序遍历二叉排序树:
2 3 4 5 9
删除3后,中序遍历二叉排序树:
2 4 5 9
 

总结

使用二叉排序树在查找表中做查找操作的时间复杂度 同建立的二叉树本身的结构有关。即使查找表中各数据元素完全相同,但是不同的排列顺序,构建出的二叉排序树大不相同。
例如:查找表  {45,24,53,12,37,93}  和表  {12,24,37,45,53,93}  各自构建的二叉排序树图下图所示:
 
 
 
 
使用二叉排序树实现动态查找操作的过程,实际上就是从二叉排序树的根结点到查找元素结点的过程,所以时间复杂度同被查找元素所在的树的深度(层次数)有关。
 
为了弥补二叉排序树构造时产生如图 5 右侧所示的影响算法效率的因素,需要对二叉排序树做“平衡化”处理,使其成为一棵平衡二叉树。
 
 
 
 
 
 

 

平衡二叉树是一种特殊的二叉搜索树,它的左右子树的高度差不超过1,这样可以保证平衡二叉树查找插入删除操作的时间复杂度都是O(log n)。 平衡二叉树有很多种,其中比较常见的有AVL树、红黑树、B树等。在本文中,我们主要介绍AVL树。 AVL树是一种严格平衡的二叉搜索树,它的每个节点的左右子树高度差不超过1。当节点的高度差超过1时,就需要通过旋转操作来重新平衡。AVL树的特点是:对于一个节点,其左右子树的高度差不超过1,且左右子树都是AVL树。 插入操作 插入操作是AVL树中比较复杂的操作,因为插入一个节点可能导致整个树失去平衡。下面是AVL树的插入操作: 1. 在AVL树中插入一个节点,首先按照二叉搜索树的规则找到插入的位置。 2. 如果插入节点后,其父节点的左右子树高度差不超过1,则不需要进行旋转操作,直接返回。 3. 如果插入节点后,其父节点的左右子树高度差超过1,则需要进行旋转操作。 4. 如果插入节点在父节点的左子树中,并且插入节点的左子树高度大于插入节点的右子树高度,则进行右旋操作;如果插入节点在父节点的右子树中,并且插入节点的右子树高度大于插入节点的左子树高度,则进行左旋操作。 5. 如果插入节点在父节点的左子树中,并且插入节点的左子树高度小于插入节点的右子树高度,则进行左右旋转操作;如果插入节点在父节点的右子树中,并且插入节点的右子树高度小于插入节点的左子树高度,则进行右左旋转操作。 删除操作 删除操作也是AVL树中比较复杂的操作,因为删除一个节点可能导致整个树失去平衡。下面是AVL树的删除操作: 1. 在AVL树中删除一个节点,首先按照二叉搜索树的规则找到要删除的节点。 2. 如果要删除的节点没有子节点,则直接删除即可。 3. 如果要删除的节点只有一个子节点,则将子节点替换成要删除的节点。 4. 如果要删除的节点有两个子节点,则先找到要删除节点的后继节点(即右子树中最小的节点),将后继节点的值赋给要删除的节点,然后将后继节点删除。 5. 删除一个节点可能会导致整个树失去平衡,因此需要进行旋转操作。 6. 如果删除节点后,其父节点的左右子树高度差不超过1,则不需要进行旋转操作,直接返回。 7. 如果删除节点后,其父节点的左右子树高度差超过1,则需要进行旋转操作。 8. 如果删除节点在父节点的左子树中,并且删除节点的左子树高度大于删除节点的右子树高度,则进行右旋操作;如果删除节点在父节点的右子树中,并且删除节点的右子树高度大于删除节点的左子树高度,则进行左旋操作。 9. 如果删除节点在父节点的左子树中,并且删除节点的左子树高度小于删除节点的右子树高度,则进行左右旋转操作;如果删除节点在父节点的右子树中,并且删除节点的右子树高度小于删除节点的左子树高度,则进行右左旋转操作。 总结 AVL树是一种严格平衡的二叉搜索树,它的每个节点的左右子树高度差不超过1。插入删除操作可能会导致整个树失去平衡,需要通过旋转操作来重新平衡。AVL树比较适合用于读取操作比较频繁的场景,因为它的查找插入删除操作的时间复杂度都是O(log n)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值