搜索二叉树的认识以及底层实现

  如果说到对一个数组进行查找相应的数据,要求效率最高,大家会想到什么方式呢?二分查找?二分查找的效率确实很高,时间复杂度为O(logN)。但是如果我们想要在数组当中添加新的数据呢?加上这一功能之后二分查找的效率可能就不那么好了。因为二分查找的时候要求数组连续并且有序,那么我们在插入数据的时候就一定会伴随着数据的移动操作。那么我们插入数据的时间复杂度就成为了O(N)。为了解决这个困扰我们将进行学习搜索二叉树。

  (一)认识搜索二叉树

  1.什么是搜索二叉树

 所谓的搜索二叉树其实就是具有特殊规则要求的二叉树。搜索二叉树要求我们每一个子树都应该满足其左孩子节点当中存储的值都应该小于父节点当中存储的值,其右孩子节点当中存储的值都应该大于我们父节点当中存储的值。搜索二叉树结构如下图所示:

  只要我们满足这样的规则就说明我们的二叉树是一颗搜索二叉树。

  2.如何构建一颗搜索二叉树

  想要构建一颗搜索二叉树我们就需要熟悉搜索二叉树的规则,也就是左子树当中的值一定小于根节点,右子树当中的值一定大于根节点。之后我们一次将数据根据这种规则加入到树当中即可。

  如上图中的例子所示。我们只需要将数组当中的数据依次根据规则插入到树当中即可。小于我们的根节点就进行左子树当中进行判断,如果小于我们的父节点当中的值就作为左叶子节点插入到树当中。如果大于我们的父节点当中的值就作为右叶子节点插入到树当中。直到最后所有的数据均插入到树当中为止。

  3.搜索二叉树的作用

  相信大家都很奇怪,为什么我们的搜索二叉树可以代替二分查找算法呢?我们来仔细进行观察。由于我们构建搜索二叉树的规则,所以我们想要对一个数据进行查找的时候每一次都可以删除掉近似乎一般的数据。例如加入我们想要对23进行查找的时候。我们需要先根32进行比较,小于32之后直接进入左子树进行下一步的查找,右子树当中的数据直接忽略不计。因为右子树当中的值是大于32的,而我们的23小于32因此不可能在右子树当中出现。

  就这样每次删除一般的数据所以我们查找一个数据的时间复杂度为O(logN)。这个效率是跟我们二分查找的时间复杂度基本相符的。但是搜索二叉树在插入数据的时候的时间复杂度要远优于二分查找数组的时间复杂度。

  对于搜索二叉树来说,插入一个数据我们只需要进行高度次的判断之后就可以插入我们的目标数据。所需要的时间复杂度只有在查找插入为止的时候的O(logN)而已。找到数据应该的插入为止之后直接进行插入即可。

  同时搜索二叉树还有一个优点——当我们构建完成搜索二叉树之后相当于我们已经对数据完成了排序操作。因为对于搜索二叉树进行中序遍历我们就可以得到一个有序的数组。

  但是对于我们的搜索二叉树来说也有一个很明显的弊端,那就是极端的情况下效率并不高。

  想象一下:跟我们上述结构相同的搜索二叉树,想要搜索5实际上就是对我们数组进行了依次遍历操作时间复杂度就变成了O(N)同样的我们的插入数据的操作的时间复杂度也变成了O(N)。如果是这种情况其实并不比我们的二分查找数组好。但是毕竟是极端情况很少会遇到,并且之后我们为了避免这种情况的产生还引入了一种平衡二叉搜索树的结构,使得我们每一棵子树的高度相差小于2,这样就避免了我们这种情况的产生,当然这都是后话,我们先进行学习二叉搜索树。

  (二)二叉搜索树的迭代实现

  那么接下来我们就通过迭代循环的方式实现我们搜索二叉树的相关的功能。

  首先我们要搭建好二叉搜索树的一个框架。有了之前实现list底层的经验之后相信这一部分对大家来说都很简单了。首先我们需要创建一个BSTree作为搜索二叉树的主体。之后再创建一个BSNode类作为我们二叉搜索树当中的单个节点。在BSNode当中我们需要存储左子树的指针,右子树的指针,以及该节点当中保存的数据即可。

  如上图说是,我们在BSTree类当中创建了一个节点作为root节点,之后通过BSNode类对我们常见的节点进行初始化操作,也就是将指针置为空,并赋予相对应的值的操作。

  1.insert功能

  首先我们来实现搜索二叉树的插入功能。

  根据我们前面的分析我们可以知道,想要插入一个新的数据。我们需要先对根节点进行判断操作,如果根节点为空,那么我们可以直接构建一个newnode节点作为我们的根节点即可。如果根节点不为空,我们就需要依次根节点当中所已经存储的数据进行比较,如果想要插入的数据的值小于我们节点当中所存储的值,那么我们在左子树当中继续查找,知道找到数据需要插入的位置即可。右子树同样的道理,如果插入的数据大于我们节点当中存储的值就进入右子树当中进行查找知道找到我们的目标位置即可。

  2.inorder函数

  之后为了便于打印输出检查我们构建的搜索二叉树功能是否一切正常,我们可以编写一个中序遍历的函数,因为根据搜索二叉树的性质,在构建好搜索二叉树的时候,数据就已经被排好序了,我们只需要走一遍中序遍历将数据打印出来即可。

  由于我们的根节点是一个私有的成员变量,对于我们的每一个搜索二叉树对象来说只有一个唯一的根节点。所以为了减少函数之间的耦合程度我们可以进行一次小小的封装。我们在类的内部单独创建一个inorder函数,传入我们的根节点的指针。使得我们在外部使用的时候不需要知道我们根节点的指针具体是什么了。之后直接编写一个简单的中序遍历树的函数即可。测试内容如下:

  经过测试我们可以发现我们的插入函数以及中序遍历函数功能一切正常。

  3.find函数

  接下来我们来进行数据的查找操作。

  查找所需要进行的操作无非就是和我们节点当中的数据进行比较,如果大于根节点当中的数据就到右子树当中继续进行查找。如果小于节点当中存储的数据就到左子树当中继续进行查找,直到查找到目标的数据或者到叶子节点位置。

  运行代码发现结果一切正常。

  4.erase函数

  对于搜索二叉树来说比较复杂的部分就是我们的删除操作了。首先我们需要进行分类讨论。根据我们具体的删除情况来说总共会有以下几种需要删除数据的情况:

  具体数据的删除操作大致分为以上三种形式:

  第一种:我们需要删除的数据在叶子节点的位置,查找到需要删除的数据之后我们直接将数据删除即可。

  第二种:我们需要删除的数据只拥有一个子节点,也就是说另一个左孩子或者右孩子节点为空。这种情况也很好处理,我们只需要将需要删除的节点的指针替换为唯一的孩子节点即可。

  第三种:需要删除的节点同时拥有左孩子和右孩子节点,这个时候我们就需要着重进行处理了。首先我们需要选出一个可以替换我们删除节点的子节点。并且替换之后要保持我们的搜索二叉树的结构不变。我们仔细观察会发现,当我们选择左子树当中的最大值或者右子树当中的最小值进行替换可以满足搜索二叉树的结构不变。为了方便我们采用左子树当中的最大值进行替换。编写代码如下:

  对于我们需要删除节点没有孩子节点的情况其实可以和只有一个子节点的情况进行合并处理。对于没有孩子的情况我们可以将其看成是将另一个子节点当中的空赋值给我们父亲节点对应的指针。实现上述逻辑就可以正确编写我们的erase函数。运行效果如下:

代码运行一切正常。

  5.析构函数

  之后我们只需要进行简单的补充即可。首先我们来实现析构函数。对于我们的析构函数来说我们只需要进行特定顺序的节点的释放即可。

  代码如上述所示。我们需要注意的是最好使用后序遍历操作。因为如果我们使用其他方式进行遍历释放的时候都会因为释放掉根节点造成一部分节点的缺失,带来内存泄漏的问题。

  6.拷贝构造和赋值运算符重载函数

  对于构造函数其实我们需要进行的就是按照指定的顺序进行数据的插入操作。但是我们需要注意我们数据的插入的顺序,因为插入的顺序不同我们构建的树的结构也不同。所以我们需要按照特定的顺序进行数据的插入操作才可以构建出一棵一模一样的搜索二叉树。在这里我们采用前序遍历的方式进行插入操作。

  之后对于我们的赋值运算符重载函数只需要使用我们的新型写法即可。也就是将赋值的操作交给我们的拷贝构造进行,我们将构造好的内容进行交换即可。

  此上我们的搜索二叉树也就实现完毕了。

  (三)二叉搜索树的递归实现

  对于二叉搜索树的递归形式我们仅需要改变一些很小的细节即可。

1.insert函数

  首先我们想要编写递归函数,就需要传入一个可以作为递归标志的变化的参数。通常对于树来说,我们要想实现递归的形式就需要将根节点的指针作为参数传入给我们的函数。因此为了完成递归操作我们在insert函数当中嵌入一个_insert函数,手动的将_root作为参数传入给我们的_insert函数当中。

  相信大家对于大部分的逻辑都是清楚的,我们只需要以递归的形式找到叶子节点当中需要插入数据的位置即可。但是有一点很新颖的地方。那就是我们对于引用的使用,之前我们在使用循环编写的时候都会需要找到父节点,之后通过父节点起到插入节点的作用。否则当我们找到空节点的时候还得回过头重新查找该空节点的父节点是谁。

  但是当我们使用引用的时候就不需要进行这个操作了。因为使用引用之后我们得到的就是“父亲”节点指向的那个空间,直接进行修改就可以达到我们想要的效果。

  运行代码,结果一切正常。

2.find函数

  对于我们的find函数来说甚至更加简单了。我们连数据的插入都不需要了,只需要在找到数据的时候返回相对应的节点即可。

  当我们找到相应的值的时候就返回该节点的指针,当查找到叶子节点还没有找到相应的值的时候就返回nullptr。

  3.erase函数

  最后将erase函数修改成为递归的形式即可。

  我们只需要将erase函数当中查找的操作修改成为递归的形式即可。对于子树的判断操作,我们仍需要手动进行。

  经过上述的操作之后我们的代码也就完全改编成了递归的形式了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿白逆袭记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值