数据结构+算法=程序,这是共识,是真理,还是学生时代卷子中的考点。但大多数程序员往往缺乏数据结构和算法的知识,或是根本没有学过,或是学过,但在工作时频繁地与业务逻辑打交道,也就逐渐忘记了有这么一回事。
话不多说,直接开始吧,本文将介绍二叉树的基本概念,以及平衡二叉树增删改查节点的实现,由于网上关于数据结构的资料,C++相对居多,而本身作为一个Javaer,将会重新用Java实现一个平衡二叉树,仅供参考,欢迎纠正。
定义:二叉树,是一种特殊的树,二叉树的任意一个节点的度都不大于2,不包含度的节点称之为叶子。
分类:二叉树又有细分,满二叉树,以及完全二叉树,等等等等,本文就不一一介绍了,本文暂且只介绍二叉查找树,也是最常见的一种数据类型。
遍历方式:二叉树的遍历方式有三种,中序遍历,先序遍历,后序遍历:
光说显得乏味,我们列一道经典的二叉树题目吧,已知一棵二叉树,如果先序遍历的节点顺序是:ADCEFGHB ,中序遍历是:CDFEGHAB ,则后序遍历结果为:
这道题需要实现一个二叉树重构,个人的解题思路是这样的--首先,先序遍历最早遍历父亲节点,也就意味着A是根节点,而中序遍历最晚遍历右子树,所以我们可以推测出A的右边也就是B,是A的右子树。于是我们将两个去除AB,也就是DCEFGH,CDFEGH,推测父节点左子树的结果,同样的方法我们可以确定D是左子树的父亲节点,C是左子树的唯一一个左子树,从而继续去除。
说着好像十分复杂,但是其实就是一个递归的思想,不断的确认父节点,以及拆分左子树,和右子树,完成树的重构,最后得到的树是下图:
如此也就不难得出该树的后序遍历结果为:CFHGEDBA。
基本概念:二叉查找树是一种特殊的二叉树,要求左子树的全部节点小于父亲节点,右子树的全部节点大于父亲节点,同时,左子树和右子树也为二叉查找树,中序遍历一个二叉查找树,会得到一个有序的元素集合。
优点:二叉查找树拥有良好的查找性能,查找次数最多为树的深度,这也就意味着二叉查找树的查找效率取决于树的深度,而满二叉树的深度为log(N+1),所以此时他的查找时间复杂度为O(logN)。
缺点:缺点在优点中说的十分明确了,二叉查找树在极端的情况下会大大降低他的查找性能,比如说,顺序插入节点时,二叉树就会像一个链表,查找的时间复杂度将为O(N)。
构建一个深度合理的二叉查找树本文暂不做介绍,之后的文章会讲述平衡二叉树的概念,可以关注。
咳咳,注意,笔者要点题了。
且不说二叉树,任何的数据结构都离不开增删改查四个步骤,就由增删改查分块讲解二叉查找树吧。
要求:二叉查找树插入时,要满足二叉查找树的特性,左小右大。
实现思路:递归地去遍历一颗树,如果大于节点就遍历节点的右子树,如果小于节点就遍历节点的左子树,当节点为空时插入。
代码:
//以查找二叉树的规则插入,小的在左,大的在右
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);
}
}
}
要求:删除的要求相对复杂,因为,删除之后,还是必须要保持二叉查找树的特性。
实现思路:先找到要删除的节点,这时会有三种情况:
代码(有些长...):
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);
}
}
要求:修改的话其实就是查找并修改,并无太大要求。
实现思路:当大于节点查找右子树,当小于节点查找左子树,当等于节点,返回节点。
代码(此处仅仅放出查找的代码):
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;
}
上文已经把查找的代码和实现思路写出来了,这里写一下中序遍历的两种实现方式和思路吧(先序遍历和后序遍历大同小异,由于是二叉查找树,只实现中序遍历)。
第一种方式--递归方式:
实现思路:先递归的遍历左子树,再输出父节点的值,最后再递归地遍历右子树,跳出递归条件:当左子树或者右子树不为空时。
代码:
//中序遍历
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