数据结构——树(7)——二叉搜索树及其操作原理

二叉树与二叉搜索树

在之前的文章中,我们提到过三叉树,n叉树,但是我们实际用的最多却是二叉树,因为这样的结构更适合我们编程和更适合我们使用递归的方式。所以我们可以限制孩子的数量使得生成的树更容易实施。那么怎么定义二叉树呢?
- 树中的每个节点至多有两个孩子。
- 除根节点之外的每个节点均被指定为其父项的左侧子项或右侧子项。

第二个条件强调了二叉树中的子节点相对于其父母排序。也就是说当子节点的顺序不一样的时候,他们就不是一棵相同的树。(跟堆不同)。看下面的例子:
这里写图片描述
尽管他们都是同一个节点B组成,在这两种情况下,标记为B的节点是标记为A的根节点的子节点,但是B是第一棵树中A的左边的孩子,在第二棵树中却是A的右边的孩子,是不同的。这样定义的几何关系的使得用二叉树表示有序的数据集合变得方便。最常见的应用程序使用称为二叉搜索树(binary search tree)的特殊类二叉树,它有以下属性定义:
- 每个节点包含(可能除了其他数据之外)一个特殊值,称为定义节点顺序的关键字。
- 节点值是唯一的
- 在树中的每个节点上,其值必须大于以其左子树为根的子树中的所有值,并且小于以其右子树为根的子树中的所有值。

简单的来说就是,对于二叉搜索树中所有的节点X,在它左子树的所有元素都小于X,在它右字数的元素都大于X.看下面的两幅图:
这里写图片描述
左边的是一棵标准的二叉搜索树,根节点为6,在6的左子树中,所有的值都小于6(1 2 3 4),在右子树中所有的值都大于6(8)。而子树中也同样满足这样的规律,如节点2.
右边就不是一棵二叉搜索树。问题出在节点4中,7在4的右边没错,但是7 > 6,节点7应该在6的右子树。
二叉搜索树(如果构造得够好)的平均深度是OTrees(log2(n))。

为什么要使用二叉搜索树?

在我们之前的问题中,我们提到过一个问题叫插入问题。在一串数字之间插入一个数字,那么在数组的实现中,我们要为这个数后挪或者前移n个单位,以便为插入的数据提供空间以便插入。假设我们要在一串数子中间插入某个数字,那么数组的算法复杂度就是O(N).为了很好的解决这个问题,我们采用了另一种数据结构来表示这样的结构。那就是链表。我们前面提到过,在链表中实现插入删除的算法复杂度是O(1)。但是问题在于,在数组中,找到这个中间元素是很容易的(长度减一嘛),在链表中,你必须得遍历整个链表才能找到这个元素。
那么为什么链表有这个限制呢?我们用下面的例子来说明(假设你有一个包含下面单词的链表):
这里写图片描述
给定一个这样的链表,你可以很容易地找到第一个元素,因为初始指针会给你它的地址。从那里,你可以按照链接指针找到第二个元素,但是找到序列中间出现的元素并不容易。要做到这一点,你必须遍历每个链指针,一直数到N / 2。这个操作需要线性时间。如果二叉搜索能够提高效率,那么数据结构就能够快速找到中间元素。
我们可以直接将指针指向我们的中间元素,这样我们就可以很方便找到我们的中间元素了。是不是觉得很愚蠢这个想法,但是这却是最有效果的。我们来看看:
这里写图片描述
显然,这个的话,我们可以说我们以Grumpy为起点,只能遍历到这个单词后面的部分,前面的我们都不能遍历,没有实际意义。那么现在的问题是,我们要怎么样才能遍历左边的单词呢?其实很简单,我们只要把这边的箭头反转就好了:
这里写图片描述
这样我们就可以遍历所有的单词了,算法复杂度比我们的链表直接遍历快了一半。而且中间的数据访问的复杂度为o(1).此时,我们再想,如果这个时候我们继续对分开的部分进行同样的处理,那么同样可以快速实现搜索。这就是我们的递归策略。这个时候结构就变成了这样:
这里写图片描述
关于这种特殊风格的二叉树最重要的特点是它是有序的。对于树中的任何特定节点,它所包含的字符串必须遵循降序到左边的子树中的所有字符串,并且在子树的所有字符串之前。在这个例子中,Bashful在Doc之前,Dopey在Doc之后。这样我们就可以很方便的寻找到我们适合的元素。

二叉搜索树的基本操作

为了更好的使用二叉搜索树,我们为它们定义一些方法。很幸运,因为树这样的特殊结构我们为它们定义的方法都是可以用递归去实现的: 我们假设我们有下面一棵二叉搜索树:
这里写图片描述
我们希望在二叉搜索树中得到的功能主要有:

findMax() //寻找这颗二叉搜索树的最大值并返回其值
findMin() //寻找这颗二叉搜索树的最小值并返回其值
contains() //判断这颗二叉树是否包含某个值.就是上述的找到某个节点
add() //往二叉搜索树里面添加某个值
/*还有个比较难的方法*/
remove() //向树中移除某个值

讨论下列的方法:

findMax()

根据二叉搜索树的特点,我们从根部开始,直接搜索右子树(因为左子树的值一定小于根的值),在右子树中,每个节点的右孩子一定大于左孩子,所以我们的操作是:从根开始,向右搜索,直到节点不再有右孩子为止,返回该节点的值。

findMin()

原理同上,但是操作为:从根开始,向左搜索,直到节点不再有左孩子为止,返回该节点的值。

contains()

判断元素X是不是属于这棵二叉搜索树T,我们可以这样实现:
1. 如果树为空,直接返回false
2. 如果这树只有一个节点,且节点就为X,那么直接返回true
3. 判断X与根的关系
- 若X > root,那么在右子树中递归调用contains()
- 若X < root,那么在左子树中递归调用contains()

add()

向树中添加元素X,跟contains方法相似:
1. 如果树为空,直接添加
2. 判断X与根的关系
- 若X > root,那么在右子树递归调用add()
- 若X < root,那么在左子树递归调用add()

3.直到遍历的节点为空,添加。

扫描二维码关注公众号,回复: 2343685 查看本文章

这里我们举个例子,假如在上图中加入一个元素5,那么步骤应该是这样的,先判断5是否大于6.否,插入的应该是左子树,也就是6的左边。再判断5是否大于2,是,插入的应该是对应的右子树,再判断5是否大于4,是,插入的应该是对应的右子树,此时4对应的右节点为空,因此我们将5插入这里。

remove()

删除节点,为什么说这个操作比较难呢,因为存在几种可能性。操作如下:
1. 寻找要删除的节点
2. 如果要删除的点是节点,那么直接执行删除操作
3. 如果节点有一个孩子,肯定是左孩子,那么删除的操作就是直接删除该节点,并将上面节点的右节点直接指向这个节点的左孩子(是不是就是我们的链表的删除呢?对的!!
4. 如果节点有两个孩子,用它右子树中最小的数代替这个节点,然后递归的删除这个空节点。
5. 如果要删除的数是树根,那么要特殊考虑

他们的算法复杂度(平均)都是O(logN)

下一篇我就基于这篇原理来谈谈C++代码的实现

猜你喜欢

转载自blog.csdn.net/redrnt/article/details/79209079
今日推荐