二叉树查找 java_Java实现二叉树(一):二叉查找树的实现

本文介绍了二叉树的基本概念,特别是二叉查找树的特性,包括中序遍历、先序遍历和后序遍历。通过示例展示了如何在Java中实现二叉查找树的插入、删除、查找和更新操作,以及中序遍历的递归和非递归方法。文章还讨论了二叉查找树的优缺点,并提供了完整的Java代码实现。
摘要由CSDN通过智能技术生成

Java实现二叉树(一):二叉查找树的实现

数据结构+算法=程序,这是共识,是真理,还是学生时代卷子中的考点。但大多数程序员往往缺乏数据结构和算法的知识,或是根本没有学过,或是学过,但在工作时频繁地与业务逻辑打交道,也就逐渐忘记了有这么一回事。

话不多说,直接开始吧,本文将介绍二叉树的基本概念,以及平衡二叉树增删改查节点的实现,由于网上关于数据结构的资料,C++相对居多,而本身作为一个Javaer,将会重新用Java实现一个平衡二叉树,仅供参考,欢迎纠正。

一、二叉树的基本概念

定义:二叉树,是一种特殊的树,二叉树的任意一个节点的度都不大于2,不包含度的节点称之为叶子。

分类:二叉树又有细分,满二叉树,以及完全二叉树,等等等等,本文就不一一介绍了,本文暂且只介绍二叉查找树,也是最常见的一种数据类型。

遍历方式:二叉树的遍历方式有三种,中序遍历,先序遍历,后序遍历:

中序遍历:先遍历左子树,再遍历父亲节点,最后遍历右子树;

先序遍历:先遍历父亲节点,再遍历左子树,最后遍历右子树;

后序遍历:先遍历父亲节点,再遍历右子树,最后遍历左子树;

光说显得乏味,我们列一道经典的二叉树题目吧,已知一棵二叉树,如果先序遍历的节点顺序是:ADCEFGHB ,中序遍历是:CDFEGHAB ,则后序遍历结果为:

这道题需要实现一个二叉树重构,个人的解题思路是这样的--首先,先序遍历最早遍历父亲节点,也就意味着A是根节点,而中序遍历最晚遍历右子树,所以我们可以推测出A的右边也就是B,是A的右子树。于是我们将两个去除AB,也就是DCEFGH,CDFEGH,推测父节点左子树的结果,同样的方法我们可以确定D是左子树的父亲节点,C是左子树的唯一一个左子树,从而继续去除。

说着好像十分复杂,但是其实就是一个递归的思想,不断的确认父节点,以及拆分左子树,和右子树,完成树的重构,最后得到的树是下图:

0616006a463233d6aa34b2b089a8b817.png

如此也就不难得出该树的后序遍历结果为:CFHGEDBA。

二、二叉查找树的基本概念

基本概念:二叉查找树是一种特殊的二叉树,要求左子树的全部节点小于父亲节点,右子树的全部节点大于父亲节点,同时,左子树和右子树也为二叉查找树,中序遍历一个二叉查找树,会得到一个有序的元素集合。

优点:二叉查找树拥有良好的查找性能,查找次数最多为树的深度,这也就意味着二叉查找树的查找效率取决于树的深度,而满二叉树的深度为log(N+1),所以此时他的查找时间复杂度为O(logN)。

缺点:缺点在优点中说的十分明确了,二叉查找树在极端的情况下会大大降低他的查找性能,比如说,顺序插入节点时,二叉树就会像一个链表,查找的时间复杂度将为O(N)。

构建一个深度合理的二叉查找树本文暂不做介绍,之后的文章会讲述平衡二叉树的概念,可以关注。

三、二叉查找树的基本操作和Java实现

咳咳,注意,笔者要点题了。

且不说二叉树,任何的数据结构都离不开增删改查四个步骤,就由增删改查分块讲解二叉查找树吧。

Insert

要求:二叉查找树插入时,要满足二叉查找树的特性,左小右大。

实现思路:递归地去遍历一颗树,如果大于节点就遍历节点的右子树,如果小于节点就遍历节点的左子树,当节点为空时插入。

代码://以查找二叉树的规则插入,小的在左,大的在右

public static void insert(Tree tree,int value)

{

if(tree.getValue() == null)

{

tree.setValue(value);

}

else if(tree.getValue() < value)

{

if(tree.getrChild()!=null)

{

insert(tree.getrChild(),value);

}

else {

tree.setrChild(new Tree());

insert(tree.getrChild(),value);

}

}

else if(tree.getValue() > value)

{

if(tree.getlChild()!=null)

{

insert(tree.getlChild(),value);

}

else {

tree.setlChild(new Tree());

insert(tree.getlChild(),value);

}

}

}

Delete

要求:删除的要求相对复杂,因为,删除之后,还是必须要保持二叉查找树的特性。

实现思路:先找到要删除的节点,这时会有三种情况:

删除节点为(左或者右)叶子--此时直接将节点的父亲节点的(左或者右)节点置为空;

删除节点只包含一个(左或者右)子树--此时,判断删除节点是父节点的左子树,还是右子树,用删除节点的唯一那一个“儿子”去替换自己;

删除节点既有左子树又有右子树--此时,可以找到右子树中最小的节点,替换删除节点,再递归地删除了它(做法不唯一,同样也可以用左子树中最大的节点去替换删除节点);

代码(有些长...):public static void delete(Tree tree,int value)

{

//查询到的节点的父节点

Tree parent = null;

//查询到的节点

Tree searchTree = tree;

//当节点不为空的时候开始循环

while(searchTree != null)

{

//如果节点的值等于查询值,跳出循环,之后的逻辑就是一个树查找的逻辑

if(searchTree.getValue()==value)

{

break;

}

else if(searchTree.getValue() > value)

{

parent = searchTree;

searchTree = searchTree.getlChild();

}

else if(searchTree.getValue() < value)

{

parent = searchTree;

searchTree = searchTree.getrChild();

}

}

//查询到的节点是父节点的左节点与否

boolean parentLeftFlag = false;

//查询到的节点的父节点是否存在

boolean hasParent = true;

//查询到的节点的左子树存在与否

boolean leftFlag = false;

//查询到的节点的右子树存在与否

boolean rightFlag = false;

if(parent == null)

{

hasParent = false;

}

else if(parent.getrChild() == searchTree)

{

parentLeftFlag = false;

}

else{

parentLeftFlag = true;

}

if(searchTree == null)

{

return;

}

if(searchTree.getlChild() != null){

leftFlag = true;

}

if(searchTree.getrChild() != null){

rightFlag = true;

}

if(!leftFlag && !rightFlag)

{

if(hasParent)

{

if(parentLeftFlag)

{

parent.setlChild(null);

}

else{

parent.setrChild(null);

}

}

else{

searchTree.setValue(null);

}

}

else if(!leftFlag || !rightFlag)

{

if(hasParent)

{

if(parentLeftFlag)

{

if(leftFlag)

{

parent.setlChild(searchTree.getlChild());

}

else {

parent.setlChild(searchTree.getrChild());

}

}

else {

if(leftFlag)

{

parent.setrChild(searchTree.getlChild());

}

else {

parent.setrChild(searchTree.getrChild());

}

}

}

else

{

if(leftFlag){

searchTree.setValue(searchTree.getlChild().getValue());

searchTree.setrChild(searchTree.getlChild().getrChild());

searchTree.setlChild(searchTree.getlChild().getlChild());

}

else

{

searchTree.setValue(searchTree.getrChild().getValue());

searchTree.setlChild(searchTree.getrChild().getlChild());

searchTree.setrChild(searchTree.getrChild().getrChild());

}

}

}

else if(leftFlag && rightFlag){

Tree minTree = searchTree.getrChild();

while (minTree.getlChild() != null)

{

minTree = minTree.getlChild();

}

Integer minTreeValue = minTree.getValue();

delete(searchTree,minTreeValue);

searchTree.setValue(minTreeValue);

}

}

Update

要求:修改的话其实就是查找并修改,并无太大要求。

实现思路:当大于节点查找右子树,当小于节点查找左子树,当等于节点,返回节点。

代码(此处仅仅放出查找的代码):public static Tree findNode(Tree tree,int value)

{

if(tree.getValue() == value)

{

return tree;

}

else if(tree.getValue() < value){

if(tree.getrChild() != null)

{

return findNode(tree.getrChild(),value);

}

else {

return null;

}

}

else if(tree.getValue() > value){

if(tree.getlChild() != null)

{

return findNode(tree.getlChild(),value);

}

else {

return null;

}

}

return null;

}

Read

上文已经把查找的代码和实现思路写出来了,这里写一下中序遍历的两种实现方式和思路吧(先序遍历和后序遍历大同小异,由于是二叉查找树,只实现中序遍历)。

第一种方式--递归方式:

实现思路:先递归的遍历左子树,再输出父节点的值,最后再递归地遍历右子树,跳出递归条件:当左子树或者右子树不为空时。

代码://中序遍历

public static void middleSearch(Tree tree)

{

if(tree.getlChild()!=null)

{middleSearch(tree.getlChild());}

System.out.print(tree.getValue());

if(tree.getrChild()!=null)

{middleSearch(tree.getrChild());}

}

第二种方式--非递归方式:

实现思路:手动的建立一个栈,循环地去轮询左子树,直到左子树为空,将还未输出的父亲的节点依次压入栈中,当轮询到空树时,开始弹出栈内的节点,输出节点的值,并将节点的右子树作为轮询的值压入栈。

代码://使用非递归方式中序遍历

public static void midddleSearchUseStack(Tree tree)

{

Stack stack = new Stack<>();

Tree rollTree = tree;

while(rollTree != null || stack.size() != 0)

{

if(rollTree != null)

{

stack.push(rollTree);

rollTree = rollTree.getlChild();

}else

{

rollTree = stack.pop();

System.out.print(rollTree.getValue());

rollTree = rollTree.getrChild();

}

}

}

总结:其实两种方式大致无差别,其实递归也是使用方法栈的方式将每一个方法压入栈中,时间复杂度两者没有差异。但是考虑到方法栈的深度限制,和方法栈更多的内存开销(方法栈会将局部变量等信息压入栈中),还有方法调用的开销,显然非递归方式效率是更加高的。但是递归方式有个优点,可读性高,简单,易实现。

好了,就到这,本文的Java代码并没有全量的放在博客中,笔者将实现的全部代码上传到了GitHub上,需要的同学可以上去下载(记得给星哦)。

GitHub下载地址:https://github.com/liufangqi/treeTestRealOne

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值