二叉查找树
二叉查找树(Binary Search Tree)又称为有序二叉树(Ordered Binary Tree)
1、定义
二叉查找树具有以下形式:
对树中的每个节点n,其左子树(左子树的根是它的左子节点)中保存的所有数值都小于n中保存的数值v,其右子树中保存的所有数值都大于v。
2、遍历
树的遍历(tree treaversal)是访问树的所有节点,每个节点恰被访问一次。遍历可以看作是将所有的节点放在一条线上或是线性化一棵树。
遍历的定义只指定了一个条件——每个节点恰被访问一次,但是没有指定节点访问的顺序,因此节点有多少种排列次序,就有多少个树的遍历。对n个节点的树,就有n!种不同的遍历。这些遍历数量众多,而大部分遍历显然没用,这个事实使得我们更想将精力集中到两类遍历上,也就是著名的广度优先遍历和深度优先遍历。
(1) 广度优先遍历
广度优先遍历是从最底层(或最高层)开始逐层向上(或向下)遍历,而在同层自左向右(或自右向左)访问每一个节点。
(2) 深度优先遍历
深度优先遍历尽可能地沿着左边(或右边)前进,然后返回到第一个岔路口,转而访问其右边(或左边)的一个节点,然后再次尽可能地沿着左边(或右边)前进。重复这个过程,直到访问完所有的节点。
但是,这个定义并没有有明确地指出节点究竟是在向下前进之前还是返回后被访问的。这里有一些不同的深度优先遍历。下面给出这种遍历关心的三个任务:
V——访问一个节点
L——遍历左子树
V——遍历右子树
如果对每个节点都以相同的顺序执行这些任务,就是一个有序的遍历。这三个任务自身可以排列成3!=6中方式,因此,共有六种有序深度优先遍历:
VLR LVR LRV VRL RVL RLV
由于人们习惯于自左向右,因此取了其中的三种遍历方式,规定遍历总是自左向右移动:
VLR——先序遍历树
LVR——中序遍历树
LRV——后序遍历树
这三种遍历方式的名称均是根据根节点的访问时机而命名的。
这三种遍历方式的实现如果使用递归,非常简单明了,但是给系统增加了很重的任务,如果采用非递归的方式,仍然需要使用堆栈来存储一些相关的数据,因为需要回溯(出现回溯时,使用堆栈是很合理的),因此它是过分依赖堆栈的。然而某些时候,非递归的实现方式其实效率并不比递归的实现方式高。
无堆栈深度优先遍历
(1) 通过线索树遍历
上面分析的遍历方法不是递归就是非递归,但是它们都显式或隐式地使用堆栈存储尚未结束处理的节点信息。在递归方法中用了运行时堆栈,在非递归方法中,使用显式定义的堆栈,并由用户维护堆栈。问题是维护堆栈需要额外的时间,而且还要给堆栈留出一定的存储空间。最坏情况下,当数很歪斜时,堆栈可能会保存了树的几乎所有节点的信息,这对一个大的树来说有着很大的影响。
通过为给定节点增加线索,将堆栈合并成树的一部分,效率会高些。节点使用线索的树就称为线索树(threaded tree)。
(2) 通过转换树遍历
遍历算法需要使用堆栈来保存成功处理所必须的信息。线索树将堆栈合并为树的一部分,代价是为节点扩展一个域来区分右引用指向的是子节点还是后继结点。如果前驱节点和后继节点都考虑到的话就需要两个这样的域。但是不适用任何堆栈或线索来遍历一个树也是可能。有很多这样的算法都是在遍历期间对树进行了暂时性的改变,为一些引用域赋了新的值。但是,树可能会暂时失去了它的树形结构,这在遍历完成后需要恢复。这个技术是Joseph M.Morris为中序遍历算法设计的一个技巧。
以上讨论的遍历过程效率如何呢?所有的这些都是以Θ(n)运行的,线索的实现比转换树的实现多Θ(n)的额外空间,递归和迭代的遍历都需要Θ(n)的额外空间(运行是堆栈或用户自定义堆栈)。对随机产生的5000个节点的树进行多次运行测试,在先序遍历和后序遍历程序中(迭代、递归、Morris和线索),执行时间的差别仅仅是5%~10%。Morris遍历无疑比其他类型的遍历多了一项优势:它不需要额外空间。递归调用依赖于运行时堆栈,当遍历树很高时,堆栈可能会溢出。迭代遍历也使用了堆栈,尽管这个堆栈也会溢出,但是问题并不像运行是堆栈那样突出。线索树使用的节点比非线索树的节点大,这倒不成问题。但是迭代和线索的实现不如递归版本直观,所以,实现的清晰性和相似的运行时间决定了大多数情况下递归实现都是由于其他实现的。
3、插入
插入算法比较简单,从根节点扫描,如果比根节点大,就转到右子树,比根节点小,就转到左子树,继续比较,直到找到一个空的节点,把这个节点插入到此。
4、删除
删除比较麻烦,从二叉查找树中删除一个节点,一共有三种情况:
(1) 节点是一个树叶,没有子女。这种情况最简单,直接删除即可。
(2) 节点有一个子女。这种情况并不复杂,将父节点对该节点的引用改为对其子女的引用即可。
(3) 节点有两个子女。这是最复杂的情况,因为如果删除了该节点,其下的两个子女就成了两棵孤立的树了。有两种方法可以解决这一问题:归并删除法和复制删除法。
归并删除法:
将节点的两个子树合并成一棵树,然后将它连接到节点的父节点上。根据二叉查找树的性质,右子树中的每个键都大于左子树的键,因此最好的方法是寻找左子树中键最大的节点(即最右边的一个节点),并将其作为右子树的父节点。对称地,也可以找到右子树中键最小的节点(即最左边的一个节点),并将其作为左子树的父节点。

复制删除法:
节点有两个子女时可以把该节点左子树中的最大节点(或者右子树中的最小节点)的键复制到该节点中,并删除左子树的最大节点(或右子树的最小节点)。这个算法不会增加树高,但是如果删除和插入同时多次进行,也存在一个问题:算法是不对称的,如果每次都删除左子树的最大节点,将会使整棵树向右倾斜。为了解决这个问题,可以对算法的对称性作一个简单的改进。算法交替地从左子树和右子树中复制删除。

5、树的平衡
如果树中任何节点的两个子树的高度差为0或1,那么这个二叉树就是高度平衡的或简称为平衡的。
为了保持树的平衡,可以使用DSW算法。这个算法中树的变形的基本部分就是旋转(rotation)。有两种旋转方式:左旋和右旋。节点围绕它的父节点Par右转就是右旋。这个过程可以被描述为:
rotateRight(Gr,Par,Ch) //Gr是Par的父节点,Par是Ch的父节点
if Par不是树根 //即Gr非空
Gr成为Ch的父节点;
Ch的右子树成为Par的左子树;
Par成为节点Ch的右子女;
出自:《数据结构与算法——Java语言版(第二版)》
注:最后的归并删除和复制删除两个函数是我自己写的,因为书给还了。写的不是很好,没时间改了。
出自:《数据结构与算法——Java语言版(第二版)》