比较器
所谓比较器指的就是大小关系的确定判断。
比较器问题引出
如果要进行数组操作,肯定要使用java.util.Arrays的操作类完成,这个类里面提供绝大部分的数组操作支持,同时这个类中提供有对象数组的排序支持:public static void sort(Object[] a);
实现对象数组排序
import java.util.Arrays;
public class Comparable_Study {
public static void main(String[] args) {
Integer data [] = new Integer [] {
10, 9, 20,2,5}; //对象数组
Arrays.sort(data);
System.out.println(Arrays.toString(data));
}
}
String型对象数组也可以进行排序;
java.lang.Integer与java.lang.String两个类都是系统提供的程序类,但如果有一个自定义类需要实现排序处理?
自定义类排序处理
任意一个类默认情况下是无法使用系统内部类进行数组排序或比较需求的,是因为没有明确指出如何进行比较,那么Java为了统一比较的规则,所以提供有比较器的接口:Comparable接口。
Comparable比较器
通过分析可以发现如果要实现对象的比较肯定需要有比较器制定规则,而比较的规则就通过Comparable来实现,对于Comparable而言,需要知道其定义结构:
public interface Comparable<T>{
/**
* 实现对象比较处理操作
* @param o 要比较的对象
* @return 当前数据-传入对象
*/
public int compareTo(T o);
}
import java.util.Arrays;
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=" + this.name + ", age=" + this.age + "]";
}
@Override
public int compareTo(Person per) {
return this.age - per.age;
}
}
public class Comparable_Study {
public static void main(String[] args) {
Person per [] = new Person[] {
new Person("xiaoqiang",60),
new Person("xiaoqiang1",70),
new Person("xiaoqiang2",80),
};
Arrays.sort(per);
System.out.println(Arrays.toString(per));
}
}
排序中只需要有一个compareTo()方法进行规则的定义,而后整个Java系统里就可以为其实现排序处理了。
Comparator比较器
Comparator属于一种挽救的比较器支持,其主要目的是解决一些没有使用Comparable排序的类的对象数组排序操作。
先期设计未考虑排序功能,经过版本更新后需要对Person排序,但无法修改Person类,此事后需要使用挽救的形式来进行排序,在Arrays类里面排序有另外一种实现:
- 基于Comparator的排序处理;
public staitc <T> void sort(T[] a,Comparator<? super T> c);
在java.util.Comparator里面最初自定义一个排序的compare()方法:public int compare(T o1,T o2);
挽救类型排序规则类
import java.util.Arrays;
import java.util.Comparator;
class PersonComparator implements Comparator<Person>{
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
}
class 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=" + this.name + ", age=" + this.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;
}
}
public class Comparable_Study {
public static void main(String[] args) {
Person per [] = new Person[] {
new Person("xiaoqiang",60),
new Person("xiaoqiang1",70),
new Person("xiaoqiang2",80),
};
Arrays.sort(per, new PersonComparator());
System.out.println(Arrays.toString(per));
}
}
对于这种排序的操作如果不是必须的情况下强烈不建议使用Comparator,最好以Comparable为主。
面试:请解释Comparable和Comparator的区别
- java.lang.Comparable是类在定义的时候实现的父接口,主要用于定义排序规则,里面只有compareTo()方法;
- java.util.Comparator是挽救的比较器操作,需要单独设置比较器规则类实现排序,里面有compare()方法;
二叉树结构简介
在进行链表结构的开发过程中,发现所有的数据按照首尾相连的状态进行保存,那么当要进行某一个数据查询的时候(判断数据是否存在),这种情况下时间复杂度是“O(n)”,但数据量很大的时候,此时时间复杂度就会严重影响程序性能,此时存储结构就应该改变,应该尽可能的减少检索次数,对于现在的数据结构而言,最好的性能就是“O(logn)”,要想实现他就可以利用二叉树的结构来完成。
如果要想实现一棵树的定义,那么就需要考虑数据的存储形式,在二叉树的实现之中基本的实现原理如下,取第一个数据为保存的根节点,小于等于根节点的数据放在节点的左子树,大于节点的数据要放在节点的右子树。
此时要进行数据检索的话,就需要进行每个节点的判断,但是该判断是区分左右的,所以不会是整个结构都进行遍历,所以该结果的时间复杂度为O(logn)。
而对于二叉树而言,在进行数据获取的时候也有三种形式:==前序遍历(根-左-右)、中序遍历(左-根-右)、后序遍历(左-右-根);==若以中序为主,则以上的数据进行遍历时:10-20-25-30-38-50-80-100,可以发现二叉树的内容都属于排序的结果。
二叉树基础实现
实现二叉树的处理之中最为关键性的问题在于数据的保存,而且数据由于牵扯到对象比较的问题,那么一定要有比较器的支持,这个比较器首选的一定就是Comparable。所以本次将保存一个Person类数据。
随后如果想要进行数据的保存,首先一定需要有节点类:节点类里面牵扯到数据的保存问题,所以必须使用Comparable(可以区分大小)。
import java.util.Arrays;
/**
* 实现二叉树的数据操作
* @author Liu Yizhu
*
* @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;
}
@Override
public int compareTo(Person per) {
return this.age - per.age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]\n";
}
}
public class Erchashu_Study {
public static void main(String[] args) {
BinaryTree<Person> bt = new BinaryTree<Person>();
bt.add(new Person("lyz",60));
bt.add(new Person("lyz",70));
bt.add(new Person("lyz",80));
bt.add(new Person("lyz",90));
System.out.println(Arrays.toString(bt.toArray()));
}
}
在进行数据添加的时候只是实现了节点关系的保存,而这种关系保存后的结果就是所有的数据都属于有序排列。
二叉树数据删除
二叉树中数据删除是非常复杂的,因为在进行数据删除的时候需要考虑的情况很多:
- 若待删除节点没有子节点,直接删除即可;
- 若待删除节点只有一个子节点,那么直接删掉,并用其子节点去顶替它;
- !!! 若待删除节点有两个子节点:首先找到他的后继节点,然后处理“后继节点”和“被删除节点的父节点”之间的管,最后处理“后继节点的子节点”和“被删除节点的子节点”之间的关系;
实现二叉树数据删除
- 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;
}
}
}
整体代码:包括删除/增添/判断
import java.util.Arrays;
/**
* 实现二叉树的数据操作
* @author Liu Yizhu
*
* @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 检索结果
*/
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 是否查到
*/
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.left = null;
this.root = moveNode;
this.count --;
} 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;
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;
}
@Override
public int compareTo(Person per) {
return this.age - per.age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]\n";
}
}
public class Erchashu_Study {
public static void main(String[] args) {
BinaryTree<Person> bt = new BinaryTree<Person>();
bt.add(new Person("lyz",80));
bt.add(new Person("lyz",50));
bt.add(new Person("lyz",60));
bt.add(new Person("lyz",90));
bt.add(new Person("lyz",30));
bt.add(new Person("lyz",10));
bt.add(new Person("lyz",55));
bt.add(new Person("lyz",70));
bt.add(new Person("lyz",85));
bt.add(new Person("lyz",95));
bt.remove(new Person("lyz",80));
System.out.println(Arrays.toString(bt.toArray()));
}
}
这种数据结构的删除操作是非常繁琐的,如果不是必须的情况下不建议使用删除。
红黑树原理简介
通过整个二叉树的实现已经可以清楚二叉树的主要特点:数据查询的时候可以提供更好的查询性能,但是这种原始的二叉树结构是由明显缺陷的,例如当二叉树结构改变的时候(增加或删除)就可能出现不平衡的问题。
如果想要达到最良好效果的二叉树,同时所有节点深度应该相同。
如果所有的数据按照以上结构进行保存,那么二叉树的执行效率(检索)是最高的。可是该树需要频繁的增加或删除,所以针对于二叉树有了进一步要求:
- 红黑树本质是一种二叉查找树,在二叉树基础上添加了一种标记,同时具有一定的规则,这些规则使红黑树保证一种平衡;
- 每个节点都有颜色;
- 根节点一定是黑色的;
- 每个叶子节点是黑色的;
- Java实现红黑树将使用null来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到的每个叶子节点都是红色的。
- 如果一个节点是红色的,则它的子节点必须是黑色的;
- 从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。若给定的黑色节点个数为N,最短路径情况是连续的N个黑色,树的高度为N-1,最长路径的情况为节点红黑相间,数的高度为2(N-2)。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点数量;
- 成为红黑树最主要的条件,后序的插入、删除操作都是为了遵循这个规定。
注: 红色节点之后绝不可能是红色节点,但是黑色节点后可以是黑色节点,主要是实现均衡配置。
但是对于平衡性还要考虑数据删除和增加的平衡,增加和删除后都需要对红黑树进行修复。
数据插入平衡
- 在进行红黑树处理的时候为了方便操作都会将新的节点使用红色来描述,于是当设置根节点时就会违反规则2,此时只需将该节点涂黑即可。
- 若插入节点的父节点是黑色的,则无需改变红黑树,但遇到以下三种情况下,就要开始变色和旋转:
- 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色;
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点;
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;
数据删除处理
- 删除操作后,若当前节点是黑色的根节点,那么不用任何操作,因为并未破坏树的平衡,即并未违背红-黑树规则;
- 若当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后续节点的父节点是什么颜色,只需要将当前节点涂黑即可;
- 一下四种情况需要通过变色和旋转进行平衡:
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的);
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的;
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是黑色的;
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是任意颜色,右子节点是红色的;
为了方便操作都会将新的节点使用红色来描述,于是当设置根节点时就会违反规则2,此时只需将该节点涂黑即可。
- 若插入节点的父节点是黑色的,则无需改变红黑树,但遇到以下三种情况下,就要开始变色和旋转:
- 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色;
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点;
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;
数据删除处理
- 删除操作后,若当前节点是黑色的根节点,那么不用任何操作,因为并未破坏树的平衡,即并未违背红-黑树规则;
- 若当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后续节点的父节点是什么颜色,只需要将当前节点涂黑即可;
- 一下四种情况需要通过变色和旋转进行平衡:
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的);
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的;
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是黑色的;
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是任意颜色,右子节点是红色的;