二叉树
二叉树结构介绍
在进行链表结构开发的过程之中会发现所有的数据按照收尾相连的状态进行保存,那么当要进行某一个数据查询的时候(判断该数据是否存在),这种情况下它所面对的时间复杂度是“O(n)"。如果说现在它的数据量小(不超过三十个)的情况下,那么性能上是不会有太大差别的,而一旦保存的数据量很大,这个时候时间复杂度就会严重损耗程序的运行性能,那么对于数据的存储结构就必须发生改变,应该可以尽可能的减少检索次数为出发点进行设计,对于现在的数据结构而言,最好的性能就是“O(logn),所以现在要想实现它就可以利用二叉树的结构来完成。
构造二叉树的原理
如果想要实现一颗树结构的定义,那么就需要去考虑数据的存储形式。
在二叉树的实现之中其基本的实现原理如下:取一个数据为保存的根节点,小于根节点的数据要放在节点的左子树,而大于节点的数据要放在该节点的右子树。
如果要进行数据检索的话,此时就需要进行每个节点的判断,但判断是区分左右的,所以不会整个结构都进行判断处理,那么它的时间复杂度就是 O(logn)。
二叉树的遍历
(1) 前序遍历(根-左-右)
(2)中序遍历(左-根-右)
(3) 后序遍历(左-右-根)
对上图进行中序遍历:10,20,25,30,38,50,80,100(按顺序排列)
二叉树遍历法
二叉树的基础实现
在实现二叉树的处理之中最为关键性的问题在于数据的保存,而且数据由于牵扯到对象比较的问题,那么一定要有比较器的支持,而这个比较器首选的一定就是Comparable,所以本次将保存一个 Person 类数据。
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person o) {
return this.age - o.age;
}
}
随后如果要想进行数据的保存,首先一定需要有一个节点类。节点类里面由于牵扯到数据保存问题所以必须使用 comparable(可以区分大小)。
/**
* 实现二叉树的操作
*
* @param <T>
*/
public class BinaryTree<T extends Comparable<T>> {
private Node root; // 根节点
private int count; // 对节点数进行记录
private Object[] returnNode; // 返回的节点数组
private int foot; // 脚标
/**
* 节点类
*/
private class Node {
private Comparable<T> data; // 数据内容
private Node parent; // 父节点
private Node left;
private Node right;
public Node(Comparable data) { // 创建一个节点
this.data = data;
}
/**
* 将节点添加到树中
* @param newNode 需要添加的节点
*/
public void addNewNode(Node newNode) {
if (newNode.data.compareTo((T) this.data) > 0) { // 判断是添加到左边还是添加到右边
if (this.right == null) { // 如果为空,那就直接添加节点
this.right = newNode;
newNode.parent = this; // 设置父亲节点
} else {
this.right.addNewNode(newNode); // 继续遍历寻找节点添加点
}
} else if (newNode.data.compareTo((T) this.data) < 0) {
if (this.left == null) {
this.left = newNode;
newNode.parent = this;
} else {
this.left.addNewNode(newNode);
}
}
}
/**
* 遍历树
*/
public void toArrayNode() {
if (this.left != null) { // 不为空则向下继续遍历
this.left.toArrayNode();
}
BinaryTree.this.returnNode[BinaryTree.this.foot++] = this.data; // 将节点中的数据添加到数组中
if (this.right != null) {
this.right.toArrayNode(); // 向右侧遍历
}
}
}
public void addNode(Comparable<T> data) { // 添加节点
if (data == null) {
throw new NullPointerException("传入数据不能为空");
}
Node newNode = new Node(data);
if (root == null) {
this.root = new Node(data);
} else {
this.root.addNewNode(newNode);
}
count++;
}
public Object[] toArray() { // 遍历树
if (count == 0) {
return null;
}
foot = 0;
returnNode = new Object[count];
root.toArrayNode();
return this.returnNode;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("fuck",54);
Person person1= new Person("jack",14);
Person person2= new Person("fja",21);
Person person3= new Person("dfjks",25);
Person person4= new Person("djfks`",26);
Person person5= new Person("djf",39);
Person person6= new Person("dfsd",23);
BinaryTree<Person> binaryTree = new BinaryTree<>();
binaryTree.addNode(person);
binaryTree.addNode(person1);
binaryTree.addNode(person2);
binaryTree.addNode(person3);
binaryTree.addNode(person4);
binaryTree.addNode(person5);
binaryTree.addNode(person6);
System.out.println(Arrays.toString(binaryTree.toArray()));
}
}
在进行数据添加的时候只是实现了节点关系的保存,而这种关系保存后的结果就是所有数据都属于有序排列。
数据删除
二叉树之中数据删除是非常复杂的,因为在删除的时候需要考虑到大量的情况。
1.如果待删除节点没有子节点,那么直接删掉即可。
直接删除就可以
2.如果待删除节点只有一个子节点,那么直接删掉,并且其子节点去顶替它。(这时候要考虑两种情况,1.只有一个左子树 2.只有一个右子树)
-
只有一个左子树
-
只有一个右子树
3.如果待删除的节点有两个子节点,这种情况比较复杂:首先找出它的后继节点,然后处理”后继节点“和”被删除节点的父节点“之间的关系,最后处理”后继节点的子节点“和被删除节点的子节点”之间的关系。
在右侧节点中找出最小的节点去顶替
具体的代码实现:
// Node类中
public Node getRemoveNode(Comparable<T> data) {
if (data.compareTo((T) this.data) == 0) {
return this;
} else if (data.compareTo((T) this.data) < 0) {
if (this.left != null) {
return this.left.getRemoveNode(data);
} else {
return null;
}
} else {
if (this.right != null) {
return this.right.getRemoveNode(data);
} else {
return null;
}
}
}
// 树中
public void remove(Comparable<T> data) {
if (this.root == null) {
return;
} else {
Node removeNode = this.root.getRemoveNode(data);
if (removeNode != null) {
if (removeNode.left == null && removeNode.right == null) { // 没有子节点
if (removeNode.parent.data.compareTo((T) removeNode.data) > 0) {
removeNode.parent.left = null;
} else removeNode.parent.right = null;
} else if (removeNode.right != null && removeNode.left == null) { // 只用一个子节点
if (removeNode.parent.data.compareTo((T) removeNode.data) < 0) { // 判断其父节点是左节点,还是右节点
removeNode.parent.right = removeNode.right;
removeNode.right.parent = removeNode.parent;
} else {
removeNode.parent.left = removeNode.right;
removeNode.right.parent = removeNode.parent;
}
} else if (removeNode.right == null && removeNode.left != null) {
if (removeNode.parent.data.compareTo((T) removeNode.data) < 0) {
removeNode.parent.right = removeNode.left;
removeNode.left.parent = removeNode.parent;
} else {
removeNode.parent.left = removeNode.left;
removeNode.left.parent = removeNode.parent;
}
} else if (removeNode.left != null && removeNode.right != null) { // 左右两侧都有节点
Node moveNode = removeNode.right;
while (moveNode.left != null) { // 学找可替换的节点
moveNode = moveNode.left;
}
if (removeNode.parent.data.compareTo((T) removeNode.data) > 0) {
moveNode.parent.left = null;
removeNode.parent.left = moveNode;
moveNode.parent = removeNode.parent;
moveNode.right = removeNode.right;
moveNode.left = removeNode.left;
} else {
moveNode.parent.left = null;
removeNode.parent.right = moveNode;
moveNode.parent = removeNode.parent;
moveNode.right = removeNode.right;
moveNode.left = removeNode.left;
}
}
this.count--;
}
}
}
这种数据结构的删除操作是非常繁琐的,所以如果不是必须的情况下不建议使用删除。
红黑树原理分析
通过整个的二叉树的实现相信已经可以清楚二叉树的主要特点:数据查询的时候可以提供更好的查询性能。但是,二叉树的结构是有明显缺陷的,例如:当二叉树结构改变的时候(增加或删除)就有可能出现不平衡的问题。
之前所谓的解决二叉树性能问题的方式最终全部都变为了 null,也就是说如果要想达到最良好效果的查询结果是一个平衡二叉树,同时所有的节点的层次深度应该相同
如果所有的数据按照以上的结构进行保存,那么二叉树的检索操作执行效率一定是最高的(检索),可是你的树需要可以忍受频繁的增加或者是删除操作。所以针对于二叉树有了进一步的设计要求
红黑树
红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 o(logn)。
红黑树是在 1972 年由 Rudolf Bayer 发明的,当时被称为平衡二叉 B 树( symmetric binary B-trees )。后来,在1978年被 Leo J.Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。
在节点类中增加一个颜色(可以用enum来实现,也可以用true和false)
红黑树特点
-
每个节点或者是黑色,或者是红色
-
根根节点必须是黑色﹔
-
每个叶子节点是黑色﹔
-
Java 实现的红黑树将使用 null 来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。
-
如果一个节点是红色的,则它的子节点必须是黑色的。
-
从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以。
-
连续的。若给定黑色节点的个数N,最短路径情况是连续的 N 个黑色,树的高度为 N一1;最长路径的情况为节点红黑相间,树的高度为 2(N - 1)。
-
一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点数量。
(红色节点之后绝对不可能是红色节点,但是没有说黑色节点之后不能是黑色节点,即允许黑黑连接)
主要是利用红色节点与黑色节点是实现均衡的控制,简单点理解红黑树的结构就是为了可以进行右旋的控制,以保证树的平衡性。
但是对于平衡性,还需要考虑数据增加的平衡以及数据删除的平衡,增加和删除都是需要对这棵树进行平衡修复。
数据插入平衡修复
1.第一次插入,由于原树为空,所以只会违反红-黑树的规则所以只要把根节点涂黑即可。
在进行红黑树处理的时候为了方便操作都会将新的节点使用红色来进行描述,于是当设置根数据插入平衡处理规则节点的时候就会违反规则二,那么这个时候只需要将节点的颜色涂黑即可。
2.如果插入节点的父节点是黑色的,那不会违背红-黑树的规则什么也不需要做﹔但是遇到如下三种情况时,就要开始变色和旋转了:
- 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的。
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点。
插入操作分析
在红黑树进行修复处理之中,它需要根据当前节点以及当前节点的父节点和叔叔节点之间的颜色来判断树是否需要进行修复处理
数据删除平衡修复
操做处理:
-
删除操作后,如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红-黑树的规则。
-
如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复。
-
但是如果遇到以下四种情况,就需要通过变色或旋转来恢复红-黑树的平衡了∶
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的)。
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的。
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是黑色的。
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。
如果遇到以下四种情况,就需要通过变色或旋转来恢复红-黑树的平衡了∶
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的)。
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的。
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是黑色的。
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。
在红黑树之中修复的目的是为了保证树结构中的黑色节点的数量平衡,黑色节点的数量平衡了,那么才可能达到 O(logn) 的执行性能,但是修复的过程一方面是红黑的处理,另一方面就是黑色子节点的保存层次。