首先说明:来看这一节的我都默认你有一定数据结构和java基础了,对于程序实现的细节我就不做过多的解释了。如果链表堆栈都还不能理解,泛型内部类这些java基础也不大懂,那这节也不太可能听懂。
递归是能看懂二叉树的前置条件,不懂的去看我这篇
番外:递归算法
引入:为什么我们需要树这种结构
对于大量的输入数据,链表的线性访问时间太慢,不宜使用。但是数组的插入和删除最坏时间复杂度都是O(N),也只能在部分场景下使用。能不能有种数据结构能综合下?于是树出现了,它保证了在大部分操作的运行时间复杂度为O(logN),树的应用比其他的基本数据结构更加广泛,所以学好还是有必要的。不废话了。
什么是树,什么是根/叶,树的深度。这些基础概念我就不讲了,百度自己了解以下,我们还是以代码为主,实现一个二叉查找树。
简介:
我们要写二叉查找树,先要了解它是什么,才能开始写,那这里就简单介绍下吧。首先,二叉树是一棵树(废话),其中每个结点都不能有多于两个的儿子。
上面就是一颗普通的二叉树,可是我们会发现,这种并没啥用,因为它的数据分布是随意的,我们想要找到一个数完全靠运气,这是我们不能接受的。那么能把这颗树改成什么样呢?
首先让我们回忆一种游戏。我写一个0-100以内的数字,你来猜,你会使用什么策略?二分查找!很容易理解对不对。那二分查找的思想完全可以运用在我们的二叉树上,由此演变的树称之为二叉查找树。
那二叉查找树啥意思?就是以根的数据为基准,小于父结点的数据放到左子树上,大于父节点的数据放到右子树上。对于所有结点都是如此。那么我们来看下面两颗树。
你能判断哪个是二叉查找树吗?
ok,既然知道是怎么一回事了,我们程序也就好写了,终于到正题了。
正题
二叉查找树要求所有项都能够进行排序,那么我们就要继承Comparable接口,由于我不想把data域属性定死,所以使用了泛型。
由于每个结点都可以有两个孩子,所以Node中要有3个参数,data,left,right。
话不多说,先写着。
package 查找二叉树;
/**
*
* @author xuxiao
*
* @param
*/
public class BinarySearchTree >{
private BinaryNode root;
private int size;
private static class BinaryNode{
public BinaryNode(T theElement, BinaryNodelt, BinaryNodert) {
element = theElement;
left = lt;
right = rt;
}
T element;
BinaryNode left;
BinaryNode right;
}
}
简单说下吧,我们首先能初始化树和清空树吧,就叫makeEmpty,然后给个判断当前是否为空树的,isEmpty,根据实例来判断是否在树里的contains,找到树中存储的最大最小值,
findMax,findMin。最后来个增删,insert和remove,行了。
我们开始写,第一步写构造器,isEmpty,getSize,makeEmpty。
很容易吧,直接给代码
public BinarySearchTree() {
root=null;
size=0;
}
public boolean isEmpty() {
return getSize()==0;
}
public int getSize() {
return size;
}
public void makeEmpty() {
root=null;
size=0;
}
下面写我们的findMin和findMax,我们根据二叉查找树的特性知道,它的最大值就在树的最右边,最小值在最左边。
那么我们的程序就这样写
public BinaryNode findMin(){
return findMin(root);
}
private BinaryNode findMin(BinaryNode node){
if(node.left==null)
return node;//基准线
return findMin(node.left);//推进
}
public BinaryNode findMax(){
return findMax(root);
}
private BinaryNode findMax(BinaryNode node){
if(node.right==null)
return node;//基准线
return findMax(node.right);//推进
}
找最大最小值都用到了递归,这个没啥好说的,找最小值就是一直往左找,找到左子树为空,那么返回自身。右子树相反。
基准线都是单基准,就看其左/右子树为不为空。
ps:曾经我有这样的疑问,难道这种写法不是双基准吗?根为空返回根,根不为空在去找。那其实是一种误区,把问题细化了,在递归中能合并的基准线一定要合并。
针对我以前的疑问,我现在给出这样的回答,根为不为空并不影响它找最左/右结点,root为空,那么root本身就是最左和最右结点嘛。何必在进递归前作判断。
接下来写contain,依然用到递归,我们知道二叉查找树是小值在左大值在右,我们从根入手,每次选方向时先比较,最后总能找到合适的位置递归。基准线就是我们找到这个结点了,返回true,最后没有子节点了也没找到,返回false
所以contain是一个双基准递归。
那么这个api,我们这样写
public boolean contain(T x) {
return contain(root,x);
}
private boolean contain(BinaryNode node,T x) {
if(node==null)
return false;//基准线1
int result =x.compareTo(node.data);
if(result<0)
return contain(node.left,x);//推进
if(result>0)
return contain(node.right,x);//推进
else
return true;//基准线2
}
下面再来写insert,分析一下,insert其实和contain是一样的,contain是按照合适的位置去寻找结点,insert是给新结点找合适的位置。那么我们的递归大致也是相同的,不同的是,我们的insert是不存在找不到合适的位置的,所以我们的递归是单基准递归..吗?错了,是双基准,还有一种情况是我们插入的值,在树中已经存在了,那么我们就不需要做新建结点的操作了,空执行返回当前结点即可。
所以总结起来基准线是:已有该结点,空执行,没有该结点,新建结点
public void insert(T x) {
root=insert(root,x);
size++;
}
private BinaryNode insert(BinaryNode node,T x) {
if(node==null)
return new BinaryNode(x,null,null);//基准1
int result = x.compareTo(node.data);
if(result<0)
return insert(node.left,x);//推进
if(result>0)
return insert(node.right,x);//推进
else
;
return node;//基准2
}
ps:在最开始的时候,我认为insert应该有三条基准线,没根,有根有重复,有根无重复(还是空树让我多想了),后来发现,空树并不影响插入操作,因为空树是合并到无重复创建新结点上去了。
最复杂的remove,因为remove的基准线在我最开始分析的时候有5条。删除的结点无子结点/有左子节点/有右子节点/有两个子节点/没找到要删除的结点。
1.对于无子节点的,我们直接删除即可,即将其父结点的指向设为空。
2.对于有左/右子节点的,我们将其子节点上提,即用子节点代替删除结点即可。
3.对于没找到结点的,我们空执行后返回当前结点即可
4.对于两个子节点的,我们采用这种策略,用其右子树的最小结点代替被删除的结点。并删除右子树上的最小结点。
但是我们还可以合并基准线!因为不管是有一个结点还是没有结点,我们发现需要返回的都是它的一个子结点。有左子结点返回之,没有左结点返回右子结点(不管它有没有子结点)
那么我们的判断可以这样写
node=node.left!=null?node.left:node.right;
那么我们的基准线就可以合并为3条(其实你看一个递归方法的基准线就看它写几个带该有返回值的return即可)
public void remove(T x) {
root=remove(root,x);
size--;
}
private BinaryNode remove(BinaryNodenode,T x){
if(node==null)
return node;//基准1
int result=x.compareTo(node.data);
if(result<0)
return remove(node.left,x);//这可不是基准,这是推进
else if(result>0)
return remove(node.right,x);//同上
else if(node.left!=null&&node.right!=null) {//基准2
node.data=findMin(node.right).data;
remove(node.right,node.data);
}
else
node=node.left!=null?node.left:node.right;
return node; //基准3
}
我下面就直接给出二叉查找树的所有程序了。
package 二叉搜索树;
/**
*
* @author xuxiao
*
* @param
*/
public class BinarySearchTree> {
private BinaryNode root;
private int size;
private static class BinaryNode{
public T data;
public BinaryNode left;
public BinaryNode right;
public BinaryNode(T data, BinaryNode left, BinaryNode right) {
super();
this.data = data;
this.left = left;
this.right = right;
}
}
public BinarySearchTree() {
root=null;
size=0;
}
public boolean isEmpty() {
return getSize()==0;
}
public int getSize() {
return size;
}
public void makeEmpty() {
root=null;
size=0;
}
public BinaryNode findMin(){
return findMin(root);
}
private BinaryNode findMin(BinaryNode node){
if(node.left==null)
return node;
return findMin(node.left);
}
public BinaryNode findMax(){
return findMax(root);
}
private BinaryNode findMax(BinaryNode node){
if(node.right==null)
return node;
return findMax(node.right);
}
public void insert(T x) {
root=insert(root,x);
size++;
}
/*
* 视野不要停留在没有根结点就创建新的根上,而是思考问题
* 我的插入操作只有两种可能,要么需要创建新结点,要么不需要
* 所以我的基准线有二:要么node=new BinaryNode
* 要么返回node本身,所以不要以没有根则创建根分析问题。这样的话就把问题分割了
* 基准线就变成了3条,没根,有根有重复,有根无重复
* 那就没办法解释程序只写了两个基准线也能完成3种可能的递归
* 所以递归分析需要有合并基准能力。
*/
private BinaryNode insert(BinaryNode node,T x) {
if(node==null)
return new BinaryNode(x,null,null);
int result = x.compareTo(node.data);
if(result<0)
return insert(node.left,x);
if(result>0)
return insert(node.right,x);
else
;
return node;
}
public boolean contain(T x) {
return contain(root,x);
}
private boolean contain(BinaryNode node,T x) {
if(node==null)
return false;
int result =x.compareTo(node.data);
if(result<0)
return contain(node.left,x);
if(result>0)
return contain(node.right,x);
else
return true;
}
public void remove(T x) {
root=remove(root,x);
size--;
}
private BinaryNode remove(BinaryNodenode,T x){
if(node==null)
return node;
int result=x.compareTo(node.data);
if(result<0)
return remove(node.left,x);
else if(result>0)
return remove(node.right,x);
else if(node.left!=null&&node.right!=null) {
node.data=findMin(node.right).data;
remove(node.right,node.data);
}
else
node=node.left!=null?node.left:node.right;
return node;
}
}
补充:我们知道在树的所有操作中理论的平均时间复杂度是O(logN),但这种推断是建立在这颗树是一颗看起来正常的树,不会左倾或者右倾,甚至在极端情况下,一颗二叉查找树是有可能退化为一张链表的,所以为了保证二叉查找树能变得比较正常,所以出现的AVL树。
但这不是我想说的,我在这里想说的是,在实际的实践中,不管查找树一开始正不正常,只要对查找树做过大量操作之后,树都会变得左倾。其原因是因为删除操作中,我们选取了右子树中最小结点代替删除结点,久而久之,结点将汇聚在左子树上。
可以想到的解决方案是,我们可以随机选用右子树上最小结点和左子树上最大结点代替删除结点。这样我我们就可以在理论上实现操作不会改变树的形态。
但是呢,我说但是,如果这颗树一开始就是倾斜的树呢,其实对于查找树,造成倾斜的原因只有一个,即根的选取有问题,比如我要插入的都是正数,但我的根是个0,那我的树将毫无悬念的变成一颗右倾的树。倾斜的树造成树的深度比理想值深的多,进而影响实际的操作时间复杂度。所以AVL树的存在是非常有必要的。