第四章节 树(1)

目录

一、预备知识

二、二叉树

三、查找树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树

八、标准库中的集合与映射

 

posted @ 2015-05-10 22:20  chuiyuan  阅读(196)  评论(0编辑  收藏  举报