数据结构——树
(PS:终于到了树,对于树的一些理论在我之前的博客中有,我之前将树的基本的定义整理了出来,https://blog.csdn.net/weixin_44077141/article/details/100107069)
二叉搜索树
在这里先对二叉搜索树进行一个简单的学习
首先是一些对于树的基本介绍,树的结构在大自然和计算机中都有很多的出现和利用,之所以使用树,肯定是也是为了更加的高效,我们在这里主要学习二叉搜索树(Binary Search Tree),二叉搜索树,左子树小于根,根小于右子树,一个根上至多可以有两个子树,以此类推。
二叉搜索树的基本结构
private class Node {
public E e;
public Node left,right;
public Node(E e) {
this.e=e;
left=null;
right=null;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return this.e+"";
}
}
private Node root;
private int size;
public BST() {
root=null;
size=0;
}
public int size(){
return size;
}
public boolean isEmpty() {
return size==0;
}
在这里还有一些小点,二叉搜索树主要是使用就是为了使用他的排序功能,所以你需要去继承他的Comparable这个借口,还有就是因为是二叉树,所以说底层的存储结构就是左子树,右子树,自己的值,一个树只有一个根,一个记录树大小的属性size。
####二叉树的添加
//非递归的实现(自己操作)
/**
* 最开始的情况就是会判断是否为空,如果为空则元素是跟,如果不是空调用自己的方法实现插入和递归
* 然后自己的方法是:先判断值,如果是一样的直接返回,如果是大并且是右子树为空直接插入,如果是小并且左子树为空
* 如果不为空在自己调用自己自己
*
* 修改后
* 删除了代码的冗余性,将判断是否为空直接加在自己的方法里面
* 自己的方法也是进行了修改,如果为空返回值一个跟,如果小去左,如果右去右,
* @param e
*/
public void add(E e) {
// //非空才递归
// if(root==null) {
// root=new Node(e);
// size++;
// }
// else {
// add(root, e);
// }
root = add(root, e);
}
/**
* 因为Node不需要对用户可见所以是private
* 原来没有返回值,但是现在有了
* 现在返回的是插入新节点后二叉树的根
* @param node
* @param e
*/
private Node add(Node node ,E e) {
//你看上面的添加代码不纯粹,所以我们要将代码完成的更加的纯粹,
// if(e.equals(node.e)){
// return;
// }
// else if(e.compareTo(node.e)<0&&node.left==null){
// node.left=new Node(e);
// size++;
// return;
// }
// else if(e.compareTo(node.e)>0&&node.right==null){
// node.right=new Node(e);
// size++;
// return;
// }
if(node==null) {
size++;
return new Node(e);
}
if(e.compareTo(node.e)<0) {
node.left = add(node.left,e);
}
else if(e.compareTo(node.e)>0){
node.right = add(node.right,e);
}
// /**
// *如果是小的就去左,大或者==去右
// */
// else {
// add(node.right,e);
// }
return node;
}
在这里主要是使用递归的做法,我如果有时间也会去完成不需要递归的方法去完成递归操作,我们在这里就是单纯的只是完成递归的方法。
因为递归,所以将树的根放入方法中,进行递归,进行递归有两个条件
第一就是递归中止的条件,你要搞清楚在什么条件下需要停止,其实也就是临界值,特殊情况
第二就是常规的情况,如何去递归
我们在这里的逻辑就是,直接插入,如果没有根,插入元素做根,如果有根元素,看比根元素大还是小,大的话去右子树,查看右子树是否为空,进行上一步的递归,空则直接插入,不空则去看比右子树大还是小,左子树是一样的逻辑。
我写方法的是一种模式,即使用户只能够看得到和调用public的方法,他们不需要知道的底层是如何去执行的。
是否包含某元素
/**
* 递归的
* @param e
* @return
*/
public boolean contains(E e) {
return contains(root,e);
}
private boolean contains(Node node,E e) {
if(node==null) {
return false;
}
if(e.compareTo(node.e)==0) {
return true;
}
else if(e.compareTo(node.e)>0) {
return contains(node.right,e);
}
else {
return contains(node.left,e);
}
}
直接就递归,如果一样就输出true,如果比当前节点小的话就去左子树进行递归,大的话去右子树进行递归。
二叉搜索树的遍历
二叉搜索树的遍历乃至所有的树的遍历都是有三种情况的
分别是前序遍历中序遍历后序遍历
我先在这里稍微讲一下具体自己的理解
前序遍历:在树的中,每一个节点都存在三个结构,分别是左子树,右子树,自己的值,而前序遍历就实现访问自己的值,然后去看自己的左子树,如果左子树有,就先访问左子树,以此递归,右子树也一样。
中序遍历:先访问左子树,在访问自己的值,最后后访问右子树。
后序遍历:先访问左子树,在访问右子树,最后采访问自己的值
我们要的答案就是访问的值,没有看懂的看我下面的这个例子
以此图为例
前序遍历:20 15 10 5 13 16 25 30 50
中序遍历:5 10 13 15 16 20 25 30 50
后序遍历:5 13 10 16 15 30 50 25 20
PS(如果有错请指出来谢谢了)
如果仔细观察就能够发现其实,之所以使用的二叉搜索树就是因为二叉搜索树的中序遍历是有序的。
二叉搜索树前序遍历具体实现
/**
* 先序遍历先去访问他的值
*/
public void preOrder() {
preOrder(root);
}
/**
* 前序节点的非递归算法
*/
public void preOrderNP() {
Stack<Node> stack=new Stack<Node>();
stack.push(root);
while(!stack.isEmpty()) {
Node cur=stack.pop();
System.out.println(cur.e);
if(cur.right!=null) {
stack.push(cur.right);
}
if(cur.left!=null) {
stack.push(cur.left);
}
}
}
/**
* 隐藏的前序遍历的做法
* @param node
*/
private void preOrder(Node node) {
if(node==null) {
return ;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
//递归的中止
// if(node!=null) {
// System.out.println(node.e);
// preOrder(node.left);
// preOrder(node.right);
// }
}
这上面是前序遍历,递归的写法和非递归的写法。
递归的写法是先去打印节点的值,然后去执行左子树的值,再去执行右子树的值。
非递归的写法我们借鉴了系统栈,Stack,将根节点放进栈中,先放右子树,在放左子树,这样的话出栈,先出左子树,然后才是右子树。
中序遍历
/**
* 中序遍历
*/
public void inOrder() {
inOrder(root);
}
/**
* 不能给用户看的中序遍历
* @param node
*/
private void inOrder(Node node) {
if(node==null) {
return ;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
上面是中序遍历的代码,一样的代码就是优先去找他的左子树,然后才是输出他的值,然后才是右子树。
后序遍历
/**
* 后序遍历
*/
public void postOrder() {
postOrder(root);
}
/**
* 不能给用户看的后序遍历
* @param node
*/
private void postOrder(Node node) {
if(node==null) {
return ;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
后序遍历就是先去找左子树,然后是右子树,然后才是自己的值,
层序遍历
其实我们上面的搜索可以是算作深度优先搜索,就是一直搜索到最深的位置,为止,然后回退到能够进行搜索的位置为止,在这里我们完成另一种的搜索方式,叫做层序遍历,就是到了同一层,先遍历完这一层才会继续往下去找,不能说哪一种搜索方式更加的优秀,但是每一种方式都有自己的优点和缺点。
/**
* 层序遍历,一层一成的进行遍历
*/
public void levelOrder(){
Queue<Node> q=new LinkedList<>();
q.add(root);
while(!q.isEmpty()) {
Node remove = q.remove();
System.out.println(remove.e);
if(remove.left!=null) {
q.add(remove.left);
}
if(remove.right!=null) {
q.add(remove.right);
}
}
}
在这里我利用了队这个数据结构,因为是一层,所以到一层就把这一层中所有的子树按照,先放左子树后放右子树的顺序放进队中,因为队是先进先出所以,先输出左子树,然后是右子树。
二叉树中元素的删除
因为结构是树,是一个非线性结构,跟我们平时操作的线性结构不同,所以我们的删除方式也不同。
删除二叉树中最小值
因为删除的算法可能不好表达,所以我开始先从最小的值开始删除。
/**
* 最小的元素
* 再删除最小的节点时,如果有右子树,删除以后当成跟左右子树
* @return
*/
public E minimun() {
if(size==0) {
throw new IllegalArgumentException("shuweikong");
}
return minimun(root).e;
}
private Node minimun(Node node) {
if(node.left==null) {
return node;
}
return minimun(node.left);
}
/**
// 从二分搜索树中删除最小值所在节点, 返回最小值
* @return
* 删除搜索二叉树时不要考虑一个问题,就是删除的元素和删一个元素的关系吗
*/
public E removeMin() {
E ret=minimun();
//这一行的意思是可能根节点会称为最小的节点被删除
root=removeMin(root);
return ret;
}
/**
* 第一步递归找到最左边的元素进行(如果有节点和没有节点)
* 第二部删除最左边的元素并且返回
* 没有节点的直接返回,
* 如果有节点,就把右子树变成左子树,
* 第三步通过递归实现,递归去,递归会,左子树=左子树
* @param node
* @return
*/
private Node removeMin(Node node) {
//左子树为空,如果有右子树就把右子树保存一下
if(node.left==null) {
Node rightNode=node.right;
node.right=null;
size--;
return rightNode;
}
//如果左子树不为空,一直递归
node.left=removeMin(node.left);
return node;
}
我们在这里提供了四个方法,但是其实也只是为了实现两个功能,
第一个是将搜索二叉树中的最小值,输出。
第二个是删除搜索二叉树的最小值
你们看我画的图,如果最左边的也叶子结点有元素的话,那就是最左边的叶子节点是最小,这一点是二叉搜索树的定义,这一点没有问题,但是我们的问题是,如果最左边的元素没有左子树怎么办?
这两张图的区别就是第二张图是第一张图去掉了最左边的元素,那么现在谁是最小的呢?
经过观察,试验,总结,我们得出结论,其实还是最左边元素是最小值,不管他有没有子树,只要是他是最左边的元素他就是最小的。
这样我们的寻找最左边的元素就逻辑出来了。
找到最左边的元素。
那么我们删除最左边的元素的逻辑是什么的呢?
我们的逻辑是,如果最左边的元素是叶子节点,也就是没有子树,那么直接删除,并且取出。但万一有右子树呢,就像我上面的第二张图,那就是先去承将他的右子树,然后再去删除这个元素。
在这里我遇到了一个小问题,我们再删除的时候,我们将有右孩子的结点进行删除,将他的右孩子找一个新的元素去接,然后我就在想,那这个新的元素没有别的元素指向吗?是一个单独的嘛?
事实证明,是我小看了递归,递归不仅仅是递归大左子树为空的位置,而且是在返回时,node.left=removeMin(node.left);
这一句代码,被递归回来,左子树从上向下完成了指向的功能。
还有就是在root=removeMin(root);
这一行的代码,就是容易出现这个问题,我之前没有想过这个东西会出现问题,但是我在研究上一个问题的时候发现我,删除了前边的root=,没有什么变化,然后我通过读代码才发现,原来这句话意思是,如果咱们在删除最小元素的时候,删除到了根节点,这是根节点是会变得,如果根节点也被删除了根节点会发生改变,如果没有删除到根节点,他到最后返回还是根节点,这是我之前一直没有意识到的东西。
删除二叉树中最大值
一样的逻辑我就不多论述了
/**
* 删除node的最大节点
* 返回删除节点的根
* @return
*/
public E removeMax() {
E ret=maximun();
// System.out.println(ret);
root=removeMax(root);
return ret;
}
private Node removeMax(Node node) {
if(node.right==null) {
Node leftNode=node.left;
node.left=null;
size--;
return leftNode;
}
node.right=removeMax(node.right);
return node;
}
删除任意元素
public void remove(E e) {
root=remove(root,e);
}
private Node remove(Node node,E e) {
if(node==null) {
return null;
}
//左子树找到位置
if(e.compareTo(node.e)<0) {
node.left=remove(node.left, e);
return node;
}
//右子树找到位置
else if(e.compareTo(node.e)>0) {
node.right=remove(node.right, e);
return node;
}
//找到位置
else {
//左子树为空
if(node.left==null) {
Node rightNode=node.right;
node.right=null;
size--;
return rightNode;
}
//右子树为空
if(node.right==null) {
Node leftNode=node.left;
node.left=null;
size--;
return leftNode;
}
//找到指定位置右子树中最小的一个节点,当做根
Node success=minimun(node.right);
//删除这个最小的元素
success.right=removeMin(node.right);
//承接他的左子树
success.left=node.left;
node.left=node.right=null;
return success;
}
}
删除的逻辑跟我之前的说的逻辑是一样的,先去寻找位置,通过递归的想法,然后找到位置e.compareTo(node.e)>0
也就是那个else的时候,我们就开始准备删除元素,首先先判断这个元素左子树为空还是右子树为空,如果他的左子树为空,用一个新的元素来指向他的右子树,然后通过递归和上一个他的上一个元素,建立联系实现删除功能。
如果这个元素的左右子树都不为为空,就将找删除的指定位置最小的节点这个节点为叶子节点,删除这个最小的节点,将这个节点顶替我们要删除的位置,将我们要删除的节点左右子树都==null。
后记
后面关于二叉树的复杂度的比较我在后面会写。