二叉树
二叉树的提出
对于Java开发中如果使用链表保存数据,在查询一个数据时,它的时间复杂度是O(n)如果数据量比较小,那没什么影响,如果数据量大于30,再用链表就会严重损耗程序的性能,所以只有尽可能的减少检索次数来进行优化,所以就可以使用二叉树来保存数据;
二叉树的结构是根节点和叶子节点(左右子树),数据大于根节点,就放在右子树,小于根节点就放在左子树;
二叉树的检索有三种,前序遍历、中序遍历(遍历后结果有序)、后续遍历
二叉树的构造与操作
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 ;
}
//实现节点数据的适当位置的存储
public void addNode(Node newNode) {
if(newNode.data.comparaTo((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.returnDta[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;//脚标控制
public void add(Comparable<T> data) {
if(data == null) {
throws new NullPointerException("保存的数据不能为空!");
}
//将数据包装在Node中
Node newNode = new Node(data);
if(this.root == null) {
this.root = newNode ;
}else{ //保存到合适的节点
this.root.addNode(newNode) ;//交由Node类负责
}
this.count++;
}
//以对象数组的行书返回所有数据
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;//返回全部的数据
}
}
二叉树数据的删除
二叉树的删除操作可以分为三种情况,删除的节点没有子节点,删除的节点仅有一个分枝,删除的节点有两个分枝。还有一种特殊情况,删除的节点是整个二叉树的根节点。
- 对于没有叶子节点的删除,直接将该节点与它的父节点断开连接即可;
- 对于只有一个分枝的节点,连接该节点的分枝节点和根节点就可以了;
- 对于有两个子树的节点,保留节点位置,将该节点的数据保存改为,它右子树中最左边节点的数据,并且删除最左边这个节点(最左边的节点无子树,删除方式同第一点)。参考下图:
代码实现:在上面的代码上增加
//执行数据的删除操作(在BinaryTree中)
public void remove(Comparable<T> data) {
Node removeNode = this.root.getRomveNode(data);//找到要删除的节点
if(removeNode != null){//找到要删除的对象信息了
//情况一:
if(removeNode.left == null && removeNode.right == null){
removeNode.parent.left = null;
removeNode.parent.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--;
}
}
//(在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 != nu11) {
return this . left . getRemoveNode(data) ;
} else {
return null ;
}
} else {
if (this.right != nu11) {
return this right getRemoveNode(data) ;
} else {
return null ;
}
}
}
关于根节点的删除操作:
if (this. root .data. compareTo((T)data) == 0) { // 要删除的是根节点
Node moveNode = this. root.right ; // 移动的节点
while (moveNode.1eft != nu1l) { // 现在还有左边的节点
moveNode = moveNode.left ; //一直向左找
} // 就可以确定删除节点的右节点的最小的左节点
moveNode.1eft = this. root.1eft ;
moveNode.right = this.root.right ;
moveNode . parent.left = nu1ll ;
this. root = moveNode ; / /改变根节点
}
红黑树基本原理
二叉树的引出原因在上面已经分析过了,但是,如果二叉树存储的数据全在左子树或者右子树,那么对数数据的查询操作性能也没有什么优化可说,所以引出了红黑树,以颜色(红黑)或者左旋(右旋)的操作,使二叉树保持平衡,使得咋最坏情况下,关于数据的查询操作时间复杂度也在O(logn)。
**红黑树:**红黑树本质上是一种二叉查找树,但它在= -叉查找树的基础上额外添加了一个标记(颜色) ,同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为O(logn)。
enum Color {//引入颜色,整体结构和二叉树没有什么不同
RED,BLACK ;
}
class BinaryTree<T> {
private class Node {
private T data ;
private Node parent ;
private Node left ;
private Node right ;
private Color colo ;
}
}
红黑数的特点
- 每个节点或者是黑色,或者是红色;
- 根节点必须是黑色;
- 每个叶子节点是黑色:Java实现的红黑树将使用null来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的;
- 如果一个节点是红色的,则它的子节点必须是黑色的:从每个根到节点的路径.上不会有两个连续的红色节点,但色节点是可以连续的。若给定黑色节点的个数N ,最短路径情况是连续的N个黑色,树
的高度为N- 1 ;最长路径的情况为节点红黑相间,树的高度为2(N - 1) ; - 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点数量:成为红黑树最主要的条件,后序的插入、删除操作都是为了遵守这个规定;
数据插入的修复
-
第一次插入,由于原树为空,所以只会违反红-黑树的规则2,所以只要把根节点涂黑即可;
-
如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做;
但是遇到如下三种情况时,就要开始变色和旋转了:
- 插入节点的父节点和其叔叔节点(祖父节点的另-个子节点)均为红
色的; - 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点
的左子节点。 - 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点
的在子节点.
- 插入节点的父节点和其叔叔节点(祖父节点的另-个子节点)均为红
数据插入平衡处理规则:
-
插入节点的父节点和其叔叔节点均为红色的;
- 将当前节点的父节点以及叔叔节点涂黑;
-
插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其度节点的左子节点;
- 将新增节点的父节点鱼祖父节点进行互换,随后采用右旋处理。
-
插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点,此时新节点为左节点,这样就可以按照情况二进行处理。
- 将新增节点与父节点进行左旋处理,将新节点变为左节点,随后参考上面的第二点。
数据删除的修复
- 删除操作后I如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红黑树的规则。
- 如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复。
- 但是如果遇到以下四种情况,就需要通过变色或旋转来恢复红黑树的平衡了:
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的) ;
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为色的;
- 当前节点是黑色的,且兄弟节点是黑色的,且貺弟节点的左子节点是红色,右子节点时黑色的;
- 当前节点是黑色的,貺弟节点是黑色的,脱弟节点的右子节点是红色,左子节点任意颜色。
数据删除平衡处理规则
-
当前节点是黑色的,且兄弟节点是红色的(父节点和兄弟节点的子节点肯定是黑色的);
- 当前节点是删除后重新保存的节点信息
- 将当前节点的父节点涂红将兄弟节点涂黑,然后将当前节点的父节点作为左旋支点;
-
当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的。
- 将当前节点的兄弟节点涂红,将当前节点指向其父节点,将当前节点的父节点指向其祖父节点,不旋转
-
当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是色的。
- 将当前节点的兄弟节点涂红,把兄弟节点的左子节点涂黑,然后以兄弟节点作为支点进行右旋。
-
当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。
- 将当前节点的兄弟节点涂成与父节点相同的颜色,再把父节点涂成黑色,把兄弟节点的右子节点涂黑,然后以当前节点的父节点为支点进行左旋。