二叉树结构简介
在进行链表结构开发的过程之中会发现所有的数据按照首尾相连的状态进行保存,当要进行某一个数据查询的时候(判断该数据是否存在),这种情况下它所面对的时间复杂度为"O(n)“,如果说现在它的数据量小(不超过30个)情况下性能上是不会有太大差别的,而一旦保存的数据量很大,这个时候时间复杂度就会严重损耗程序的运行性能,那么现在对于数据的存储结构就必须发生改变,应该可以尽可能的减少检索次数为出发点进行设计,对于现在的数据结构而言,最好的性能就是"O(logn)”,现在要想实现它就可以利用二叉树的结构来完成。
如果要想实现一棵树结构的定义,那么就需要去考虑数据的存储形式,在二叉树的实现之中其基本的实现原理如下:取第一个数据为保存的根节点,小于等于根节点的数据要放在节点的左子树,而大于节点的数据要放在该节点的右子树
如果要进行数据检索的话,此时就需要进行每个节点的判断,但是它的判断是区分左右的,所以不会是整个结构都进行判断处理,那么它的时间复杂度就是O(logn)
而对于二叉树而言,在进行数据获取的时候也有三种形式:前序遍历(根-左-右),中序遍历(左-根-右),后续遍历(左-右-根),那么现在只是以中序遍历为主,则以上的数据进行中序遍历的时候最终的结果:10,20,25,30,38,50,80,100,可以发现二叉树中的内容全部都属于排序的结果
class Person {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// setter,getter省略
@Override
public String toString() {
return "[Person类对象]姓名:" + this.name + "\t年龄:" + this.age + "\n";
}
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;
}
}
二叉树基础实现
在实现二叉树的处理之中最为关键性的问题是在于数据的保存,而且数据由于牵扯到对象比较的问题,那么一定要有比较器的支持,而这个比较器首选的一定就是Comparable,所以本次将保存一个Person类数据:
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// setter,getter省略
@Override
public String toString() {
return "[Person类对象]姓名:" + this.name + "\t年龄:" + this.age + "\n";
}
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 int compareTo(Person per) {
return this.age - per.age; // 升序排序
}
}
/**
* 实现二叉树操作
* @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) { // compareTo()默认是升序排序,已有的减新的- 调用者减被调用者
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++;
}
/**
* 以对象数组的形式返回全部数据,如果没有数据返回空
* @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 JavaAPIDemo02 {
public static void main(String args[]) {
BinaryTree<Person> tree = new BinaryTree<Person>();
tree.add(new Person("小强-30", 30));
tree.add(new Person("小强-50", 50));
tree.add(new Person("小强-60", 60));
tree.add(new Person("小强-90", 90));
System.out.println(Arrays.toString(tree.toArray()));
}
}
随后如果要想进行数据的保存,首先一定需要哦有一个节点类。节点类里面由于牵扯到数据的保存问题,所以必须使用Comparable(可以区分大小)
在进行数据添加的时候只是实现了节点关系的保存,而这种关系保存后的结果就是所有的数据都属于有序排列。
二叉树节点删除
二叉树之中的数据删除操作是非常复杂的,因为在进行数据删除的时候需要考虑的情况是比较多的:
1.如果待删除的节点没有子节点,那么直接删掉即可
2.如果待删除节点只有一个子节点,那么直接删掉,并用其子节点去顶替它,这个时候考虑两种情况分析,
只有一个左子树
只有一个右子树的情况
3.如果待删除节点有两个子节点,这种情况比较复杂:首选找出它的后继节点,而后处理"后继节点"与"被删除节点的父节点"之间的关系,最后处理"后继节点的子节点"和"被删除节点的子节点"之间的关系
范例:在BinaryTree类进行节点的处理
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// setter,getter省略
@Override
public String toString() {
return "[Person类对象]姓名:" + this.name + "\t年龄:" + this.age + "\n";
}
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 int compareTo(Person per) {
return this.age - per.age; // 升序排序
}
}
/**
* 实现二叉树操作
*
* @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) { // compareTo()默认是升序排序,已有的减新的- 调用者减被调用者
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();
}
}
/**
* 查询节点是否存在
* @return 找到返回ture,找不到返回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++;
}
/**
* 以对象数组的形式返回全部数据,如果没有数据返回空
*
* @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
* @return
*/
public boolean contains(Comparable<T> data){
return root.containsNode(data);
}
/**
* 执行数据的删除处理
*
* @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.left = null;
this.root = moveNode; //改变根节点
}else{
Node removeNode = this.root.getRemoveNode(data);// 找到要删除的节点
if (removeNode != null) { // 找到要删除的对象信息
// 情况一:要移除的节点没有任何的子节点
if (removeNode.left == null && removeNode.right == null) {
removeNode.parent.left = null;
removeNode.parent.right = 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 JavaAPIDemo {
public static void main(String args[]) {
BinaryTree<Person> tree = new BinaryTree<Person>();
tree.add(new Person("小强-80", 80));
tree.add(new Person("小强-50", 50));
tree.add(new Person("小强-60", 60));
tree.add(new Person("小强-90", 90));
tree.add(new Person("小强-30", 30));
tree.add(new Person("小强-10", 10));
tree.add(new Person("小强-55", 55));
tree.add(new Person("小强-85", 85));
tree.add(new Person("小强-95", 95));
System.out.println(Arrays.toString(tree.toArray()));
tree.remove(new Person("小强-30", 80));
System.out.println(tree.contains(new Person("小强-30", 30)));
System.out.println(Arrays.toString(tree.toArray()));
}
}
这种数据结构的删除操作是非常繁琐的,所以如果不是必须的情况下不建议使用删除。
增加,删除,查询是数据结构里必须会的三种形式.
红黑二叉树
通过整个的二叉树的实现相信已经可以清楚二叉树的主要特点:数据查询的时候可以提供更好的查询性能,但是这种原始的二叉树1的结构是有明显缺陷的,例如:二叉树结构改变的时候(增加或删除)就有可能出现不平衡的问题
之前所谓的解决二叉树性能问题的方式最终全部都变为了null,也就是说如果要达到最良好效果的二叉树,那么它首先应该是一个平衡二叉树,同时所有的节点的层次深度应该相同
如果所有的数据按照以上的结构进行保存,那么二叉树的检索操作执行效率一定是最高的,可是你的树需要可以忍受住这些频繁的增加或者是删除操作。所以针对于二叉树有了进一步设计要求:
红黑树的本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为O(logn)
红黑树是在1972年由RudoIf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被Leo J.Guibas和Robert Sedgewick修改为如今的"红黑树"
在java的类及开发框架里面大量的采用了红黑树的结构,这也是为什么红黑树现在会成为面试的主要原因,这种改变从JDK1.8开始
红黑树的本质就是在节点上追加了一个表示颜色的操作信息而已
enum Color{
RED,BLACK;
}
class BinaryTree{
private class Node{
private T data;
private Node parent;
private Node left;
private Node right;
private Color color; // private boolen color
}
}
对于Node节点中的颜色标记也可以使用true或false来实现,不一定非要使用枚举类,一个标准的红黑树结构如下所示:
这是以后面试中回答红黑树面试题的关键所在,也可以帮助以后更好的理解java类库,将其中思想领会
红色节点之后绝对不可能是红色节点,但是没有说黑色节点之后不允许是黑色节点,允许黑-黑连接
主要是利用红色节点与黑色节点实现均衡的控制。简单点理解红黑树的结构就是为了可以进行左旋和右旋控制,以保证树的平衡性
但是对于平衡性,还需要考虑数据增加的平衡以及数据删除的平衡,增加和删除都是需要对这颗树进行平衡修复的
数据插入平衡修复
在进行红黑树处理的时候为了方便操作都会将新的节点使用红色来进行描述,于是当设置根节点的时候就会违反"规则二",那么这个时候只需要将节点颜色涂黑即可
在红黑树进行修复的处理之中,它需要根据当前节点以及当前节点的父节点和叔叔节点之间的颜色来推断树是否需要进行修复处理
数据删除平衡修复
在红黑树之中修复的目的是为了保证树结构中的黑色节点的数量平衡,黑色节点的数量平衡了,那么才可能得到"()O)logn"的执行性能,但是修复的过程一方面是红黑的处理,另一方面你就是黑色子节点的保存层次
学习来源
阿里云开发者社区