挑战408——数据结构(21)——二叉搜索树的插入和删除

在二叉搜索树种最重要的就是插入和删除操作了

二叉搜索树的插入

我们要在二叉搜索树中执行各种操作的前提就是,我们首先要有一棵二叉搜索树。那么,如何创建一棵二叉搜索树呢?最简单的方法就是我们可以从一棵空树开始,每次调用一个addNode函数,将一个新的值插入二叉搜索树中。但是在每次插入的时候我们都要保持树的一个排序关系,因此我们要做的就是在插入的时候,找到我们要插入的值应该在的位置
因此和遍历的代码一样,addNode的代码可以从树根开始递归地进行。在每个节点上,addNode必须将要插入的值与当前节点中的值进行比较。 如果要插入的值小于当前的值,则该值属于左子树。相反,如果要插入的值大于当前的值,则属于右子树。最终,该进程将遇到一个NULL子树,该子树表示需要添加新节点的树中的点。此时,addNode用一个初始化为包含新的值的新节点替换NULL指针。(也就是说这样的做法是,先找到要插入的点,然后再生成新的节点,随后插入树中)。
看起来挺简单吧,但是实现的代码却并非如此简单,难点在于,我们插入值后,改变了树的结构,因此我们的参数就要用我们的引用参数。我们最终返回的是一个指向树的指针,因此返回的类型是指针。我们有两种方式来实现这个函数:

利用引用参数传递
//第一个辅助函数
void stringSet::add(string s, Node *&node){
    if(node = NULL){                   //基础事件(base case)
        node = new Node(s);
        count++;
    }else if(node -> str > s){        //用else if是因为这样的情况只会出现某一种,提高效率
        add(s, node -> left);
    }else if (node -> str < s){
        add(s,node -> right);
    }
}

我们先来详细分析一下这个函数。原型的话我们看的很特别:

void stringSet::add(string s, Node *&node)

特点在于第二个参数Node * &node。如果树为空,则addNode将创建一个新节点,初始化其字段,然后用指向新节点的指针替换现有结构中的NULL指针。 如果树不为空,则addNode将新数值与树根进行比较。如果数值相等,则说明这个值已经在树中,不需要进一步的操作。如果不等,则addNode使用比较结果来确定是将数值插入左边还是右边的子树,然后进行适当的递归调用。
为了更好的理解第二个参数的原理,我们试着画出整个过程的图解,假设我们现在新初始化了一棵新树:

stringSet *dwarfTree = NULL;

在调用上述的语句后,我们可以得到如图所示的图解:
在这里插入图片描述
接下来我们调用:

addNode("Grumpy", dwarfTree);

在addNode的参数中,我们建立一个数值为Grumpy的节点,那么&node(下图中的&t)就是dwarfTree(树根)的一个引用,或者说是它的地址:
在这里插入图片描述
第一句if代码检测&node是否为空树,此时为true,那么我们就执行:

node = new Node(s); //Node中有三个变量,value,left,right

这一行在堆上分配一个含值为S的新节点,并将其分配给引用参数node,从而改变调用者中的指针值,如下所示:
在这里插入图片描述在这里插入图片描述
上图的结构代表了只含有一个节点Grumpy时的存储情况。此时树不再为空,变量dwarfTree现在包含了指向节点Grumpy的地址了。假设Sleep在Grumpy的后面,那么程序就会调用:

add(s,node -> right);

在这里插入图片描述
当通过add方法添加空节点的时候,就将Grumpy的右指针指向这个空节点。在函数返回的时候再将具体的内容填入该节点中:
在这里插入图片描述
在这里插入图片描述
此外,对addNode的调用将创建新的节点,并将其插入结构中,从而保留了二进制搜索树所需的排序约束。 例如,如果按Doc,Bashful,Dopey,Happy和Sneezy的顺序插入其余五个单词,则最终可以得到以下的二叉树:
在这里插入图片描述
对应于下图:
在这里插入图片描述

利用指向指针的指针
void StringSet::add(string s, Node **node) {
   if (*node == nullptr) {
       *node = new Node(s);
       count++;
   } else if ((*node)->str > s) {
       add(s, &((*node)->left));
   } else if ((*node)->str < s) {
       add(s, &((*node)->right));
   }
}

上面的代码看起来会比较的抽象,我们还是图解一下,假设我们要执行add(5):
在这里插入图片描述
首先我们的node为变量存着树根的地址,*node为指向树根的指针的指针。如图所示。当根为空的时候,赋值。

当为非空的时候,比较值,然后递归调用add方法。当根为空的时候新建一个节点,并赋值。然后将*node指向这个新增的节点:
在这里插入图片描述
在这里插入图片描述

注意,在此过程中node*的指向一直都在变化的!

二叉搜索树的移除操作

二叉搜索树的移除是很麻烦的,因为他要考虑三种情况,假如我们有下面一棵树:
在这里插入图片描述1. 当要删除的节点是叶子的时候,我们直接删除,因为并不影响二叉搜索树的结构
在这里插入图片描述
2. 当要删除的节点含有一个孩子的时候,先将节点删除,再将指向该节点的指针,指向该节点的孩子。(如删除sleep操作)
在这里插入图片描述
3. 当要删除的节点含有两个孩子的时候,我们先将节点删除,然后选择左子树中的最右节点或右子树中的最左节点。(任意选一个,例如选择左节点该节点保证比左侧子树中的其他任何元素都大,但小于右侧子树中的值。),然后重复此操作(如删除此时的树根):
在这里插入图片描述
在这里插入图片描述
于是可以有以下代码:

//重载remove函数
Node * stringSet::remove(string s, Node *node, Node *parent){
    //遍历相应的子树,直到找到我们要删除的节点
    if(s < node -> str){ //此时要删除的节点应该在左子树
        if(node -> left != NULL){ //如果存在左孩子
            return remove(s, node -> left, node); //递归寻找对应的s,并删除
        }else{
            return NULL; //当我们要删除的数值在树中不存在
        }
    }else if(s > node -> str){ //此时此时要删除的节点应该在右子树,解释同上
        if(node -> right != NULL){
            return remove(s, node ->right, node);
        }else{
            return NULL;
        }
    }else{ //我们找到了要删除的节点node
        if(node ->left != NULL && node ->right != NULL){ //情况一,有两个孩子
            node -> str = findMin(node -> right);//将此节点的值用右子树的最小值代替
            return remove(node ->str, node ->right,node);//递归删除右子树的最小值
        }else if(parent -> left == node){
            //将父节点的左侧替换为该节点的右侧或左侧子节点
            //取决于哪一个存在(如果它没有子元素,则右边是NULL)
            parent -> left = (node -> left != NULL) ? node ->left : node ->right;
            return node;
        }else if(parent ->right == node){
            parent -> right = (node->left != NULL) ? node->left : node->right;
            return node;
        }
    }
    return NULL; //这一行永远不会到达,但是没有这一行编译时会有问题
}

完整二叉搜索树的代码可以点击 二叉搜索树的操作实现C++代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值