数据结构基础-平衡二叉树的删除操作

回忆二叉搜索树和平衡二叉树的相关操作

在学习过BST(二叉搜索树)的删除和AVL树(平衡二叉树)的插入操作后,我们可以先回忆一下两个操作的思路和代码书写,这样有利于这节课程的展开

BST的删除

BST的删除,是比较繁琐的,是根据节点的度进行的判断,进而写出了三个删除思路,要通过设置父亲节点pre和寻找到的需要删除节点node来改变指针方向和free空间,度为0和1的节点都好说,只需要直接删除或者令父亲节点pre继承被删除节点node的左或者右孩子即可,而节点度为2的需要找到其左子树的最右节点或者右子树的最左节点进行替换操作后进行节点删除(思路就是中序序列的前驱后继)

AVL树的插入

总体的一个递归思想,然后加入平衡因子判断和旋转操作

(详情可自行查找之前写过的代码)

而我们在面对AVL树的删除操作时,应该如下手AVL树节点的删除才能保证其删除某节点后进行何等操作还可以成为AVL树?

假设一节点x节点,由于我在x节点的右子树删除了一个节点导致了失衡,造成的结果相当于:我在x节点左子树添加了一个节点导致失衡

所以删除导致的失衡,可以转化为在另一棵子树上添加一个节点导致的失衡

所以我么可以将AVL树的删除拆分为两个操作:一个是删除二叉搜索树上的节点(广义可理解为删除AVL树上的节点,因为AVL树的本质也是二叉搜索树),另一个是对整棵树进行左旋或右旋的判断

那么本节的思路主要是融会贯通好二叉搜索树的删除和AVL树的插入操作,没有其他需要进行记忆理解的新思路

本节代码思路讲解

1.BST的删除(以AVL树为主体):

(1)删除的节点x是叶子结点

(2)删除的节点x只有一个孩子

(3)删除的节点x左右孩子都有

代码如下:

//找树tree的最左边的节点,也就是找tree中的最小节点
avlTree mini_node(avlTree tree){
    if(tree==NULL){
        return NULL;//好像有些多余?
    }
    while(tree->left != NULL){
        tree = tree->left;
    }
    return tree;
}

//AVL树中删除数据为k的节点,并且返回修改后的根节点
avlTree avlTree_deleteNode(avlTree tree,int k){
    if(tree==NULL){
        return NULL;
    }
    if(k==tree->data){
        if(tree->left!=NULL && tree->right!=NULL){
            //使用方法二进行查找 度为2
            avlNode *min_node = mini_node(tree->right);//寻找右子树中的最小节点
            tree->data = min_node->data;//成功将后继值进行替换
            tree->right = avlTree_deleteNode(tree->right,min_node->data);//通过递归继续删除需要删除的节点
        }
        else{
            //本次将节点度为0和1的操作合并,因为空节点也可以被视作子树,并不会影响什么
            avlNode *node = tree;//标记被删除节点位置
            if(tree->left != NULL){
                tree = tree->left;
            }
            else{
                tree = tree->right;
            }
            free(node);
            node = NULL;
            return tree;
        }
    }
    //被删除节点k位于树的左子树上
    else if(k < tree->data){
        //继续查找
        tree->left = avlTree_deleteNode(tree->left,k);
    }
    else if(k > tree->data){
        tree->right = avlTree_deleteNode(tree->right,k);
    }
    return tree;
}

该代码块只做到了递归删除,递归的每一轮都将一个新的树节点传了进来,所以思路一定要清晰,其呈现一个阶梯状的递归调用,先有一层if用于查找data所在处,找到后再有一层if用于修改父节点等的连接,但如果说上述代码不进行值的返回的话,也没什么问题,但其为了要对接AVL树的旋转操作,可以理解,所以我们继续分析下面的操作

2.AVL树的删除

//找树tree的最左边的节点,也就是找tree中的最小节点
avlTree mini_node(avlTree tree){
    if(tree==NULL){
        return NULL;//好像有些多余?
    }
    while(tree->left != NULL){
        tree = tree->left;
    }
    return tree;
}

//AVL树中删除数据为k的节点,并且返回修改后的根节点
avlTree avlTree_deleteNode(avlTree tree,int k){
    if(tree==NULL){
        return NULL;
    }
    if(k==tree->data){
        if(tree->left!=NULL && tree->right!=NULL){
            //使用方法二进行查找 度为2
            avlNode *min_node = mini_node(tree->right);//寻找右子树中的最小节点
            tree->data = min_node->data;//成功将后继值进行替换
            tree->right = avlTree_deleteNode(tree->right,min_node->data);//通过递归继续删除需要删除的节点
        }
        else{
            //本次将节点度为0和1的操作合并,因为空节点也可以被视作子树,并不会影响什么
            avlNode *node = tree;//标记被删除节点位置
            if(tree->left != NULL){
                tree = tree->left;
            }
            else{
                tree = tree->right;
            }
            free(node);
            node = NULL;
            return tree;
        }
    }
    //被删除节点k位于树的左子树上
    else if(k < tree->data){
        //继续查找
        tree->left = avlTree_deleteNode(tree->left,k);
        //进行平衡因子的判断,由于此时删除了左子树的节点,可能会造成右子树深度大于左子树的情况,所以进行判断
        if(get_h(tree->right)-get_h(tree->left) > 1){
            //判断出该节点失衡了,继续判断该节点失衡的具体情况,且目前只能是RR或者RL,进行种类判断
            avlNode *temp = tree->right;
            //拿失衡节点右孩子的左右子树进行判断(目前存疑=需不需要进行讨论)
            if(get_h(temp->right)>=get_h(temp->left)){
                //RR形
                tree = rr_rotation(tree);
            }
            else{
                //RL形
                tree = rl_rotation(tree);
            }
        }
    }
    else if(k > tree->data){
        tree->right = avlTree_deleteNode(tree->right,k);
        //判断出该节点失衡了,继续判断该节点失衡的具体情况,且目前只能是LL或者LR,进行种类判断
        avlNode *temp = tree->left;
        //拿失衡节点右孩子的左右子树进行判断(目前存疑=需不需要进行讨论)
        if(get_h(temp->left)>=get_h(temp->right)){
            //LL形
            tree = ll_rotation(tree);
        }
        else{
            //LR形
            tree = lr_rotation(tree);
        }
    }
    tree->h = max(get_h(tree->left),get_h(tree->right));
    return tree;
}

AVL树的应用场景和BST应用场景一样,AVL不适合频繁地插入和删除的场景,否则时间复杂度仍然会增多,不如二叉搜索树

所以结合之前的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#define max(a,b) ((a,b) ? (a) : (b))

//设置节点
typedef struct node{
    int data;
    struct node *left;
    struct node *right;
    int h;//节点所处树的高度
}avlNode,*avlTree;

//中序遍历-用于输出平衡二叉树的序列
void in_order(avlTree tree){
    if(tree!=NULL){
        in_order(tree->left);
        printf("%d ",tree->data);
        in_order(tree->right);
    }
}

//创建新节点,该函数用于在插入函数中,即找到节点时使用
avlNode *create_node(int key,avlNode *left,avlNode *right){
    avlNode *node = (avlNode*)malloc(sizeof(avlNode));
    node->data = key;
    node->left = left;
    node->right = right;
    node->h = 0;
    return node;
}

//返回该节点处于树中的高度
int get_h(avlNode* node){
    if(node==NULL){
        return 0;
    }
    else{
        return node->h;
    }
}

//四个函数对应四种情况
//单右旋LL
avlTree ll_rotation(avlNode *x){
    //x为失衡节点,因在其左子树左孩子处插入
    avlNode *y = x->left;
    x->left = y->right;
    y->right = x;
    
    //改变x和y节点的高度
    x->h = max(get_h(x->left),get_h(x->right)) + 1;
    y->h = max(get_h(y->left),get_h(y->right)) + 1;
    //除更改节点xy外其他节点的相对高度均没有发生变化
    return y;
}
//单左旋RR
avlTree rr_rotation(avlNode *x){
    //x为失衡节点,因在其右子树右孩子处插入
    avlNode *y = x->right;
    x->right = y->left;
    y->left = x;
    
    x->h = max(get_h(x->left),get_h(x->right)) + 1;
    y->h = max(get_h(y->left),get_h(y->right)) + 1;
    //除更改节点xy外其他节点的相对高度均没有发生变化
    return y;
}
//双旋LR
avlTree lr_rotation(avlNode *x){
    //失衡节点是由于 其左子树的右子树插入节点 所以先对x的左子树本身进行节点转移rr_rotation(x->left)
    x->left = rr_rotation(x->left);
    //此时以x节点为根节点所在树的结构变成了LL形
    x = ll_rotation(x);
    return x;
}
//双旋RL
avlTree rl_rotation(avlNode *x){
    //失衡节点是由于 其右子树的左子树插入节点 所以先对x的右子树本身进行节点转移rr_rotation(x->left)
    x->right = ll_rotation(x->right);
    //此时以x节点为根节点所在树的结构变成了RR形
    x = rr_rotation(x);
    return x;
}

avlTree avlTree_insertNode(avlTree tree,int k){
    //该递归插入巧妙地解决了新节点的插入 平衡因子的判断和树高度的值的返回 
    if(tree==NULL){
        tree = create_node(k,NULL,NULL);
    }
    else if(k < tree->data){
        tree->left = avlTree_insertNode(tree->left,k);//执行完插入
        //判断平衡因子,该节点的左子树比右子树高,所以是L_
        if(get_h(tree->left)-get_h(tree->right) >1){
            //判断该节点左子树值与k值的关系以此判断是LL还是LR
            if(k < tree->left->data){
                //如果k值比较小,那么说明在x左孩子的左子树上
                //LL
                tree = ll_rotation(tree);
            }
            else{
                //如果k值比较大,那么说明在x左孩子的右子树上
                //LR
                tree = lr_rotation(tree);
            }
        }
    }
    else{
        tree->right = avlTree_insertNode(tree->right,k);//执行完插入
        //判断平衡因子,该节点的右子树比左子树高,所以是R_
        if(get_h(tree->right)-get_h(tree->left) >1){
            //判断该节点左子树值与k值的关系以此判断是RR还是RL
            if(k > tree->right->data){
                //如果k值比较大,那么说明在x右孩子的右子树上
                //RR
                tree = rr_rotation(tree);
            }
            else{
                //如果k值比较小,那么说明在x右孩子的左子树上
                //RL
                tree = rl_rotation(tree);
            }
        }
    }
    tree->h = max(get_h(tree->left),get_h(tree->right))+1;
    return tree;
}

//找树tree的最左边的节点,也就是找tree中的最小节点
avlTree mini_node(avlTree tree){
    if(tree==NULL){
        return NULL;//好像有些多余?
    }
    while(tree->left != NULL){
        tree = tree->left;
    }
    return tree;
}

//AVL树中删除数据为k的节点,并且返回修改后的根节点
avlTree avlTree_deleteNode(avlTree tree,int k){
    if(tree==NULL){
        return NULL;
    }
    if(k==tree->data){
        if(tree->left!=NULL && tree->right!=NULL){
            //使用方法二进行查找 度为2
            avlNode *min_node = mini_node(tree->right);//寻找右子树中的最小节点
            tree->data = min_node->data;//成功将后继值进行替换
            tree->right = avlTree_deleteNode(tree->right,min_node->data);//通过递归继续删除需要删除的节点
        }
        else{
            //本次将节点度为0和1的操作合并,因为空节点也可以被视作子树,并不会影响什么
            avlNode *node = tree;//标记被删除节点位置
            if(tree->left != NULL){
                tree = tree->left;
            }
            else{
                tree = tree->right;
            }
            free(node);
            node = NULL;
            return tree;
        }
    }
    //被删除节点k位于树的左子树上
    else if(k < tree->data){
        //继续查找
        tree->left = avlTree_deleteNode(tree->left,k);
        //进行平衡因子的判断,由于此时删除了左子树的节点,可能会造成右子树深度大于左子树的情况,所以进行判断
        if(get_h(tree->right)-get_h(tree->left) > 1){
            //判断出该节点失衡了,继续判断该节点失衡的具体情况,且目前只能是RR或者RL,进行种类判断
            avlNode *temp = tree->right;
            //拿失衡节点右孩子的左右子树进行判断(目前存疑=需不需要进行讨论)
            if(get_h(temp->right)>=get_h(temp->left)){
                //RR形
                tree = rr_rotation(tree);
            }
            else{
                //RL形
                tree = rl_rotation(tree);
            }
        }
    }
    else if(k > tree->data){
        tree->right = avlTree_deleteNode(tree->right,k);
        //判断出该节点失衡了,继续判断该节点失衡的具体情况,且目前只能是LL或者LR,进行种类判断
        avlNode *temp = tree->left;
        //拿失衡节点右孩子的左右子树进行判断(目前存疑=需不需要进行讨论)
        if(get_h(temp->left)>=get_h(temp->right)){
            //LL形
            tree = ll_rotation(tree);
        }
        else{
            //LR形
            tree = lr_rotation(tree);
        }
    }
    tree->h = max(get_h(tree->left),get_h(tree->right));
    return tree;
}
    


int main()
{
    int a[9];
    srand(time(NULL));
    for(int i=0;i<9;i++){
        int random_num = rand()%100;
        a[i] = random_num;
    }

    avlTree tree = NULL;
    //int a[9] = {1,4,2,8,5,10,78,66,30};
    for(int i=0;i<9;i++){
        tree = avlTree_insertNode(tree,a[i]);
    }
    in_order(tree);
    printf("\n");
    
    tree = avlTree_deleteNode(tree,a[2]);
    in_order(tree);
    printf("\n");

    return 0;
}

可自行修改删除节点用于测试

运行结果:

自己看着办

小结

结合前面的二叉排序树和平衡二叉树的概念,我们需要对该知识点进行多次的复习,去找一些在线oj网站进行一些练习。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值