树
定义
以层次化方式组织和存放数据的特定数据结构。
在我们日常的生活中也会有树的结构,只不过我们不会敏感到:哦 原来这就是一棵树呀。比如我们的家谱,爷爷下有爸爸,爸爸这一层又有兄弟姐妹,爸爸下一层还有你。
这样的结构有什么好处呢:
1)我能迅速的知道爷爷有多少子女
2)我还能迅速知道堂妹1还有个亲姐妹是谁
3)等等。。。。
所以树的最大的意义是:提高我们的搜索性能!
在树里每一个存放值的空间叫做节点
爷爷和爸爸之间的关系为父子节点
堂妹1和堂妹2分别为叔叔节点的左子节点和右子节点
二叉树
定义
二叉树一种特殊的树结构-二叉树(binary tree),它每个节点最多有两个子结点,亦称左孩子和右孩子。
我们在看上面那张家谱图,它就不是一棵二叉树,因为爷爷下有三个子女,而二叉树限制了每个节点最多有两个节点。
代码表示
二叉树一般是以链表的形式进行表示,我们把每个节点当作一个对象,则它有哪些属性呢?
- 节点本身的值
- 根节点,也就是最顶层的节点(只有一个)
- 左节点
- 右节点
其实节点还有父节点,但是在每个节点对象里是没有保存的,我们只需要在类里定义一个根节点的全局变量,每次去搜索一个树的时候,都从根节点至上而下搜索就性了。
/**
* @program: java-club
* @description: 树节点 链表的结构
* @author: heyunpeng
* @created: 2021/04/18 13:56
*/
public class TreeNode {
private int val;
private TreeNode leftTreeNode;
private TreeNode rightTreeNode;
public TreeNode(int val) {
this.val = val;
}
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
public TreeNode getLeftTreeNode() {
return leftTreeNode;
}
public void setLeftTreeNode(TreeNode leftTreeNode) {
this.leftTreeNode = leftTreeNode;
}
public TreeNode getRightTreeNode() {
return rightTreeNode;
}
public void setRightTreeNode(TreeNode rightTreeNode) {
this.rightTreeNode = rightTreeNode;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", leftTreeNode=" + leftTreeNode +
", rightTreeNode=" + rightTreeNode +
'}';
}
}
二叉搜索树
定义
单从名字来看,它的优势应该是表现在搜索,也就是如何从一棵二叉树里迅速的找到对应的节点,我们先来看看二叉树的规则有哪些。
- 如果它的左子树不为空,则左子树上结点的值都小于根结点。
- 如果它的右子树不为空,则右子树上结点的值都大于根结点。
- 子树同样也要遵循以上两点
二叉树还有个特性:它的中序遍历是有序的。
二叉搜索树的插入
要领:
- 插入的值最后一定是落在叶子节点的(也就是不需要变更树的结构)
- 插入是从根节点开始比较,如果插入的值比它大则继续找它的右子节点,反之找左子节点,直到比较节点不在有对应的子节点为止,这时候只需要把该值设置为对应的子节点即可。这其实就是一个查找的过程,这不过到查找的最后挂上了个叶子节点罢了。
/**
* @program: java-club
* @description: 二叉搜索树
* @author: heyunpeng
* @created: 2021/04/18 13:55
*/
public class SearchBinaryTree {
private TreeNode root; //需要维护root,毕竟增删查都从root开始
private void insert(int val) {
if (root == null) {
root = new TreeNode(val);
return;
} else {
//从根节点开始递归,毕竟每个子节点的任务其实是一致的
compareAndInsert(root, val);
}
}
//这个过程其实是只有递没有归的
private void compareAndInsert(TreeNode treeNode, int val) {
//如果插入的值比节点值小,则去找左边的子节点
if (val <= treeNode.getVal()) {
if (treeNode.getLeftTreeNode() != null) {
//继续递归
compareAndInsert(treeNode.getLeftTreeNode(), val);
} else { //如果该节点没有需要继续查找的左子节点,则直接把val设置为子节点
TreeNode finalTreeNode = new TreeNode(val);
treeNode.setLeftTreeNode(finalTreeNode);
}
} else { //反之亦然
if (treeNode.getRightTreeNode() != null) {
//继续递归
compareAndInsert(treeNode.getRightTreeNode(), val);
} else { //如果该节点没有需要继续查找的左子节点,则直接把val设置为子节点
TreeNode finalTreeNode = new TreeNode(val);
treeNode.setRightTreeNode(finalTreeNode);
}
}
}
}
*完整代码会在文章最后给出,需要注意的是目前的实现并没有考虑线程安全问题的
我这里是通过递归的方式进行的编写,当然你也可以通过直接遍历循环的方式实现。
其实直接通过程序我们可以看出来它的时间复杂度为logn,我们每一层都进行了大小比较,来决定接下来是否左或者右的路由,则其实就是一种二分查找的思想嘛。当然我们如果需要给搜索二叉树插入n个数,那它的时间复杂度则为nlogn
搜索二叉树的查找
查找其实和插入的方法是一致的,直接贴代码
private TreeNode targetTreeNode = null; //全局变量,表示被找到的节点
private TreeNode find(int val) {
compareAndFind(root, val);
return targetTreeNode;
}
private void compareAndFind(TreeNode treeNode, int val) {
if (treeNode != null) {
//如果值与节点值相等,则找到了该节点了,停止递,并给全局变量targetTreeNode赋值
if (val == treeNode.getVal()) {
targetTreeNode = treeNode;
return;
} else if (val < treeNode.getVal()) {
if (treeNode.getLeftTreeNode() != null) {
//继续左子节点查找
compareAndFind(treeNode.getLeftTreeNode(), val);
}
} else {
if (treeNode.getLeftTreeNode() != null) {
//继续右子节点查找
compareAndFind(treeNode.getRightTreeNode(), val);
}
}
}
}
搜索二叉树的删除
删除可就比查找和查找麻烦多了,总共可以分为3中情况:
-
情况a,删除的为叶子节点,直接取消掉父节点与之连接的指针
-
情况b,删除的为树节点,但是这个树节点下面只有一个子节点,则直接将这个子节点替换父节点然后挂在爷爷节点即可。
-
情况c,删除的为树节点,但是这个树节点有左右两个子节点,这种情况下的删除简直是灾难。
先说奥义:
step1:找出后继节点,一个节点的后继节点是指,这个节点在中序遍历序列中的下一个节点,记住中序遍历是有序的,其实就是找第一个比他大的节点。step2:用后继节点去代替被删除节点的位置。
而最重要的就是何为后继节点,这里又要分为3种情况了。。。。
1) 后继节点为待删除节点的子节点,后继节点直接代替待删除节点。
2)后继节点不为待删除节点的子节点
2.1)后继节点没有右节点,同样可以直接代替
2.2)后继节点有右节点,就要先将后继节点的右子节点去替换后继节点的位置,然后再将后继节点去替换待删除节点的位置
删除节点的代码逻辑如下:
-
通过调用查找方法,找到待删除的节点
-
判断待删除节点是否是叶子节点,直接去掉
-
判断待删除节点是子树,但是只有一个子节点,直接将子节点去替代待删除节点的位置
-
判断待删除节点是子树,但是却有两个节点
1)找出后继节点,(这里是一定能找到的,可以想想为什么)
2)若后继节点就是待删除节点的子节点,则执行替代操作
3)若后继节点不是待删除节点的子节点,且没有右子节点(其实这个后继节点就是个叶子节点了),执行替换操作
4)若后继节点不是待删除节点的子节点,且有有子节点,就要先将后继节点的右子节点去替换后继节点的位置,然后再将后继节点去替换待删除节点的位置。 -
节点删除的代码实现先放着,等有空再实现吧。。。。
二叉搜索树的缺陷
比如我们现在新建一个搜索二叉树,并插入如下有序数组[1,2,3,4,5,6]
这种情况下会出现一种极端现象,二叉树直接退化为链表了,而这也是红黑树或者平衡二叉树诞生的背景,它们的出现就是为了解决二叉搜索树的退化问题。
红黑树请看下一篇文章 算法与数据结构–从二叉搜索树到红黑树(2)
搜索二叉树完整代码如下:
package com.yan.javaClub.algorithms.tree;
/**
* @program: java-club
* @description: 树节点 链表的结构
* @author: heyunpeng
* @created: 2021/04/18 13:56
*/
public class TreeNode {
private int val;
private TreeNode leftTreeNode;
private TreeNode rightTreeNode;
public TreeNode(int val) {
this.val = val;
}
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
public TreeNode getLeftTreeNode() {
return leftTreeNode;
}
public void setLeftTreeNode(TreeNode leftTreeNode) {
this.leftTreeNode = leftTreeNode;
}
public TreeNode getRightTreeNode() {
return rightTreeNode;
}
public void setRightTreeNode(TreeNode rightTreeNode) {
this.rightTreeNode = rightTreeNode;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", leftTreeNode=" + leftTreeNode +
", rightTreeNode=" + rightTreeNode +
'}';
}
}
package com.yan.javaClub.algorithms.tree;
/**
* @program: java-club
* @description: 二叉搜索树
* @author: heyunpeng
* @created: 2021/04/18 13:55
*/
public class SearchBinaryTree {
private TreeNode root; //需要维护root,毕竟增删查都从root开始
private void insert(int val) {
if (root == null) {
root = new TreeNode(val);
return;
} else {
//从根节点开始递归,毕竟每个子节点的任务其实是一致的
compareAndInsert(root, val);
}
}
//这个过程其实是只有递没有归的
private void compareAndInsert(TreeNode treeNode, int val) {
//如果插入的值比节点值小,则去找左边的子节点
if (val <= treeNode.getVal()) {
if (treeNode.getLeftTreeNode() != null) {
//继续递归
compareAndInsert(treeNode.getLeftTreeNode(), val);
} else { //如果该节点没有需要继续查找的左子节点,则直接把val设置为子节点
TreeNode finalTreeNode = new TreeNode(val);
treeNode.setLeftTreeNode(finalTreeNode);
}
} else { //反之亦然
if (treeNode.getRightTreeNode() != null) {
//继续递归
compareAndInsert(treeNode.getRightTreeNode(), val);
} else { //如果该节点没有需要继续查找的左子节点,则直接把val设置为子节点
TreeNode finalTreeNode = new TreeNode(val);
treeNode.setRightTreeNode(finalTreeNode);
}
}
}
//中序遍历 左根右
//把握奥义 根节点输出
private void traverse() {
mid_traverse(root);
}
//拆分为子任务
//有递有归
private void mid_traverse(TreeNode treeNode) {
//递的停止条件
if (treeNode != null) {
mid_traverse(treeNode.getLeftTreeNode());//左
System.out.println(treeNode.getVal());//根节点输出
mid_traverse(treeNode.getRightTreeNode());//右
}
}
private TreeNode targetTreeNode = null; //全局变量,表示被找到的节点
private TreeNode find(int val) {
compareAndFind(root, val);
return targetTreeNode;
}
private void compareAndFind(TreeNode treeNode, int val) {
if (treeNode != null) {
//如果值与节点值相等,则找到了该节点了,停止递,并给全局变量targetTreeNode赋值
if (val == treeNode.getVal()) {
targetTreeNode = treeNode;
return;
} else if (val < treeNode.getVal()) {
if (treeNode.getLeftTreeNode() != null) {
//继续左子节点查找
compareAndFind(treeNode.getLeftTreeNode(), val);
}
} else {
if (treeNode.getLeftTreeNode() != null) {
//继续右子节点查找
compareAndFind(treeNode.getRightTreeNode(), val);
}
}
}
}
public static void main(String[] args) {
SearchBinaryTree searchBinaryTree = new SearchBinaryTree();
//插入一段无序的数组
int[] array = {5,2,6,8,9,1,3,10};
for (int i : array) {
searchBinaryTree.insert(i);
}
//中序遍历出来,看看是否是有序排列的
System.out.println("***打印遍历结果*****");
searchBinaryTree.traverse();
//查找
System.out.println("***打印查找值为5的节点*****");
System.out.println(searchBinaryTree.find(5));
}
}