树总结

在这里,我们详细分析各种树的性质和操作。树形结构的特点是一个数据元素可以有很多个直接后继,但只有一个直接前驱。而这里面最常见的就是二叉树。

一、二叉树

1、二叉树概述

二叉树是每个节点最多两棵子树,而且需要区别左右子树的树。

如果二叉树中任意一层的节点个数都达到最大(2^(h-1),h为层号),则称为满二叉树。

如果满二叉树最底层从右向左(不能跳过)删去若干节点,称为完全二叉树。堆就是一种完全二叉树。完全二叉树的性质我们要熟悉,因为完全二叉树是可以单纯用数组表示的。对于堆排序或者其他一些编程,我们需要使用这些性质。我们把一个有n个节点的完全二叉树的节点从上到下,从左到右依次编号,对第i个节点(从1开始):

(1)如果i=1,则为根节点;如果i>1,他的父节点编号为i/2向下取整;

(2)他的左右孩子的编号分别为2i和2i+1。当然,如果2i>n或2i+1>n,则不存在该左或右孩子。

2、二叉树的遍历

二叉树的遍历,分为前序,中序,后序和层次遍历。遍历从图上看是很简单的,关键是要理解各种算法的代码实现。在对于树的学习中,对于算法实现是很重要的。因为在实际的编程题中,不可能只是叫你进行一些简单的树的操作,肯定会有各种不同的题目。而这其中对基本算法的灵活运用以及对递归的深刻理解就很重要。对于树的算法来说,递归是其核心。其实对于递归来说,递归本质上也是有一个递归树,因此递归天然符合树的性质和结构。在这里,为了演示,先构造了一棵如图所示的树。


class treenode{
	public String value;//为了方便,直接全部用public,很不规范
	public treenode leftchild;
	public treenode rightchild;
	public treenode() {
		value="";
		leftchild=null;
		rightchild=null;
	}
	public treenode(String value) {
		this.value=value;
		leftchild=null;
		rightchild=null;
	}
}

(1)前序遍历,先后遍历根节点,左子树,右子树

ALBECDWX

    public static void preorder(treenode root) {
    	if(root==null)
    		return;
    	System.out.print(root.value+" ");
    	preorder(root.leftchild);
    	preorder(root.rightchild);
    }

(2)中序遍历,先后遍历左子树,根节点,右子树

BLEACWXD

    public static void inorder(treenode root) {
    	if(root==null)
    		return;
       	inorder(root.leftchild);
    	System.out.print(root.value+" ");
    	inorder(root.rightchild);
    }

(3)后序遍历,先后遍历左子树,右子树,根节点

BELXWDCA 

    public static void postorder(treenode root) {
    	if(root==null)
    		return;
       	postorder(root.leftchild);
    	postorder(root.rightchild);
    	System.out.print(root.value+" ");
    }

(4)层次遍历

层次遍历不需要递归,而是必须借助队列先进先出的性质。层次遍历的实现方法如下:首先将根结点入队。当队列不为空时,出队,访问出队的结点,然后把它非空的左、右子树先后入队。

ALCBEDWX 

    public static void depthtraverse(treenode root) {//层次遍历
    	LinkedList<treenode> list=new LinkedList<treenode>();//在java中,LinkedList实现了Queue
    	//接口,因此可以把LinkedList当做先进先出队列来使用。使用add添加元素,使用poll来移除并返回头部元素(先进入的元素)
    	//使用peek返回头部元素
    	list.add(root);
    	while(!list.isEmpty()) {
    		treenode temp=list.poll();
    		System.out.print(temp.value+" ");
    		if(temp.leftchild!=null)//一定要有判断条件,否则如果把null加入队列会抛出NullPointerException异常
    			list.add(temp.leftchild);
    		if(temp.rightchild!=null)
    			list.add(temp.rightchild);
    	}
    }

一个重要的问题:如何根据前序遍历和中序遍历,以及中序遍历和后序遍历,唯一的重建一棵二叉树(前+后不能确定)?这里就需要用到递归的算法设计。

3、二叉树其他的方法:创建树、返回树的高度、返回节点个数。创建树用的是类似层次遍历的队列,后两者都是用了递归的方法。

    public static treenode createbinarytree(){//创建树
    	LinkedList<treenode> createlist=new LinkedList<treenode>();
    	Scanner sc=new Scanner(System.in);
    	System.out.println("Input root node:");
    	treenode root=new treenode(sc.next());
    	createlist.add(root);
    	while(!createlist.isEmpty()){
    		treenode temp=createlist.poll();
    		System.out.println("Input left and right child of node "+temp.value+" :(@ is null node)");
    		String leftchild=sc.next();
    		String rightchild=sc.next();
    		if(!leftchild.equals("@"))
    			createlist.add(temp.leftchild=new treenode(leftchild));
    		if(!rightchild.equals("@"))
    			createlist.add(temp.rightchild=new treenode(rightchild));
    	}
    	System.out.println("Create complete");
    	return root;
    }

    public static int depth(treenode root) {//返回树的高度
    	if(root==null)
    		return 0;
    	else {
    		int lrdepth=depth(root.leftchild);
    		int ridepth=depth(root.rightchild);
    		return 1+((lrdepth>ridepth?lrdepth:ridepth));
    	}
    }
    public static int nodenum(treenode root) {//返回节点个数
    	if(root==null)
    		return 0;
    	return 1+nodenum(root.leftchild)+nodenum(root.rightchild);
    }

二、二叉搜索树(查找树)

他的特点是若左子树不空,左子树中所有元素的键值都比根节点小;若右子树不空,右子树中所有元素的键值都比根节点大;他的左右子树也是二叉查找树。中序遍历一棵二叉查找树就是按照键值从小到大排列。基本操作有查找、插入、删除。这些都是属于二叉搜索树的操作,因为对于普通二叉树,他的数据组织是没有规律的,也谈不上这些操作了。

(1)查找,使用递归不断查找其左子树和右子树即可

    public static treenode find(treenode root, String key){
    	if(root==null)
    		return root;
    	if(root.value.equals(key))
    		return root;
    	if(root.value.compareTo(key)>0)
    		return find(root.leftchild, key);
    	else
    		return find(root.rightchild, key);
    }

(2)插入,先查找,若找到,不插入。若没找到,最后的位置就是插入的位置。

    public static treenode insert(treenode root, String value){//注意,这里由于java传值的特性,比较麻烦!
    	treenode newroot=root;
    	if(root==null){
        	treenode newnode=new treenode(value);
    		newroot=newnode;
    		return newroot;
    	}
    	if(root.value.equals(value)){
    		System.out.println("Node has already exist");
    		return null;
    	}
    	if(root.value.compareTo(value)>0&&root.leftchild!=null)
    		return insert(root.leftchild, value);
    	if(root.value.compareTo(value)<0&&root.rightchild!=null)
    		return insert(root.rightchild, value);
    	if(root.value.compareTo(value)>0&&root.leftchild==null){
    		treenode newnode=new treenode(value);
    		root.leftchild=newnode;
    		return newroot;
    	}
    	if(root.value.compareTo(value)<0&&root.rightchild==null){
    		treenode newnode=new treenode(value);
    		root.rightchild=newnode;
    		return newroot;
    	}
		return null;
    }

(3)删除

若删除的那个节点是叶子,立刻删除。

若被删除的节点只有一个儿子,将父节点连到它的边改为连到他儿子。

最复杂的是删除有两个儿子的节点。此时需要找到左子树中最大的节点或者右子树中最小的节点,然后将那个节点的值放进被删节点的位置,然后把那个节点删除。那个节点必然是要么是叶子,要么只有一个儿子。


    public static void delete(treenode root, String value){//这里假设要删除的value一定存在于这个树中
    	if(root==null)
    		return;
    	treenode currentnode=find(root,value);
    	if(currentnode.leftchild==null&¤tnode.rightchild==null){//第一种情况:要删除的是叶子结点
    		treenode parent=findparent(root, currentnode.value);
    		if(parent.leftchild.value.equals(currentnode.value))
    			parent.leftchild=null;
    		else {
				parent.rightchild=null;
			}
    		return;
    	}
    	if(currentnode.leftchild==null&¤tnode.rightchild!=null){//第二种情况:要删除的节点有一个孩子
    		treenode parent=findparent(root, currentnode.value);
    		if(parent.leftchild.value.equals(currentnode.value))
    			parent.leftchild=currentnode.rightchild;
    		else {
				parent.rightchild=currentnode.rightchild;
			}
    		return;
    	}
    	if(currentnode.leftchild!=null&¤tnode.rightchild==null){
    		treenode parent=findparent(root, currentnode.value);
    		if(parent.leftchild.value.equals(currentnode.value))
    			parent.leftchild=currentnode.leftchild;
    		else {
				parent.rightchild=currentnode.leftchild;
			}
    		return;
    	}
    	if(currentnode.leftchild!=null&¤tnode.rightchild!=null){//第三种情况:要删除的节点右两个孩子。我们处理办法是找到左子树中最大的节点
    		treenode temp=currentnode.leftchild;
    		treenode wantdelete=currentnode;
    		int flag=0;//用于判断到底左子树的最大节点是不是左子树的根
    		while(temp!=null&&temp.rightchild!=null){
    			currentnode=temp;
    			temp=temp.rightchild;
    			flag=1;
    		}
    		wantdelete.value=temp.value;
    		//要删除的这个temp只能是第一种和第二种情况
    		//此时currentnode是要删除的那个节点的父亲
    		if(temp.leftchild==null&&temp.rightchild==null)//第一种情况
    			if(flag==0)
    				currentnode.leftchild=null;
    			else {
					currentnode.rightchild=null;
				}
    		else if(temp.leftchild!=null&&temp.rightchild==null)//第二种情况
    			if(flag==0)
    				currentnode.leftchild=temp.leftchild;
    			else {
					currentnode.rightchild=temp.leftchild;
				}
    		else
    			if(flag==0)
    				currentnode.leftchild=temp.rightchild;
    			else {
					currentnode.rightchild=temp.rightchild;
				}
    	}
    }
    public static treenode findparent(treenode root, String key){
    	if(root==null)
    		return null;
    	if(root.leftchild.value.equals(key)||root.rightchild.value.equals(key))
    		return root;
    	else if(root.value.compareTo(key)>0)
    		return findparent(root.leftchild, key);
    	else {
			return findparent(root.rightchild, key);
		}
    }

三、AVL树

我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。因此,我们需要二叉平衡树。

二叉平衡树是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。常用的二叉平衡树有AVL树和红黑树。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log(n)),大大降低了操作的时间复杂度。

AVL是最先发明的自平衡二叉查找树算法。AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。在AVL中任何节点的两个儿子子树的高度最大差别为一,所以它也被称为高度平衡树,n个结点的AVL树最大深度约1.44log2n。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树的基本操作有旋转、插入和删除。它的查找和普通二叉树是完全一样的。

(1)旋转和插入

假设用一般的二叉树插入算法插入了一个新的节点。这个时候,平衡性被破坏了。那个,这个时候肯定有一个节点,该节点下面的所有节点平衡性都没被破坏,而这个节点的平衡性却被破坏了。设这个节点为A。一棵AVL树每个节点一般要维护平衡因子的属性,该属性是这个节点左子树和右子树的高度差。例如:


在这里,可以明显看到,原本B的平衡因子是0,而A的平衡因子是1,因为A的左子树高度是h,右子树的高度是h-1。而在B的左子树插入了一个新节点后,A的平衡因子变成2了,因此就失衡了,因此就需要对A进行调整。调整对于四种不同的插入情况,有四种不同的处理,都涉及到旋转操作。我们先来看看什么是旋转,旋转包括左旋和右旋。

左旋:对某个节点左旋,也就是把该节点变成该节点的右子节点的左节点。右旋同理。



(图来自:https://blog.csdn.net/tuhuolong/article/details/6844850)

了解了旋转操作后,我们再回到AVL树的平衡性问题。为了处理节点A的失衡,有以下四种情况:

插入方式    破坏平衡原因                                                                旋转方式

LL           在A的左子树根节点的左子树上插入节点而平衡被破坏          对A右旋

RR          在A的左子树根节点的左子树上插入节点而平衡被破坏          对A左旋

LR           在A的左子树根节点的左子树上插入节点而平衡被破坏          先左旋后右旋

RL           在A的左子树根节点的左子树上插入节点而平衡被破坏          先右旋后左旋

LL和RR情况:


我们以LL情况为例。事实上,旋转操作的本质是在不改变节点的大小关系的情况下,改变整棵树的高度。哪边高度高了,也就是太长了,就把哪边往上旋转。可以看到,在LL情况下,由于左边的高度高了,因此对A进行右旋,也就是把B往上旋转了。此时,A就变得平衡了。其实对于RR也同理。

LR和RL情况:


对于LR就不一样了。可以看到,为了令树达到平衡,如果只对A进行左旋,那更不平衡了,不可能。对A进行右旋,这样B也变得不平衡了。这个时候,需要对B进行一次左旋,然后再对A进行一次右旋即可。RL不再赘述。



(2)删除(参考https://blog.csdn.net/ivan_zgj/article/details/51495926)

删除结点可能会导致该结点所在的树高发生变化,从而引起树的不平衡。AVL树删除结点先执行和普通二叉搜索树一样的删除过程,然后再从最开始因为删除结点而树高发生变化的结点向上回溯调整树高。该操作的关键在于如何确定“最先发生树高变化的结点”。因为普通二叉搜索树的删除结点有3种情况,所以AVL树对“最先发生树高变化的结点”的确定也有3种情况。假设被删除的结点为x。

1). x是叶子结点,“最先发生树高变化的结点”为x的父结点;

2). x只有一个孩子,“最先发生树高变化的结点”为x的父结点;

3). x有两个孩子,则“最先发生树高变化的结点”为x的后驱的父结点。

在确定了“最先发生树高变化的结点”之后,就需要从该结点出发向上回溯,与插入结点一样,对沿路经过的结点按需进行旋转。

四、红黑树

红黑树是一种被广泛使用的二叉平衡树,包括Java集合中的TreeMap和TreeSet。不过红黑树的操作也相当复杂。

很推荐(http://www.cnblogs.com/skywang12345/p/3245399.html)

五、外存储中的多叉树(B树(B-树)、B+树、B*树)

在外存中(如磁盘),为什么需要使用多叉树来存储数据呢?因为查找所需的时间与树的深度相关。二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。采用多叉树结构减少树的深度,从而达到有效避免磁盘过于频繁的查找存取操作,从而有效提高查找效率。而降低深度最直观方法的就是增加树的分支。一棵完全M叉树的高度约为logMN。与平衡二叉树同理,为了不让M叉树退化为链表(或二叉树),需要寻找平衡的M叉查找树。

1、B树

B树是平衡的M叉查找树。定义如下:


B树的度的概念,B树的度即为每个节点的儿子树的最小值,设为t,t=M/2向上取整。在这里,每个节点就是每个磁盘块。因此,如果M比较大,在磁盘中的读取速度就很快。可以发现,对于每个磁盘块中,有一个数据n,表示这个磁盘块中有多少个关键字。然后,对于i从1到n,有Ki和Ri两个个数据项。Ki是该节点存储的关键字。Ri是Ki关键字值对应的某个磁盘文件在磁盘中的地址,通过这个Ri可以访问该磁盘文件。而对于i从0到n,一共有n+1个Ai。这个Ai是指向下一个磁盘块的指针。A0,A1,...,An分别指向0~K1,K1~K2,...,Kn-1~Kn,>Kn的范围的磁盘块。如图所示:


(https://blog.csdn.net/guoziqing506/article/details/64122287)

在B树中的操作有:

(1)查找

跟一般的查找树没有太大区别。

(2)插入

要插入的新节点一般都在最后一层的叶子结点。从上面B树的性质我们知道,一个节点,最多有M个孩子,也就是说这个节点最多有M-1个数据项。运用这个性质,插入分为如下两个情况:

如果该结点的关键字个数没有到达M-1个,那么直接插入即可;

如果该结点的关键字个数已经到达了M-1个,那么根据B树的性质显然无法满足,需要将其进行分裂。分裂的规则是该结点将一半数量的关键字元素分裂出来,形成一个新的兄弟节点。然后将中间的某个关键字进行提升,加入到父亲结点中。要注意,这里虽然分裂出了一个新节点,但是由于中间有个关键字进行了提升进入了父节点中,因此父节点会多了一个指针,因此没有违反规则3。但是这又可能存在父亲结点也满员的情况,则不得不向上进行回溯,甚至是要对根结点进行分裂,那么整棵树都加了一层。


(https://www.cnblogs.com/George1994/p/7008732.html)

(3)删除

有以下几种情况:

如果要删除的关键字在叶子节点上,且叶子节点关键字数大于t,则可以直接删除。因为这样没有违反规则;

如果要删除的关键字在内节点中,由于每一个关键字左右都有一个子树,删除后会违背B树的定义,因此必须进行调整。调整无非是进行合并等操作。 具体方法就不进行赘述了。

2、B+树

事实上,在实际中应用如文件系统和数据库索引,很少用到B树甚至红黑树,更多是使用B树的变体B+树。这是有以下几个原因:

(1)对于IO来说:可以看到,B树的叶子结点中存在着数据的关键字、指向关键字具体信息的指针以及指向子树的指针,相比较于B+树,B+树的内部节点并没有指向关键字具体信息的指针,只有数据的关键字和指向孩子的指针,也就是仅包含索引信息。因此其内部节点相对B树更小。假设一个B树和一个B+数,有同样数量的关键字,而B树的节点显然比B+树大,也就意味着需要把内部节点读入内存时,B树的所需要查找时间比B+树更多,相对IO读写速度就更慢了;

(2)对于查询性能来说:对于B+树,由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。而B+树只需要找到包含该关键字的节点,就可以找到该文件内容了。虽然看起来,B树对查询的性能比B+树优秀,但是B+树的好处是所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

(3)对于遍历来说:对于数据库的应用,遍历是一个很重要的方面。B树并没有解决元素遍历的效率低下的问题, B树则需要对整个树的每一层遍历,需要更多的磁盘读写,这非常影响性能。而B+树由于底部的叶子结点是链表形式, 因此也可以实现更方便的顺序遍历。

以一个M阶B+树来说,在定义上来说,它与B树的区别在于:

1.有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。

2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。



(https://blog.csdn.net/whoamiyang/article/details/51926985)

3、B*树

是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。B*树分配新结点的概率比B+树要低,空间使用率更高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值