目录
一、预备知识
二、二叉树
三、查找树ADT-----二叉查找树
四、AVL树
五、伸展树
六、树的遍
七、B树
八、标准库中的集合与映射
对于大量的输入数据 ,链表的线性访问时间太慢,不宜使用。而树的大部分操作都是O(logN).这种结果就是二叉查找树,它是两种库
集合类 TreeSet /TreeMap 的基础。
一、预备知识
深度:从根到 ni的唯一 的路径长,根的深度是0。
高度:从ni到树叶的最长路径长。 树叶的高为0。一个树的高为它根的高。
- 树的实现
- 树的遍历和应用
先序遍历
对节点的处理在它的诸儿子节点处理前。如列出分级文件系统中目录的伪代码,效果如下:
private void listAll(int depth ){
printName(depth) ;
if (isDirectory())
for each file c in this dir(for each child)
c.listAll(depth+1);
}
public void listAll(){
listAll(0);
}
从深度为0开始。
中序遍历
先左,再中,再右,可以用于顺序的输出所有的项。
后序遍历
对节点的处理是在它的儿子节点被计算后再进行。如要计算每个文件的大小。
public int size(){
int totalSize = sizeOfThisFile();
if (isDirectory())
for each file c in this dir (for each child)
totalSize+=c.size();
return totalSize ;
}
二、二叉树
每个节点都不能有多于两个儿子。
二叉树的一个重要的性质:
一个平均二叉树的深度比节点个数 N小得多。平均深度为N开方。对于特殊的,也就是二叉查找树,平均深度为
O(logN).
class BinaryTree {
Object element ;
BinaryTree left ;
BinaryTree right;
}
二叉树有很多与搜索不相关的应用,如编译器设计 。
三、查找树ADT-----二叉查找树
二叉树一个重要 的应用是查找 。
要使二叉树成为二叉查找树,则:
对于树中的每个节点 X ,它的左子树中所有项目的值小于X,右子树中所有项的值 大于X的值。
二叉查找查要求所有的项目都可以排序。要写出一个一般 的类,就要有一个Comparebla 接口。
下面是代码文件,和链表中一样,BinaryNode是一个嵌套的类。
private static class BinaryNode <Anytype>{
Anytype element ;
BinaryNode<Anytype> left ;
BinaryNode<Anytype> right ;
BinaryNode(Anytype element ) {
}
BinaryNode(Anytype element , BinaryNode<Anytype> lt, BinaryNode<Anytype> rt ) {
this.element = element ;
this.left = lt ;
this.right = rt ;
}
}
下面是BinarySearchTree类的代码 。
package Tree;
public class BinarySearchTree <Anytype extends Comparable<? super Anytype>>{
private static class BinaryNode <Anytype>{
Anytype element ;
BinaryNode<Anytype> left ;
BinaryNode<Anytype> right ;
BinaryNode(Anytype element ) {
}
BinaryNode(Anytype element , BinaryNode<Anytype> lt, BinaryNode<Anytype> rt ) {
this.element = element ;
this.left = lt ;
this.right = rt ;
}
}
private BinaryNode< Anytype> root ;
public BinarySearchTree(){
root = null;
}
public void makeEmpty (){
root = null ;
}
public boolean isEmpty (){
return root==null ;
}
public boolean contains(Anytype x){
return contains(x, root ) ;
}
public Anytype findMin () throws Exception{
if (isEmpty())
throw new Exception() ;//should be UnderflowException
return findMin (root).element;
}
public Anytype findMax () throws Exception{
if (isEmpty())
throw new Exception() ;
return findMax(root ).element ;
}
public void insert (Anytype x ){
insert(x, root) ;
}
public void remove (Anytype x ){
remove(x, root );
}
/**
* 没完成
*/
private boolean contains(Anytype x , BinaryNode<Anytype> t ){
return true ;
}
private BinaryNode<Anytype> findMin (BinaryNode<Anytype> t){
return null ;
}
private BinaryNode<Anytype> findMax (BinaryNode<Anytype> t){
return null ;
}
private BinaryNode<Anytype> insert (Anytype x , BinaryNode<Anytype> t ){
return null ;
}
private BinaryNode<Anytype> remove(Anytype x, BinaryNode<Anytype> t ){
return null ;
}
private void printTree (BinaryNode<Anytype> t ){
}
}
- contains方法
代码如下:
private boolean contains(Anytype x , BinaryNode<Anytype> t ){
if (t == null){
return false ;
}
int result = x.compareTo(x) ;
if (result<0){
return contains(x,t.left) ;
}else if (result>0){
return contains(x , t.right) ;
}else {
return true ;
}
}
这里使用的是尾递归。可以用一个while循环代替,不过这里使用栈的空间量也不过是 O(logN)而已,没有大的问题。
下面是一种使用函数对象而不是要求这些 项是 Comparable 的方法。(省)
- findMax与finaMin方法
findMax:只要有右儿子就向右进行。
findMin :只要有左儿子就向左进行。
我们一种用递归 ,一种不用递归写。
private BinaryNode<Anytype> findMin (BinaryNode<Anytype> t){
if (t== null){
return null ;
}else if ( t.left==null) {
return t;
}
return findMin(t.left) ;
}
private BinaryNode<Anytype> findMax (BinaryNode<Anytype> t){
if (t!=null){
while (t.right!=null)
t= t.right ;
}
return t ;
}
- insert方法
可以像contains那样查找 (实际就是一次遍历)
1.如果找到,什么也不用做。
2.如果没有,则将X插入到遍历路径的最后 一个点上。
由于t 引用树的根,而根在第一次插入时变化 ,因此 insert返回的是新树的根。
如下:
private BinaryNode<Anytype> insert (Anytype x , BinaryNode<Anytype> t ){
if (t== null){
return new BinaryNode<Anytype>(x, null,null) ;
}
int result = x.compareTo(t.element) ;
if (result<0){
t.left = insert(x, t.left) ;
}else if (result>0) {
t.right = insert(x, t.right) ;
}else {
//重复,不处理
}
return t;
}
- remove方法
和很多数据结构一样,最复杂 的是删除操作。
如果删除的节点是:
1.一个树叶:直接删除。
2.有一个儿子:这个节点可以在其父亲节点调整自己的链以绕过自己后删除 。
3.有两个儿子:用其右子树最小的节点(容易找到)代替这个节点的数据,并递归的删除那个节点(现在它是空的)。因为右
树中最小的节点不可能 有左儿子,所以第二次remove很容易。
注意3中,因为总是用右子树的节点来代替被 删除的节点 ,所以倾向于使械子树比右子树高。
下面是代码 ,但是效率并不是很高,因为它对树进行两次搜索以查找 和删除右子树中最小的节点 ,通过写一个removeMin()可以解决这个问题。
我们先不考虑这个 。
private BinaryNode<Anytype> remove(Anytype x, BinaryNode<Anytype> t ){
if (t== null) return t ;
int result = x.compareTo(t.element) ;
if (result<0){
t.left = remove(x, t.left) ;
}else if (result>0) {
t.right = remove(x, t.right) ;
}else if (t.left!= null && t.right!= null) {
//用其右子树最小的节点(容易找到)代替这个节点的数据
t.element = findMin(t.right).element ;
//并递归的删除那个节点(现在它是空的)
t.right = remove(t.element, t.right) ;
}else {
//只有一个儿子,这个节点可以在其父亲节点调整自己的链以绕过自己后删除
t = (t.left!= null) ? t.left: t.right ;
}
return t;
}
如果 删除的元素不多,我们使用的是惰性删除,也就是并没有真的删除元素,只是标记被删除的元素。这种特别是在有重复项的时候很适用,只用将频率减1。
- 平均情况分析
如果所有 的插入序列都是等可能 的,则树的所有节点的平均深度为O(logN)。
如果一个树的输入预先进行了排序 ,则一连串的insert操作将会花费二次的时间。而链表的实现代价会非常的大,因为这时树只有右儿子。一
一种解决的办法就是,让任何节点的深度都不能过深。(平衡树?)
下面要引入的是一个很古老的平衡查找树-AVL。
另外 一种比较新的方法是放弃平衡条件,允许树有任意的深度,但是每次操作后用一定的规则进行调整,使后面的效率更高。这种是自调整结构。在二叉查找树下,我们不再保证 O(logN)的时间界,但是可以证明任意M次操作,复杂度为O(MlogN).
四、AVL树
AVL树是带有平衡条件的树二叉查找树。这个平衡条件要容易保持 ,而且能够保证树的深度是O(logN).
一个AVL树是每个节点的左子树和右子树高度最多相差1 的二叉查找树(空树的高度-1)。一个实际的AVL树的高度只略大于logN. 是
除了可能插入外(假设是惰性删除),所有的树的操作都 可以在O(logN)里完成。插入可能会破坏AVL树的特性,这个问题可能通过旋转来搞定 。
只有那些从插入点到根节点路径上的点的平衡性才有可能变化。称要重新平衡的节点为A,出现不平衡就要A点的两个子树的高度差2.有以下几种情况 :
1、对A点的左儿子的左子树进行一次插入。
2、对A点的左儿子的右子树进行一次插入。
3、对A点的右儿子的右子树进行一次插入。
4、对A 点的右儿子的左子树进行一次插入。
理论上只有两种,编程上看有四种。理论上看:
第一种:发生在外边,(左-左,右-右),可以通过单旋转解决。
第二种:发生在内边,(左-右,右-左),可能 通过双旋转处理。
上面处理都是对树的基本操作。将会用在别的平衡算法中。
- 单旋转
如下图示,这里出现了情况 都是外边情况。
插入3,2,1,在1时出现了问题。
插入4时没有问题,5时节点3处有问题。
插入6时,根节点处左子树高度是0,右子树高度是2.
插入7时有问题。
- 双旋转
上面的对于下图的情况没有作用。
当在上面的基础上插入16, 15时,出现不平衡。
再插入14时,出现不平衡
经过上面的分析 ,我们总结出,为将一个新的结点X插入到T中,我们递归的将X插入到T相应的子树(TrL)中,并更新高度。
如果子树高度不变,则完成。
如果子树高度变化,则要进行调整。
- AVL树的节点 声明
private static class AvlNode <Anytype>{
Anytype element ;
int height ;
AvlNode<Anytype> left ;
AvlNode<Anytype> right ;
AvlNode(Anytype element ,AvlNode<Anytype> right, AvlNode<Anytype> left ){
this.element = element ;
this.left = left ;
this.right = right ;
}
AvlNode(Anytype element){
this(element, null, null) ;
}
}
我们要有一个快速的返回高度的方法,同时要处理null引用的问题。
private int height(AvlNode<Anytype> t ){
return t== null? -1: t.height ;
}
- 单旋转方法
第一种是左旋转,如下图
/**
* single rotate for case 1
* there should be a method rotateWithRightChild
* @param k2
* @return new root
*/
private AvlNode<Anytype> rotateWithLeftChild(AvlNode<Anytype> k2){
AvlNode<Anytype> k1 = k2.left;
k2.left = k1.right ;
k1.right = k2;
k2.height = Math.max(height(k2.left), height(k2.right))+1;
k1.height= Math.max(height(k1.left), k2.height)+1;
return k1 ;
}
- 双旋转方法
/**
* first left child with its right child
* then node k3 with new left child
* @param k3
* @return
*/
private AvlNode<Anytype> doubleWithLeftChild(AvlNode< Anytype> k3 ){
k3.left = rotateWithRightChild(k3.left) ;
return rotateWithLeftChild(k3) ;
}
AVL树的删除更加复杂 ,不过如果 删除比较少,可以用惰性删除。
五、伸展树
六、树的遍历
七、B树
八、标准库中的集合与映射