zoukankan      html  css  js  c++  java
  • 第四章节 树(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树

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

  • 相关阅读:
    进程
    Visual Studio Code 使用教程
    C# 多线程中的lock与token模式
    JavaScript中的多态
    简说GC垃圾回收
    C# 简单的SQLHelper
    JavaScript中addEventListener/attachEvent 与内联事件
    JavaScript中事件冒泡与事件捕获
    ASP.Net ScriptManager 与 UpdatePanel
    Nhibernate 使用sql语句查询
  • 原文地址:https://www.cnblogs.com/chuiyuan/p/4493250.html
Copyright © 2011-2022 走看看