二叉树
1、二叉树结构简介
在进行链表结构开发的过程之中会发现所有的数据按照首尾相连的状态进行保存,那么当要进行某一个数据查询的时候(判断该数据是否存在),这种情况下它所面对的时间复杂度是“O(n)”,如果说现在它的数据量小,这样的情况下,性能上是不会有太大差别的,但是一旦保存的数据量很大,这个时候时间负责度就会严重很多,严重损耗程序的运行性能。这个时候对于数据的存储结构就必须发生改变,应该可以尽可能的减少检索次数为出发点进行设计,对于现在的数据结构而言,最好的性能就是“O(logn)”。所以要想实现它就可以利用二叉树的结构来完成。
如果要想实现一个树结构的定义,那么就需要考虑数据的存储形式,在二叉树实现之中,基本的实现原理如下:
取第一个数据为保存的根节点,小于等于根节点的数据要放在节点的 左子树 ,而大于节点的数据要放在节点的 右子树。
如果要进行数据检索的话,此时就需要进行每个节点的判断,但是它的判断是区分左右的,所以不会是整个结构都进行判断处理,那么它的时间复杂度就是(O(logn))。
而对于二叉树而言,在进行数据获取的时候也有三种形式:前序遍历(根-左-右)、中序遍历(左-根-右)、后序遍历(左-右-根)。二叉树的内容都属于排序的结果。
2、二叉树基础实现
在实现二叉树的处理之中最为关键性的问题在于数据的保存,而且由于数据牵扯到对象比较的问题,那么一定要有比较器的支持,而这个比较器首选一定就是Comparable,所以本次将保存一个Person类数据:
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person person) {
return this.age - person.age;
}
}
随后如果想要进行数据的保存,首先一定需要有一个节点类。节点类里面由于牵扯到数据的保存问题,所以必须使用Comparable(可以区分大小)
public class JavaAPIDemo {
public static void main(String[] args) throws ParseException {
BinaryTree<Person> tree = new BinaryTree<>();
tree.add(new Person("小一-80" , 80));
tree.add(new Person("小二-30" , 30));
tree.add(new Person("小三-20" , 20));
tree.add(new Person("小四-60" , 60));
tree.add(new Person("小五-90" , 90));
System.out.println(Arrays.toString(tree.toArray()));
}
}
/**
* 实现二叉树操作
* @param <T> 要进行二叉树的实现
*/
class BinaryTree<T extends Comparable<T> >{
private class Node{
private Comparable<T> data; //存放Comparable,可以比较大小
private Node parent; //保存父节点
private Node left; //保存左子树
private Node right; //保存右子树
public Node(Comparable<T> data){ //构造反方法直接负责进行数据的存储
this.data = data;
}
/**
* 实现节点数据的适当位置的存储
* @param newNode 创建的新节点
*/
public void addNode(Node newNode){
if (newNode.data.compareTo((T)this.data) <= 0) { //比当前节点那数据小
if (this.left == null) { //现在没有左子树
this.left = newNode; //保存左子树
newNode.parent = this; //保存父节点
} else { //需要向左便继续判断
this.left.addNode((newNode)); //继续向下判断
}
} else {
if (this.right == null) { //现在没有左子树
this.right = newNode; //没有右子树
newNode.parent = this; //保存父节点
} else { //需要向左便继续判断
this.right.addNode((newNode)); //继续向下判断
}
}
}
/**
* 实现所有数据的获取处理,按照中序遍历的形式完成
*/
public void toArrayNode() {
if (this.left != null) { //没有左子树
this.left.toArrayNode(); //递归调用
}
BinaryTree.this.returnData[BinaryTree.this.foot ++] = this.data;
if (this.right != null) {
this.right.toArrayNode();
}
}
}
//---------- 以下为二叉树的实现 ----------
private Node root; //保存的很节点
private int count; //保存数据的个数
private Object [] returnData; //返回的数据
private int foot = 0; //脚标控制
/**
* 进行数据的保存
* @param data 要保存的数据内容
* @exception NullPointerException 保存数据为空时抛出的异常
*/
public void add(Comparable<T> data) {
if (data == null) {
throw new NullPointerException("保存的数据不允许为空");
}
//所有的数据本身不具备节点的关系匹配,那么一定要将其包装在Node类之中
Node newNode = new Node(data); //保存节点
if (this.root == null) { //现在没有根节点,则第一个节点为根节点
this.root = newNode;
} else { //需要为其保存到一个合适的节点
this.root.addNode(newNode); //交由Node类处理
}
this.count ++;
}
/**
* 以对象数组的形式返回全部数据,如果没有数据则返回null
* @return 全部数据
*/
public Object [] toArray(){
if (this.count == 0){
return null;
}
this.returnData = new Object[this.count]; //保存长度为数组长度
this.foot = 0; //脚标清零
this.root.toArrayNode(); //直接通过Node类负责
return this.returnData; //返回全部的数据
}
}
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person person) {
return this.age - person.age;
}
}
在进行数据添加的时候只是实现了节点关系的保存,而这种关系保存后的结果就是所有的数据都属于有序排列。
3、二叉树数据删除
二叉树之中的数据删除操作是非常复杂的,因为在进行数据删除的时候需要考虑的情况是比较多的:
- 如果待删除节点没有子节点,那么直接删掉即可
- 如果待删除节点只有一个子节点,那么直接删掉,并用其子节点去顶替它
- 如果待删除节点有两个子节点,这种情况比较复杂:首选找出它的后继节点,然后处理”后继节点和“被删除节点的父节点”之间的关系,最后处理“后继节点的子节点”和“被删除节点的子节点”之间的关系
通过具体的代码实现操作功能:
范例:在Node类中追加有新的功能
/**
* 获取要删除的节点对象
* @param data 比较的对象
* @return 要删除的节点对象,对象一定存在
*/
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;
}
}
}
}
范例:BinaryTree类里面进行节点的处理.
public class JavaAPIDemo {
public static void main(String[] args) throws ParseException {
BinaryTree<Person> tree = new BinaryTree<>();
tree.add(new Person("小一-80" , 80));
tree.add(new Person("小二-50" , 50));
tree.add(new Person("小三-60" , 60));
tree.add(new Person("小四-30" , 30));
tree.add(new Person("小五-90" , 90));
tree.add(new Person("小六-10" , 10));
tree.add(new Person("小七-55" , 55));
tree.add(new Person("小八-70" , 70));
tree.add(new Person("小九-85" , 85));
tree.add(new Person("小十-95" , 95));
tree.remove(new Person("小六-10" , 10));
System.out.println(Arrays.toString(tree.toArray()));
}
}
/**
* 实现二叉树操作
* @param <T> 要进行二叉树的实现
*/
class BinaryTree<T extends Comparable<T> >{
private class Node{
private Comparable<T> data; //存放Comparable,可以比较大小
private Node parent; //保存父节点
private Node left; //保存左子树
private Node right; //保存右子树
public Node(Comparable<T> data){ //构造反方法直接负责进行数据的存储
this.data = data;
}
/**
* 实现节点数据的适当位置的存储
* @param newNode 创建的新节点
*/
public void addNode(Node newNode){
if (newNode.data.compareTo((T)this.data) <= 0) { //比当前节点那数据小
if (this.left == null) { //现在没有左子树
this.left = newNode; //保存左子树
newNode.parent = this; //保存父节点
} else { //需要向左便继续判断
this.left.addNode((newNode)); //继续向下判断
}
} else {
if (this.right == null) { //现在没有左子树
this.right = newNode; //没有右子树
newNode.parent = this; //保存父节点
} else { //需要向左便继续判断
this.right.addNode((newNode)); //继续向下判断
}
}
}
/**
* 实现所有数据的获取处理,按照中序遍历的形式完成
*/
public void toArrayNode() {
if (this.left != null) { //没有左子树
this.left.toArrayNode(); //递归调用
}
BinaryTree.this.returnData[BinaryTree.this.foot ++] = this.data;
if (this.right != null) {
this.right.toArrayNode();
}
}
/**
* 进行数据的检索处理
* @param data 要检索的数据
* @return 找到返回true,找不到返回false
*/
public boolean containsNode(Comparable<T> data) {
if (data.compareTo((T)this.data) == 0){
return true; //查找到了
} else if (data.compareTo((T)this.data) < 0){ //左边有数据
if (this.left != null){
return this.left.containsNode(data);
} else {
return false;
}
} else {
if (this.right != null) {
return this.right.containsNode(data);
} else {
return false;
}
}
}
/**
* 获取要删除的节点对象
* @param data 比较的对象
* @return 要删除的节点对象,对象一定存在
*/
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;
}
}
}
}
//---------- 以下为二叉树的实现 ----------
private Node root; //保存的很节点
private int count; //保存数据的个数
private Object [] returnData; //返回的数据
private int foot = 0; //脚标控制
/**
* 进行数据的保存
* @param data 要保存的数据内容
* @exception NullPointerException 保存数据为空时抛出的异常
*/
public void add(Comparable<T> data) {
if (data == null) {
throw new NullPointerException("保存的数据不允许为空");
}
//所有的数据本身不具备节点的关系匹配,那么一定要将其包装在Node类之中
Node newNode = new Node(data); //保存节点
if (this.root == null) { //现在没有根节点,则第一个节点为根节点
this.root = newNode;
} else { //需要为其保存到一个合适的节点
this.root.addNode(newNode); //交由Node类处理
}
this.count ++;
}
/**
* 现在检索主要依靠的是Comparable实现的数据比较
* @param data 要比较的数据
* @return 查找到数据返回true,否则返回false
*/
public boolean contains(Comparable<T> data) {
if(this.count == 0){ //还没有数据
return false;
}
return this.root.containsNode(data); //该操作一定要交给Node类完成
}
/**
* 以对象数组的形式返回全部数据,如果没有数据则返回null
* @return 全部数据
*/
public Object [] toArray(){
if (this.count == 0){
return null;
}
this.returnData = new Object[this.count]; //保存长度为数组长度
this.foot = 0; //脚标清零
this.root.toArrayNode(); //直接通过Node类负责
return this.returnData; //返回全部的数据
}
/**
* 执行数据的删除功能
* @param data 要删除的数据
*/
public void remove(Comparable<T> data){
if (this.root == null){
return;
} else {
if (this.root.data.compareTo((T)data) == 0){ //要删除的是根节点
Node moveNode = this.root.right; //移动的节点
while (moveNode.left != null){ //现在还有左边的节点
moveNode = moveNode.left; //一直向左找
} //就可以确定删除节点的右节点的最小的左节点
moveNode.left = this.root.left;
moveNode.right = this.root.right;
moveNode.parent = null;
this.root = moveNode;
} else {
Node removeNode = this.root.getRemoveNode(data); //找到要删除的节点
if (removeNode != null){ //找到要删除的对象信息
//情况一,没有任何的子节点
if (removeNode.left == null && removeNode.right == null){
removeNode.left = null;
removeNode.right = null;
removeNode.parent = null; //父节点直接断开引用
} else if (removeNode.left != null && removeNode.right == null) { //左边不为空
removeNode.parent.left = removeNode.left;
removeNode.left.parent = removeNode.parent;
} else if (removeNode.left == null && removeNode.right != null) { //右边不为空
removeNode.parent.left = removeNode.right;
removeNode.right.parent = removeNode.parent;
}else { //两边都不为空
Node moveNode = removeNode.right; //移动的节点
while (moveNode.left != null){ //现在还有左边的节点
moveNode = moveNode.left; //一直向左找
} //就可以确定删除节点的右节点的最小的左节点
removeNode.parent.left = moveNode;
moveNode.parent.left = null; //断开连接
moveNode.parent = removeNode.parent;
moveNode.right = removeNode.right; //改变原始右节点的指向
moveNode.left = removeNode.left;
}
}
}
this.count --;
}
}
}
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person person) {
return this.age - person.age;
}
}
这种数据结构的删除操作是非常繁琐的,所以如果不是必须的情况下不建议使用删除。
4、红黑原理简介
通过整个的二叉树的实现相信已经可以清楚二叉树的主要特点:数据查询的时候可以提供更好的查询性能,但是这中原始的二叉树的结构是有明显缺陷的,例如:当二叉树结构改变的时候(增加或删除)就有可能出现不平衡的问题
比如说每个二叉树节点都只有左子节点的二叉树
那么之前所谓解决二叉树性能问题的方式最终全部变为了null,也就是说要想达到良好效果的二叉树,那它首先应该是一个平衡二叉树,节点的层次深度应该相同
乳沟说所有的数据按照以上的结构进行保存,那么二叉树的检索操作执行效率一定是最高的,可是你的树需要可以忍受着这些频繁的增加或者删除操作。所以针对于二叉树有了进一步的设计要求:
- 红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为O(logn)
- 红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉树( symmetric binary B-trees)。后来,在1978年被Leo J.Guibas和 Robert Sedgewick修改为如今的“红黑树”。
enum Color{
Red , BLACK;
}
class BinaryTree<T> {
private class Node{
private T data;
private Node left;
private Node right;
private Node parent;
private Color color;
}
}
对于Node节点中的颜色标记也可以使用true或者false来实现,不一定非要使用枚举类,一个标准的红黑树的结构如下
①、红黑树的特点
- 每个节点或者是红色,或者是黑色
- 根节点必须是黑色
- 每一叶子节点是黑色
- Java 实现的红黑树将使用null来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的
- 如果一个节点是红色的,则它的子节点必须是黑色的
- 从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。若给定黑色节点的个数N,最短路径情况是连续的N个黑色,树的高度为一1;最长路径的情况为节点红黑相间,树的高度为2(N - 1)
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点数量
- 成为红黑树最主要的条件,后序的插入、删除操作都是为了遵守这个规定
红色节点之后绝对不可能是红色节点,但是没有说黑色节点之后不允许是黑色节点,允许黑 - 黑相连
主要是李荣这个红色节点与黑色节点实现均衡控制。简单点理解红黑树的结构就是为了可以进行左旋和右旋控制以保证数的平衡性
但是对于平衡性,还需要考虑数据增加的平衡以及数据删除的平衡,增加和删除都是需要对这棵树进行平衡修复的
②、数据插入平衡修复
在进行红黑树处理的时候为了方便操作都会将新的节点使用红色来进行描述,于是当设置根节点的时候就会违反“规则2”,那么这个时候只需要将接节点的颜色涂黑即可
数据插入处理
- 第一次插入,由于原树为空,所以只会违反红-黑树的规则2所以只要把根节点涂黑即可
- 如果插入节点的父节点是黑色的,那不会违背红-黑树的规则什么也不需要做;但是遇到如下三种情况时,就要开始变色和旋转了:
- 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点
在红黑树进行修复处理之中,它需要根据当前节点以及当前节点的父节点和叔叔节点之间的颜色来推断树是否需要进行修复处理
③、数据删除平衡修复
数据删除处理
- 删除操作后,如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红-黑树的规则
- 如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复
- 但是如果遇到以下四种情况,就需要通过变色或旋转来恢复红-黑树的平衡了:
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的)
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色
变色和旋转了:
- 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点
在红黑树进行修复处理之中,它需要根据当前节点以及当前节点的父节点和叔叔节点之间的颜色来推断树是否需要进行修复处理
③、数据删除平衡修复
数据删除处理
- 删除操作后,如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红-黑树的规则
- 如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复
- 但是如果遇到以下四种情况,就需要通过变色或旋转来恢复红-黑树的平衡了:
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的)
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色
在红黑树之中修复的目的是为了保证树结构中的黑色节点的数量平衡,黑色节点的数量平衡了,那么才可能得到“logn”的执行性能,但是修复的过程一方面是红黑的处理,另一方面就是黑色子节点保存层次
下一篇:类库使用案例分析