应用场景:
定义了Student类,学生有体重和身高两个属性,并且学生类实现了comparable接口,可以按体重升序排列,那么在Student类的使用中想对排序规则做出改变应该如何实现呢?
public class Student implements Comparable<Student>{
int weight;
int height;
public Student(int weight, int height) {
this.weight = weight;
this.height = height;
}
//按体重升序
@Override
public int compareTo(Student o) {
return this.weight - o.getWeight();
}
...
wieght 和 height 的get和set方法
...
@Override
public String toString() {
return "Student{" +
"weight=" + weight +
", height=" + height +
'}';
}
}
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student(40,140));
list.add(new Student(45,125));
list.add(new Student(42,135));
list.add(new Student(43,115));
list.add(new Student(41,130));
//按体重升序排列
Collections.sort(list);
System.out.println(list);
//按身高升序排列
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getHeight() - o2.getHeight();
}
});
System.out.println(list);
//按身高降序排列
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o2.getHeight() - o1.getHeight();
}
});
System.out.println(list);
}
//按体重升序排列:
[Student{weight=40, height=140}, Student{weight=41, height=130}, Student{weight=42, height=135}, Student{weight=43, height=115}, Student{weight=45, height=125}]
//按身高升序排列:
[Student{weight=43, height=115}, Student{weight=45, height=125}, Student{weight=41, height=130}, Student{weight=42, height=135}, Student{weight=40, height=140}]
按身高降序排列:
[Student{weight=40, height=140}, Student{weight=42, height=135}, Student{weight=41, height=130}, Student{weight=45, height=125}, Student{weight=43, height=115}]
如果要实现按体重降序,那么只要将compareTo方法的返回值修改为 o.getWeight() - this.weight 就能达到目的,那么影响排序规则的因素就是两个相减对象的顺序:
o1.getHeight() - o2.getHeight() ———>按体重升序排列
o2.getHeight() - o1.getHeight() ———>按体重降序排列
那么这个返回值是如何影响排序规则的,接下来将从源码进入一探究竟 。
1.在返回值处设置断点调试
2. 进入了一个名为countRunAndMakeAscending的方法,Object[ ] a 内存放的是Student对象,对象数组下标范围为 [lo, hi),不包括下标hi,而runHi是用来标记集合中从lo开始最长的升序序列的末尾的后一位元素,也就是升序数组的范围是[lo, runHi),返回值(runHi - lo)是升序序列的长度。
最后方法的返回值为2,因为集合中从lo(下标为0的元素)按体重升序的只有[40,45],45到42已经是降序, 所以集合中从最低位开始,按体重升序的不间断的升序序列只有前两个元素,返回2 。
3. initRunLen接收了countRunAndMakeAscending方法的返回值,即对象数组内从lo开始的升序序列长度,将initRunLen的结果作为binarySort方法的输入参数,binarySort是二分插入排序。
4. 在直接插入排序过程中,待插入数据左边的序列总是有序的,针对有序序列,就可以用二分法去查找插入点,这就是二分插入排序,而countRunAndMakeAscending方法的作用就是统计对象数组左边有序序列的长度。
如果想了解二分插入排序更详细的步骤,建议查看
binarySort的start参数是lo + initRunLen的结果,即对象数组a的有序序列部分为[lo, start),无序序列部分为[start, hi), binarySort执行的四个步骤:
1)取出无序数组中的待插入元素 pivot
2)二分查找找到在有序数组中的插入位置
3)将有序数组插入位置之后的元素后移
4)插入pivot
private static void binarySort(Object[] a, int lo, int hi, int start) {
assert lo <= start && start <= hi;
if (start == lo)
start++;
for ( ; start < hi; start++) {
//1.取出无序数组中的待插入元素 pivot
Comparable pivot = (Comparable) a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
assert left <= right;
/*
* Invariants:
* pivot >= all in [lo, left).
* pivot < all in [right, start).
*/
//2.二分查找找到在有序数组中的插入位置 left
while (left < right) {
int mid = (left + right) >>> 1;
***问题的关键***
if (pivot.compareTo(a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
assert left == right;
/*
* The invariants still hold: pivot >= all in [lo, left) and
* pivot < all in [left, start), so pivot belongs at left. Note
* that if there are elements equal to pivot, left points to the
* first slot after them -- that's why this sort is stable.
* Slide elements over to make room for pivot.
*/
int n = start - left; // The number of elements to move
// Switch is just an optimization for arraycopy in default case
//3.将有序数组插入位置之后的元素后移
switch (n) {
case 2: a[left + 2] = a[left + 1];
case 1: a[left + 1] = a[left];
break;
default: System.arraycopy(a, left, a, left + 1, n);
}
//4.插入
a[left] = pivot;
}
}
当前 pivot 指向weight为42的Student对象,而最后找到的插入点left指向对象数组下标为1的Student对象,weight = 45
把下标为1的Student对象后移到下标为2的位置
替换left指向位置的元素为 pivot,一轮排序完成,而后pivot = a[3], 继续查找插入完成全部排序
那么排序的基本过程了解了之后,回到问题的关键,两个相减对象的顺序是如何影响排序结果的呢?
在二分查找的过程中,pivot.compareTo(a[mid]) 的返回值是 this.weight - o.getWeight(); pivot 对应this,a[mid] 对应 o 。 就是比较 pivot 和 a[mid]的大小,如果 pivot 比 a[mid]小,即 pivot.compareTo(a[mid]) < 0 ,那么将执行 right = mid ;缩小二分查找右边界的范围,得到新的mid值后继续比较,最后会将比中间值 ( a[mid] ) 小的元素插入到有序数组的前部分,这就形成了升序排列。
如果交换了 this.weight - o.getWeight() 的相减顺序,变成 o.getWeight() - this.weight ,就等于 - ( this.weight - o.getWeight() ),那么pivot.compareTo(a[mid])得到的返回值就会变成相反的值,pivot - a[mid] < 0的返回值变成 > 0,那么将执行 left = mid + 1,缩小二分查找左边界的范围,最后查找到的待插入位置在有序数组的后半部分,把小于中间值的元素插入到有序数组的后半部分就是降序排列。
while (left < right) {
int mid = (left + right) >>> 1;
if (pivot.compareTo(a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
//Student对象中重写的compareTo方法
@Override
public int compareTo(Student o) {
return this.weight - o.getWeight();
}
P.S:
Comparator的compare方法返回值影响升序降序的原因相同, 同样是 pivot - a[mid] 的值大小
while (left < right) {
int mid = (left + right) >>> 1;
// c就是Comparator
if (c.compare(pivot, a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getHeight() - o2.getHeight();
}
});