定义
所谓二叉树。字如其名,就是这样的一棵树。对于任何结点,它最多只能有两个孩子。我们称作left和right.
对于每个结点而言,其内部存储三个指针,分别指向parent(后文以p代替)、left child、right child.
如下所示,即为二叉树的基本单元
一棵二叉搜索树具有的性质是,对于任意节点x,其left child和right child满足以下关系
(a)
其中,key表示节点内部存储的关键字。
一.遍历二叉搜索树
因为二叉搜索树的结构单元如上述所示,是固定的。那么我们可以非常简单地想到遍历其内部的方法---递归。即从部分到整体。
递归方法分为三类,取决于遍历每一个单元时的顺序:
(1)先序遍历,即先“取得”单元根部(比如上图就是x)的key,然后依次遍历left和right
(2)中序遍历,即先遍历left,再取得单元根部的key,最后遍历right
(3)后序遍历,即先遍历left,再遍历right,最后取得单元根部的key
三种方式的区别仅仅在于取得单元根部值的次序。下面是中序遍历的伪代码(摘自《算法导论》)
二.查询二叉搜索树
2.1查找具有关键字k的节点
既然已知二叉搜索树的性质(a),我们可以很非常简便地查询其中某个具有关键字k的结点(摘自《算法导论》).
这段伪代码非常明确,从树的根部x开始寻找,如果k小于当前节点的key,就取寻找它的left child,反之寻找它的right child。同样是利用了递归的思想(利用迭代也可以实现,但是一般递归的效率比迭代要高的多)。
2.2寻找最大关键字与最小关键字的节点。
这里不多赘述。最大关键字节点就是整棵二叉搜索树的“最右节点”.最小关键字节点就是二叉搜索树的“最左节点”,代码实现非常容易,只需要一直往left或者往right遍历,直到遇到NIL(叶子节点,为空)为止。
2.3结点的前驱与后继
所谓后继节点,指的是“关键字恰好大于当前结点”的那个结点。或者说,是“所有关键字大于当前结点的集合中,关键字最小的那个结点”。
同理,所谓前驱结点,也就是“关键字恰好小于/等于当前结点”的那个结点。或者说,是“所有关键字小于/等于当前结点的集合中,关键字最大的那个结点”。
后继的概念在第三节会用到。
三.插入与删除操作(重点)
3.1旋转操作(特别重要,但凡涉及二叉树,都可能会用到的基本操作)
所谓旋转操作,如下图所示(源自算法导论),一棵二叉搜索树,即使经过了旋转操作,也会保持基本性质(a).(为什么?后文会解释)
旋转操作分为两类(1)左旋(2)右旋,因为它们互为对称操作,所以只要记住其中一种即可,这里假设旋转中心为p结点。
左旋:
p的右孩子(p.right)代替p原本的位置,p成为其原右孩子的左孩子( p.right.left),同时p原本的右孩子成为p的父结点(p.parent)。然后,让p原本右孩子的左孩子(p.right.left)成为p的右孩子(p.right).
用文字描述比较绕,可以参照下图所示的过程
右旋
p的左孩子(p.left)代替p原本的位置,p成为其原左孩子的右孩子( p.left.right),同时p原本的左孩子成为p的父结点(p.parent)。然后,让p原本左孩子的右孩子(p.left.right)成为p的左孩子(p.left).
3.2插入操作
将一个具有关键字key的结点插入到二叉搜索树T中,这个过程非常简单,与在二叉搜索树中寻找具有关键字key的结点类似。即:
(1)从根结点开始,比较当前结点与待插入结点的key
(2)如果待插入的key比当前结点小,则以当前结点的左孩子作为新的当前结点,返回(1)
(3)如果待插入的key比当前结点大,则以当前结点的右孩子作为新的当前结点,返回(1)
(4)如果当前结点是NIL(空),则把待插入结点安插到当前位置上。
具体代码不详细贴出(可以贴,但是没有必要...)。
因为插入时已经遵循二叉搜索树的性质,所以直接插入即可,不会引起二叉搜索树性质的破坏。
3.3删除操作
和插入不同,因为删除操作可能发生于树中任何位置(插入一定是插入到叶子结点)
假设待删除的结点是p,那么,p无非只有以下三种情况:
(1)p没有孩子
(2)p只有左孩子,没有右孩子(p.right = NIL)
(3)p只有右孩子,没有左孩子(p.left = NIL)
(4)p既有左孩子,又有右孩子
对于情况(1),删除操作最为方便,既然p没有任何孩子,那么,p被删除对于整棵树而言没有丝毫影响(删了就删了吧)
至于情况(2)、(3),该做的事也非常明了,因为p只有唯一孩子,一旦把p删除,就让p的孩子顶替p。OK,万事大吉
至于情况(4),在《算导》里说了一大堆,实际上就坚持一个思路---用p的后继代替p。回顾上文,所谓p的后继指的是,那个“关键字恰好比p的关键字大的结点”,我们姑且称作后继结点A。既然p拥有右孩子,那p的后继一定位于右孩子的子树内(考虑以下二叉搜索树的性质,很容易得出这个推论)。一旦确定了p的后继A,就用A代替p。over。
下面是算导里的原图,(a)、(b)、(c)分别对应于情况(2)、(3)、(4)
说到这里,看似已经完全解决了删除操作带来的问题。哦不,等等,是不是还忽略了什么?
对,p的后继有孩子怎么办?比如下面这幅图
我们看到,对于结点p而言,它的后继A是有右孩子的。如果只是简单地用A来替换p...啊哦,那么A的右孩子V怎么办呢?
还能怎么办,当然是在用A代替p以前,先用A的右孩子V代替A。再用A代替V。OK,这下真的大功告成了。
所以,对于情况(4),我们可以这样描述:
寻找结点P的后继A,用A的右孩子代替A,再用A代替P。
什么,你说A的左孩子怎么办?----仔细想想二叉搜索树的性质,A绝对且肯定是没有左孩子的,不然,P的后继肯定在A的左子树里啊。(所谓后继,在这里也就是P的右子树中的最小结点。既然是最小的,当然是在“最左”的位置,如果A还有左孩子,显然A就不是“最左”了呀)。
说了这么多,能不能用更精简的方式来概括删除操作呢?以下是基于个人理解的总结:
(1)如果待删除结点P没有右子树,就用待删除结点P的孩子代替之(可能是左孩子,也可能是NIL)
(2)如果待删除结点P具有右子树,就寻找P的后继结点A。先用后继结点A的右孩子V替换A,再用后继结点A替换P。
这样看上去,应该更凝练了吧?(大概)
四.随机二叉搜索树
如果n个关键字按严格递增顺序插入,高度一定是n-1的一条链。为让之尽量平衡,如果构建一棵二叉搜索树为按随机次序插入的,那么将其称作随机二叉搜索树。
经过证明(其实不需要看算导里的证明,大概想以下也能想到),一个拥有n个节点的随机二叉搜索树的高度是log(n).
以上就是个人对于《算导》里二叉搜索树的理解。说实话,《算导》里很多推导都显得很"复杂"...笨比只好努力用更简洁的角度去思考以下问题了XDD.
如有错误,欢迎指正。