前言
今天来说说经典数据结构之一,二叉搜索树(BST)。
在学习过链表后,再来看二叉树的实现会轻松不少,因为都包含了很多指针操作,同为通过指针指向连接起来的数据结构。建议先把链表吃透,再来看树的实现。
什么是二叉搜索树
二叉搜索树(Binary Search Tree),简称BST,也称为二叉搜索树、有序二叉树或排序二叉树,是指一棵空树或者具有下列性质的二叉树:
1、若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2、若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3、任意节点的左、右子树也分别为二叉查找树;
4、没有键值相等的节点。
下图是否是一颗二叉搜索树呢?
答案是否,因为左子树的最大节点4比根节点3要大,不符合性质1,这个在接下来的如何判断是否为BST
中还会讨论到。
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。平均查找复杂度为 O(log n),查找的效率和树的高度成正比,在最坏情况下为O(n),这时二叉树退化成一条线。
下面就和我开始一起构建一颗二叉搜索树吧!
二叉搜索树的方法们
BST的方法包括创建、查找、插入、删除和左旋右旋等,今天主要说说前几项。
BST节点的结构
1 | class binaryTreeNode |
这里为了方便讲解,暂定二叉树的节点元素为int类型,实际开发中一般会换成特定的结构体。
定义一个BST类
为了方便以后添加树的新方法,定义一个名为binaryTree的类,如下:
1 |
|
插入节点
插入操作分两步:
- 找到要插入父节点的位置;
- 比较与该结点的大小,并插入。
如何找到要插入的父节点位置,这里我们可以使用递归较为方便;在做插入操作时需要注意的是,当树为空时,需要申请一个新节点,并将其作为结果返回。为什么不直接改变根节点的指向,让它直接指向新节点呢?那样就需要用到二级指针,复杂化原有的逻辑;这样做能保持方法实现的一致性,只不过在一棵树为空树时,插入节点后要获取其返回值才行;同时还需考虑节点是否重复。
⁃ 代码如下:
1 | /* 插入一个节点 */ |
通过以上代码我们可以看到,其实插入操作的真正核心就在第一个if (root == NULL)
中。不管树的状态如何,程序最后都会走到这一步,然后再逐步返回。这也正是递归程序的真正意义,最后的结果永远会回到最终的地方,那里才是核心代码,其他都是为了走到那里的流程。
BST的创建
有了插入操作,创建BST就很简单了,我们可以把想放到树中的元素先放在一个vector里,然后逐个调用insert方法来构建BST.
代码如下:
1 | /* 创建一颗二叉树 */ |
删除节点
删除节点稍显复杂,需要分情况讨论:
- 待删除节点没有孩子
- 待删除节点有一个孩子
- 待删除节点有两个孩子
对于前两种情况很简单,分别用NIL和该孩子节点替换删除节点,再删除待删除节点即可;
- 情况一
情况二
变化前
变化后
如果删除节点有两个孩子,这种情况可以找到删除节点的左孩子中最大节点、或右孩子中最小节点替换待删除节点,再释放删除节点。如下图,要删除12:
情况三
变化前
变化后
为什么可以这样做呢?正常的思路考虑就是:用左子树中最大节点替换根节点,由于它比所有左子树其他节点要大,而且比右子树中所有节点要小,故用其替换当前根节点不会使BST的性质发生变化;用右子树中最小节点也是同理。
和这个类似的操作还有二叉树的左旋的右旋,都利用了一个BST可以有多种呈现方式的特性。
实现代码如下:
1 | binaryTreeNode *binaryTree::deleteNode(binaryTreeNode *root, int value) |
上面代码当删除节点有两个孩子时,首先用了左子树中最大节点替换了删除节点的内容,然后递归删除了左子树的最大节点,达到替换的目的。
其他一些功能方法
- 找到BST的最大节点
– 找一颗BST的最大节点就是找到其最深的右孩子节点,找到最小节点也是同理,即找到其最深的左孩子节点,便不再赘述。
代码如下:
1 | binaryTreeNode *binaryTree::maxValueNode(binaryTreeNode *root) |
- 判断一个元素是否已存在
1 | /* 利用set的无重复元素的性质,判断一个元素是否在树中 */ |
- 中序遍历
– 用递归的方法对一颗BST中序遍历(中根序遍历),中序遍历的结果是从小到大排好序的。
1 | /* 中序遍历 */ |
- 判断一棵树是否为二叉搜索树的方法(非类方法)
– 要判断一颗二叉树树是不是BST,即要判断是否它的所有节点都满足:- 左孩子中最大的节点小于当前节点;
- 右孩子中最小的节点大于当前节点;
代码如下:
1 | /* 判断一棵树是否为二叉搜索树(BST)*/ |
这种方法不是最高效的,但比较容易理解,如果想了解更高效的方法,请参阅判断是否是BST的方法
- 寻找一个节点
1 | /* 寻找一个节点,返回该节点的指针,没找到返回NULL */ |
测试程序
BST实现好了,下面来写代码测试它:
1 | int main() |
执行结果:
总结
以上对二叉搜索树(BST)的结构,性质,基本方法包括:创建、插入、删除、查找等,以及其他一些功能函数做了较为详细的分析,相信大家对BST已经有了更多的认识。
在学习BST的时候,递归是避免不了要理解的东西,想要理解递归,就要找到递归中最核心的代码部分,其余都是为了走向那里所做的流程处理,只不过是自己调用了自己,然后再从核心返回,一层一层调用回去。