在数据结构中,树是其中非常重要的一种,其优秀的结构和易于处理的方式使其在底层数据处理中被普遍使用。
其实从名字上来看,我们就能了解到,这种数据结构就是类似我们生活中树的结构,从树根出发,出现枝杈,然后每个枝杈又分成多个分支。我们今天就先用二叉树和二叉查找树这两种较为简单的树形结构来总结下树的性质。
二叉树
二叉树是指每个节点只有两个分支,我们将左边的分支叫做左子树,右边为右子树。用图表示为
有图我们可以看到,一棵树由一个根节点开始,直到最下方的节点,最下方的节点指向null。
二叉树的存在,是我们可以使用递归来解决一些问题,这样自然使编程变的非常简单,但同时我们也更需要谨慎使用递归,因为递归的设计是巧妙而脆弱的!
二叉树内部维护一个Node类,该类具体实现如下。
public class BSTNode {
private int key;//键
private int value;//值
private BSTNode leftNode;//左子树
private BSTNode rightNode;//右子树
}
这里只是最简单的给出了私有属性,具体get,set和判断是否存在子树的方法都为进行实现。
二叉查找树
二叉查找树与二叉树最大的区别便是在于数据排列形式,在二叉树中,我们向树中添加数据并无规律,只是实现一种树状的形式。实际上这种情况使用极少,这样的简单结构无法对我们有太大帮助。
二叉查找树便是对二叉树的一个调整,在查找树中,我们要保证节点(包括根)左子树的数据小于节点,右子树的数据大于节点数据。如图
为什么我们要实现这样的数据结构,或者是这样能够对我们操作有什么好处呢?
在这里我们就不得不提一种非常优秀并且易于实现的查找算法——二分查找
二分查找是一种应用非常普遍的查找方法。一般我们能想到查找的方法便是遍历集合后进行数据对比,这是最简单但是也是最昂贵的方法。但如果数组已经完成排序后,我们通过二分查找就能非常轻易并且省时的找出要找的数据。
因为数据已经完成排序,当我们要从中找一个值时,我们只需要比较该值得键和数组中部的键大小,如果小于则右侧的数据就不用进行比较,通过这种方法,我们几乎每次都能减少二分之一的搜索量,通过递归也非常容易实现该方法,至于具体细节我们不在这里赘述,等详细总结查找方法时再说。
当我们按照要求对树内数据排序时,这棵树对于二分查找就实现了可能,因为左子树中的节点都小于根节点,右子树中的节点都大于根节点,所以我们在输入一个数据时,与根节点进行比较就能减少二分之一都搜索量,然后使用递归,逐层向下寻找节点。代码实现为
查找实现
public int select(int key) {
//根节点引用
currentNode = root;
//当前节点不为空,并且当前节点和输入值不同时进行循环
while (currentNode != null && key != currentNode.getKey()) {
//如果小于节点
if (key < currentNode.getKey()) {
//则向左查找
currentNode = currentNode.getLeftNode();
}else {
//否则向右查找
currentNode = currentNode.getRightNode();
}
}
//当满足条件时跳出循环,返回查找到的值
return currentNode.getValue();
}
该方法同样可以写成递归形式
public Object select(BSTNode root,int key) {
//判空操作
if(root == null) {
return null;
}
//小于根节点,将根节点的左子树作为参数进行递归
if(key < root.getKey()) return select(root.getLeftNode(), key);
//大于根节点,将根节点的右子树作为参数进行递归
else if(key > root.getKey()) return select(root.getRightNode(), key);
//当满足条件时返回
else return root.getValue();
}
通过这两段代码我们能很清楚的理解二叉树在查找方面的巨大优势。
下面让我们来看下二叉树的遍历如何实现。二叉树遍历分为三种
- 先序遍历 访问根节点,遍历左子树,遍历右子树
- 中序遍历 遍历左子树,访问根节点,遍历右子树
- 后序遍历 遍历左子树,遍历右子树,访问根节点
思想非常简单,代码实现起来三种遍历没有差别只是代码顺序变化。
//遍历先序
public void PreSearchEach(BSTNode node) {
if(root == null) {
System.out.println("树为空");
}
if(root != null) {
//输出树内容,访问根节点
System.out.println(root.getKey()+" "+root.getValue());
//向左递归
PreSearchEach(node.getLeftNode());
//向右递归
PreSearchEach(node.getRightNode());
}
}
//中序遍历
public void MidSearchEach(BSTNode node) {
if(root == null) {
System.out.println("树为空");
}
if(root != null) {
//向左递归
MidSearchEach(node.getLeftNode());
//访问根节点
System.out.println(root.getKey()+" "+root.getValue());
//向右递归
MidSearchEach(node.getRightNode());
}
}
//后序遍历
public void LastSearchEach(BSTNode node) {
if(root == null) {
System.out.println("树为空");
}
if(root != null) {
//向左递归
LastSearchEach(node.getLeftNode());
//向右递归
LastSearchEach(node.getRightNode());
//访问根节点
System.out.println(root.getKey()+" "+root.getValue());
}
}
下面看下插入数据的操作
因为二叉查找树数据排序的特性,在我们进行插入时,需要将数据插入到合适位置,符合排序规则,一旦出现数据插入错误,很可能整棵树的性能会大幅度降低,甚至查找会出现异常。
同查找实现相同,我们在插入数据的时候首先也要进行比较,如果数据大于根节点,我们向右探索,反之向左,这样通过循环操作,我们能找到数据合理的位置,那么将该节点的引用指向新的节点,并将数据存入新节点即可。
下面是我们的实现代码
public void put(int key, int value) {
// 从根节点开始比较
boolean isLeftNode = true;//我们选取的一个辅助操作值
BSTNode newNode = new BSTNode(key, value);//创建的新的节点
BSTNode parentNode = root;//新节点的父节点引用
while (currentNode != null) {
//当小于根节点时,向左进行探索
if (value < currentNode.getValue()) {
//获取当前节点的父节点
parentNode = currentNode;
currentNode = currentNode.getLeftNode();
isLeftNode = true;
} else {
//否则向右进行探索
//获取当前节点的父节点
parentNode = currentNode;
currentNode = currentNode.getRightNode();
isLeftNode = false;
}
}
//当最后currentNode指向null时,我们的搜素进行完成
//此时我们所要插入的位置便是currentNode的父节点,即parentNode
//通过isLeftNode这个辅助操作,我们可以更简洁的判读插入左右
if (isLeftNode) {
parentNode.setLeftNode(newNode);
} else {
parentNode.setRightNode(newNode);
}
}
下面我们要讲的删除操作是二叉树中最为复杂的一个操作,正是因为二叉搜索树的结构特点,每个节点都对其他节点有着很大影响,当我们删除一个节点后,树的结构必须进行更新,所以如何精密的完成删除机构是保证树结构正确性非常重要的一个操作。
下面我们来说下deleteMin和deleteMax两个操作,这两个操作实际上是对树的镜像操作,代码实现基本相同,我们以deleteMin来举例说明。当我们要删除最小的节点时,我们可以很容易的想到,只要顺着根节点一直向左查找,就可以得到最小的节点。之后有两种情况,一种情况是该节点无子树,我们只需要直接删除,第二种情况,该节点有右子树,那么我们进行删除时,需要将右子树替换该节点,保证数据完整和结构的正确。只需要将该节点的父节点指向右子树,同时断开该节点指向右子树的链接,此时,JVM的GC系统就能个将该节点标记为可清除对象,就能够从内存中消除。
下面我们用代码来实现两种方法
public int deleteMin() {
BSTNode parent = root;
currentNode = root;
if(root == null) {
return 0;
}
//向左遍历,寻找最小节点
while(currentNode.getLeftNode() != null) {
//最小节点的父节点
parent = currentNode;
currentNode = currentNode.getLeftNode();
}
//如果最小节点有右子树
if(currentNode.getRightNode() != null) {
//将父节点指向最小节点的右子树
parent.setLeftNode(currentNode.getRightNode());
//断开最小节点和右子树的链接
currentNode.setRightNode(null);
return 1;
}else {
//如果最小节点无右子树
parent.setLeftNode(null);
return 1;
}
}
public int deleteMax() {
BSTNode parent = root;
currentNode = root;
if(root == null) {
return 0;
}
while(currentNode.getRightNode() != null) {
parent = currentNode;
currentNode = currentNode.getLeftNode();
}
if(currentNode.getLeftNode() != null) {
parent.setRightNode(currentNode.getLeftNode());
currentNode.setRightNode(null);
return 1;
}else {
parent.setRightNode(null);
return 1;
}
}
下面我们来说一下删除指定节点的操作,我们知道,删除节点后,主要需要对左右子树进行操作保证数据结构完整性,因此我们不能只将所需节点删除,当该节点有子节点时,我们需要选取一个合适的节点来代替它。幸运的是,因为二叉搜索树的特殊结构,这样的数据非常容易寻找到。我们所要删除节点左侧的数据都小于节点,右侧数据都大于节点,因此,我们只需要按照这个思想,再次寻找一个这样的节点,那么我们可以从左子树中找到最大的节点或者右子树中最小的节点,这样操作非常方便,我们只需要像之前一样不断遍历寻找即可。
该过程图形化后就是这样的,这样的操作能在很大程度上保证一棵树结构的正确,保证其性能的优秀。
只是在我们实现的过程中,需要进行非常多次数的判断,防止空指针异常,也同样要保证其节点正确指向,保证数据不会丢失在内存中而被GC收集。
下面就是实现代码。
public int delete(int key) {
currentNode = root;
BSTNode parentNode = currentNode;
BSTNode leftSonNode;
BSTNode rightSonNode;
BSTNode min;
BSTNode max;
boolean isLeftNode = true;
if (root == null) {
return 0;
}
// 先进行遍历寻找要删除的节点
while (currentNode != null && key != currentNode.getKey()) {
if (key < currentNode.getKey()) {
parentNode = currentNode;
currentNode = currentNode.getLeftNode();
} else {
parentNode = currentNode;
currentNode = currentNode.getRightNode();
}
}
if (currentNode == null) {
return 0;
}
if (parentNode.getLeftNode() == currentNode) {
isLeftNode = true;
} else {
isLeftNode = false;
}
// 现在我们已经获得了要删除的节点为currentNode
// 下面可能出现几种情况,无子树,有右子树,有左子树,左右子树都有
if (currentNode.isHaveLeftNode() == false && currentNode.isHaveRightNode() == false) {
if (isLeftNode == true) {
parentNode.setLeftNode(null);
} else if (isLeftNode == false) {
parentNode.setRightNode(null);
}
}
// 右子树
if (currentNode.isHaveLeftNode() == false && currentNode.isHaveRightNode() == true) {
min = currentNode.getLeftNode();
BSTNode minParentNode = min;
while (min.getLeftNode() != null) {
minParentNode = min;
min = min.getLeftNode();
}
//断开最小值的指向
minParentNode.setLeftNode(null);
//将min指向当前节点
min.setRightNode(currentNode.getRightNode());
//将currentNode的父节点进行调整
if (isLeftNode == true) {
parentNode.setLeftNode(min);
}
if (isLeftNode == false) {
parentNode.setRightNode(min);
}
}
//左子树
if (currentNode.isHaveLeftNode() == true && currentNode.isHaveRightNode() == false) {
max = currentNode.getRightNode();
BSTNode maxParentNode = max;
while (max.getRightNode() != null) {
maxParentNode = max;
max = max.getLeftNode();
}
//断开最小值的指向
maxParentNode.setRightNode(null);
//将min指向当前节点
max.setLeftNode(currentNode.getLeftNode());
//将currentNode的父节点进行调整
if (isLeftNode == true) {
parentNode.setLeftNode(max);
}
if (isLeftNode == false) {
parentNode.setRightNode(max);
}
}
//双子树
if(currentNode.isHaveLeftNode() == true && currentNode.isHaveRightNode() == true) {
min = currentNode.getLeftNode();
BSTNode minParentNode = min;
while (min.getLeftNode() != null) {
minParentNode = min;
min = min.getLeftNode();
}
//断开最小值的指向
minParentNode.setLeftNode(null);
//将min指向当前节点
min.setRightNode(currentNode.getRightNode());
min.setLeftNode(currentNode.getLeftNode());
//将currentNode的父节点进行调整
if (isLeftNode == true) {
parentNode.setLeftNode(min);
}
if (isLeftNode == false) {
parentNode.setRightNode(min);
}
}
return 1;
}
文章最后我们附上几篇非常详细的博客
B树类:https://blog.csdn.net/v_july_v/article/details/6530142