java中Comparator比较器顺序问题,源码分析
提示:
分析过程是个人的一些理解,如有不对的地方,还请大家见谅,指出错误,共同学习。
源码分析过程中由于我写的注释比较啰嗦、比较多,导致文中源代码不清晰,还请一遍参照源代码,一遍参照本文进行阅读。
原理:先将集合中的部分元素排列好顺序。 然后再将剩余的元素用二分法插入到已排好序(二分法的使用是建立在已排好序的前提下)的元素中去。然后得到排好序的集合。
测试代码:
- public class TestLambda {
- public static List<String> list = Arrays.asList("my","name","is","lambda","mzp");
- public static List<Integer> integerList = Arrays.asList(1,2,15,6,9,13,7);
- public static void main(String[] args) {
- System.out.println("排序前:");
- printList(integerList);
- oldIntegerSort();
- System.out.println("\noldSort排序后:");
- printList(integerList);
- }
- /**
- * @Author maozp3
- * @Description: 对String类型的lis就行排序。使用老方法(外部比较器Comparator)
- * @Date: 14:51 2019/7/5
- * @Param []
- * @return void
- **/
- public static void oldIntegerSort(){
- //排序(匿名函数)
- Collections.sort(integerList, new Comparator<Integer>(){
- //使用新的排序规则。比较器排序。
- // 原理,先确定部分元素的顺序(升序或是降序),然后把剩余的元素通过"二分插入"进行排序。
- @Override
- public int compare(Integer a, Integer b) { //源码中第一个入参(a)是数组靠后面的数,第二个入参(b)是数组靠前面的数(比如这里:a=2,b=1)
- if(a <= b){ //由条件加上返回值来确定是升序还是降序 (如果全部返回-1的话,则实现逆序,将集合中的元素顺序颠倒)
- return 1; //比如这里:原数组后面a的数小于前面的数b,返回1,1则表示这个顺序不需要调整。
- }else{
- return -1; //比如这里:原数组后面的数a小于前面的数b,返回-1,-1则表示数组中现在的顺序需要调整。根据我们的代码,前两个元素是1和2,判断条件if(2<=1),返回的是-1,即不满足我们的期望。 下面排序的时候就会对顺序进行调整了。
- }
- }
- });
- }
- /**
- * @Author maozp3
- * @Description: 打印集合元素
- * @Date: 10:38 2019/7/8
- * @Param [list]
- * @return void
- **/
- public static <T> void printList(List<T> list){
- Iterator<T> iterator = list.iterator();
- while(iterator.hasNext()){
- System.out.print(iterator.next()+",");
- }
- }
51 }
重写 Comparator 中的 compare 方法。自定义的比较规则:我这里自定义的比较器是: 期望得到 逆序排列
判断条件是:当a<=b时,返回1。否则返回-1;意思就是我期望的是“后一个元素比前一个元素小(即降序)”,如果已经满足,就不需要调换顺序(返回1),如果不满足,就需要调换一下顺序(返回-1)。
源码分析时,有一个我自己定义的“理念”,对于这个“理念”,这里有一个比较好的理解方法: a和b作比较时,a是比较者(主动),b就相当于是参照物(被比较者、被动)。
下面源码分析时用到这个所谓的“理念”的时候,会用到“主动”和“被动”这两个说辞。
测试数据:1,2,15,6,9,13,7
- public static void oldIntegerSort(){
- //排序(匿名函数)
- Collections.sort(integerList, new Comparator<Integer>(){
- //使用新的排序规则。比较器排序。 原理,先确定部分元素的顺序(升序或是降序),然后把剩余的元素通过"二分插入"进行排序。
- @Override
- public int compare(Integer a, Integer b) { //源码中第一个入参(a)是数组靠后面的数,第二个入参(b)是数组靠前面的数(比如这里:a=2,b=1)
- if(a <= b){ //由条件加上返回值来确定是升序还是降序 (如果全部返回-1的话,则实现逆序,将集合中的元素顺序全部颠倒,下面会有说明原因)
- return 1; //比如这里:原数组后面的数小于前面的数,返回1,1则表示数组中现在的顺序不需要调整。
- }else{
- return -1; //比如这里:原数组后面的数小于前面的数,返回-1,-1则表示数组中现在的顺序需要调整。
- }
- }
- });
- }
先说一下个人总结的结论:
最终排序结果由判断条件(上面代码第7行)、返回值(上面代码第8行或第9行)来决定。
重写的compare(a,b)方法的两个入参中,第一个入参a表示集合元素中相邻元素靠后的那一个;第二个入参b表示集合元素中相邻元素靠前的那一个(原因在下面源码分析中给出)。也就是说元素a的下标大于元素b的下标。
判断条件:a<=b 希望后一个元素比前一个元素小,即期望降序; a>=b 希望后一个元素比前一个元素大,即期望升序
返回1:就表示集合中元素目前的顺序满足判断条件里面期望的顺序,不需要调整;
返回-1:就表示集合中元素目前的顺序不满足判断条件里面期望的顺序,要进行调整。
比如。判断条件是:当a<=b时,返回1。否则返回-1;意思就是我期望的是“后一个元素比前一个元素小(即降序)”,如果已经满足,就不需要调换顺序(返回1),如果不满足,就需要调换一下顺序(返回-1)。
我这里集合元素为:1,2,15,6,9,13,7 当比较顺序时,a是等于2的,b是等于1的,if(a<=b) 结果返回的是 -1,表示我期望的是降序而实际情况目前是升序。所以要进行调整。至于后面的数还要不要继续调整位置,则还要继续进行判断。但首先可以肯定的是,程序后面一定会执行调换元素位置的操作,而且2肯定在1的前面。
下面是通过源码分析一下过程
测试数据:1,2,15,6,9,13,7。 并且重写重写了比较器,我们期望的结果是降序排列
1.在主方法中调用 oldIntegerSort(); 方法,对集合进行排序
- public static void main(String[] args) {
- System.out.println("排序前:");
- printList(integerList);
- oldIntegerSort();
- System.out.println("\noldSort排序后:");
- printList(integerList);
- }
2.调用 oldIntegerSort()中的,调用 Collections.sort(integerList, new Comparator<Integer>(){...} ) 。进入Collections.sort(list,比较器)方法中
- public static void oldIntegerSort(){
- //排序(匿名函数)
- Collections.sort(integerList, new Comparator<Integer>(){
- //使用新的排序规则。比较器排序。
- // 原理,先确定部分元素的顺序(升序或是降序),然后把剩余的元素通过"二分插入"进行排序。
- @Override
- public int compare(Integer a, Integer b) { //源码中第一个入参(a)是数组靠后面的数,第二个入参(b)是数组靠前面的数(比如这里:a=2,b=1)
- if(a <= b){ //由条件加上返回值来确定是升序还是降序 (如果全部返回-1的话,则实现逆序,将集合中的元素顺序颠倒)
- return 1; //比如这里:如果原数组后面的数小于等于前面的数,返回1,1则表示这个顺序不需要调整。
- }else{
- return -1; //比如这里:如果原数组后面的数大于前面的数,返回-1,-1则表示数组中现在的顺序需要调整。根据我们的测试数据,前两个元素是1和2,这里if(2<=1,返回的是-1,不满足我们的期望,后面排序的时候就会对这俩元素的位置进行调整了
- }
- }
- });
- }
3. Collections.sort(list,比较器)的源码 (从这里开始为jdk的源码)
- @SuppressWarnings({"unchecked", "rawtypes"})
- public static <T> void sort(List<T> list, Comparator<? super T> c) {
- list.sort(c);
- }
4. 进入list.sort(c); 这里面调用了 Arrays.sort(a, (Comparator) c);
- @SuppressWarnings({"unchecked", "rawtypes"})
- default void sort(Comparator<? super E> c) {
- Object[] a = this.toArray();
- Arrays.sort(a, (Comparator) c);
- ListIterator<E> i = this.listIterator();
- for (Object e : a) {
- i.next();
- i.set((E) e);
- }
- }
5.进入 Arrays.sort(a, (Comparator) c); 这里面调用 TimSort.sort(a, 0, a.length, c, null, 0, 0);
- public static <T> void sort(T[] a, Comparator<? super T> c) {
- if (c == null) {
- sort(a);
- } else {
- if (LegacyMergeSort.userRequested)
- legacyMergeSort(a, c);
- else
- TimSort.sort(a, 0, a.length, c, null, 0, 0);
- }
- }
6. 进入 TimSort.sort(a, 0, a.length, c, null, 0, 0); 这里面代码比较多,(目前理解的不是很深,请见谅),只列出关键代码。
这里的 countRunAndMakeAscending(a, lo, hi, c); 是排序的关键;
- binarySort(a, lo, hi, lo + initRunLen, c); 是对排序后剩余的元素进行二分插入 的关键。
- // If array is small, do a "mini-TimSort" with no merges
- if (nRemaining < MIN_MERGE) {
- int initRunLen = countRunAndMakeAscending(a, lo, hi, c); //这个方法是决定集中合部分元素的顺序。这里的入参a是要排列的集合;lo是集合的第一个元素的下标,是0;hi是集合的总长度length;c是自定义的比较器
- binarySort(a, lo, hi, lo + initRunLen, c); //这个方法是将剩余的元素通过二分法插入到排好序的那部分元素中去
- return;
- }
7.进入 countRunAndMakeAscending(a, lo, hi, c);中。
执行前: 1,2,15,6,9,13,7
执行后: 15,2,1,6,9,13,7
我们测试数据目前是 1,2,15,6,9,13,7 这个顺序,我们期望的是降序。在第10行的if判断时,返回的就是-1;然后在while循环中继续判定还有多少个元素的顺序不符合我们的期望,全部找出来并进行位置调换。
执行完之后,我们集合的数据就变成了 15,2,1,6,9,13,7. 可以看出,前三个不满足我们的期望,对他们进行了位置调换(将与期望的完全相反的顺序进行调换之后,就变成了期望的顺序)。从第4个元素开始,满足了我们的期望,因为当比较if(a<=b)时发现6<=15是成立的,就返回了1。然后就确定了从第4个元素(也就是6这个元素)开始往后的所有元素都是要通过二分法进行插入排序了。
- private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
- Comparator<? super T> c) {
- assert lo < hi; //这里lo表示集合的第一个元素,也就是0; hi表示集合的大小。 assert是个断言,如果不满足就会抛出AssertionError异常,并终止执行。
- int runHi = lo + 1; //runHi表示当前元素的位置。
- if (runHi == hi)
- return 1;
- // Find end of run, and reverse range if descending
- //我们重写了c.compare(a,b)方法。这里的这个if条件(第10行)就是执行排序的关键,这里就是对元素进行排序操作了。如果在我们重写的比较器中,返回了-1; 即目前集合中比较的相邻的两个元素的顺序不是我们所期望的,那么要执行调换位置了
//这里还解释了上面的一个问题。 在compare(a,b)相邻两个元素,a表示靠后的一个,b表示靠前的一个。 即a的下标大于b的下标。 这里还有一个“理念”:不满足期望(返回-1)时,集合中a元素(主动)的位置要在b元素(被动比较)前面(下面通过位置调换实现)- if (c.compare(a[runHi++], a[lo]) < 0) { // Descending 这个英文注释的“降序”描述的是jdk默认的排序规则。我们重写了compare(a,b)所以可以忽略他这个英文注释。
- //下面的这个while循环就是相邻的两个元素依次进行比较,确定的是有多少个元素不符合我们的期望(最少就是上一行(第10行)对比的那两个),下面的runHi++最后记录的那个就是第一个满足我们期望(不需要调换)的元素下标了(或者是集合元素的总个数,这种情况出现在全部元素都要调换的情况下)。
- //这里就出现了上文提到的。如果我们在定义比较器的时候,全部返回了-1,那么这里就会认为所有元素都不符合我们的期望,runHi最后的值就是集合的长度(是总长度,比最后一个元素的下标大1),然后在下面调换顺序的时候,就会全部调换了(调换规则在下面介绍)。
- while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
- runHi++;
- //确定了多少个元素不满足期望之后,就要对这部分元素 [0,runHi-1] 进行调换了,排列成我们期望的顺序(降序排列)。剩余的元素 [runHi,集合总长度-1] 就是要进行二分插入了
- reverseRange(a, lo, runHi);
- } else { // Ascending 这个默认注释的“升序”描述的是jdk默认的排序。判断条件就类似if(a>=b) return 1 ; else return -1; 而我们重写了compare(a,b),所以忽略他的英文注释
- //如果上面第10行的if判断返回的是1,则表示集合中前两个元素的实际顺序就是我们期望的顺序,所以就不需要做调换操作了。 下面while循环也只是为了记录一共有多少个元素[0,runHi-1]满足我们的要求(最少就是上一步(第10行)对比的这两个)。剩余的元素[runHi,集合总长度-1]就是要进行二分插入了
- while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
- runHi++;
- }
- //这里最后返回的是一共有多少个元素已经排好顺序了。或者可以理解为从 list[runHi]元素这个开始(包含这个元素),集合中剩下的元素要进行二分法插入了。
- return runHi - lo;
- }
- 8.进入 reverseRange(a, lo, runHi); 这个方法是进行元素位置调换的。 调换的范围 [0,runHi) 《=== 这里是右开区间
a:是被操作的集合
lo:等于0,表示集合的首元素下标。
runHi:第一个不需要调换的元素下标(而他前面的元素都要进行调换) 或 集合的元素总个数。
替换的逻辑:第一个和最后一个对调、第二个和倒数第二个对调、第三个和倒数第三个对调。。。。。 一直到lo和runHi不满足循环条件lo < hi, 即此时的 lo>=runHi
- private static void reverseRange(Object[] a, int lo, int hi) {
- hi--;
- while (lo < hi) {
- Object t = a[lo];
- a[lo++] = a[hi];
- a[hi--] = t;
- }
- }
9.进入 binarySort(a, lo, hi, lo + initRunLen, c); 这个方法是将剩余的元素进行二分法插入操作的。
- a:是被操作的集合
lo:等于0,表示集合的首元素下标。
hi:集合元素的总个数
lo + initRunLen:从这个位置开始(包含这个位置)的元素,都要进行二分法插入了。
c:自定义的比较器
- private static <T> void binarySort(T[] a, int lo, int hi, int start,
- Comparator<? super T> c) {
- assert lo <= start && start <= hi;
- if (start == lo)
- start++;
- //这个for循环就是把剩下的元素都进行二分法插入。 要插入的元素的区间: [start,hi-1]
- for ( ; start < hi; start++) {
- T pivot = 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).
- */
- //这个while就是二分法插入的关键所在了: 确定插入位置。 当left>=right的时候,就是二分法完成的时候,left就是要找的位置的下标。
- //(实际这里只可能出现left<=right,不可能出现大于的情况.因为这里的mid是向下取整的,导致mid在取值时(除以2时)永远比right至少小1,所以right的值就大于或等于mid(在主动把mid赋值给right的情况下,right会等于mid)。而left永远是小于或等于mid的)
- //所以left只会小于或等于right,不会出现left大于right。
- while (left < right) {
- int mid = (left + right) >>> 1; //这一个是一个位运算,二进制右移一位,相当于是除以2。 int mid = (left + right)/2
- //这一步又利用到我们重写的compare(a,b)方法了。之前是把不满足我们期望(返回-1)的顺序的元素进行了调换位置。这里利用同样的规则(规则:自定义的比较器),把不满足我们期望(返回-1)的值放在二分法的前半段区间(以mid为区分)
- //这里和我们前面排序时的“理念”一样。 因为他和排序 用的是我们自定义的同一个规则,当他们的条件是返回了-1时,“主动”元素a要放在“被动”元素b的前面,所以放在了前半段区间
- if (c.compare(pivot, 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.
- */
- //left就是最终要插入的元素的位置。 这里计算的n是后面的元素需要后移多少次。如果是移动次数小于2,则通过java代码来移动。 如果大于2次,则调用其他的方法来完成
- //这里的 System.arraycopy(a, left, a, left + 1, n);不是java自己的方法,被native关键字修饰,表示的是调用其他语言的方法。比如调用底层操作系统的方法。
- //既然不是jdk的源码,那我们本次也没必要了解。我猜测这个方法内容就和C语言里面对数组插入新元素时,进行的元素移动是类似的。
- int n = start - left; // The number of elements to move
- // Switch is just an optimization for arraycopy in default case
- 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);
- }
- a[left] = pivot; //移动完之后,这里就要插入新元素了。
- } //继续for循环,用二分法插入完成对所有元素插入操作。
- }
到此,整个排序的过程就分析完成了。
java中Comparator比较器顺序问题,源码分析的更多相关文章
- java中的锁之AbstractQueuedSynchronizer源码分析(一)
一.AbstractQueuedSynchronizer类介绍. 该抽象类有两个内部类,分别是静态不可继承的Node类和公有的ConditionObject类.AbstractQueuedSynchr ...
- java中的锁之AbstractQueuedSynchronizer源码分析(二)
一.成员变量. 1.目录. 2.state.该变量标记为volatile,说明该变量是对所有线程可见的.作用在于每个线程改变该值,都会马上让其他线程可见,在CAS(可见锁概念与锁优化)的时候是必不可少 ...
- RocketMQ中Broker的HA策略源码分析
Broker的HA策略分为两部分①同步元数据②同步消息数据 同步元数据 在Slave启动时,会启动一个定时任务用来从master同步元数据 if (role == BrokerRole.SLAVE) ...
- 【Java】NIO中Selector的select方法源码分析
该篇博客的有些内容和在之前介绍过了,在这里再次涉及到的就不详细说了,如果有不理解请看[Java]NIO中Channel的注册源码分析, [Java]NIO中Selector的创建源码分析 Select ...
- Java入门系列之集合HashMap源码分析(十四)
前言 我们知道在Java 8中对于HashMap引入了红黑树从而提高操作性能,由于在上一节我们已经通过图解方式分析了红黑树原理,所以在接下来我们将更多精力投入到解析原理而不是算法本身,HashMap在 ...
- Java入门系列之集合LinkedList源码分析(九)
前言 上一节我们手写实现了单链表和双链表,本节我们来看看源码是如何实现的并且对比手动实现有哪些可优化的地方. LinkedList源码分析 通过上一节我们对双链表原理的讲解,同时我们对照如下图也可知道 ...
- Java ThreadPoolExecutor线程池原理及源码分析
一.源码分析(基于JDK1.6) ThreadExecutorPool是使用最多的线程池组件,了解它的原始资料最好是从从设计者(Doug Lea)的口中知道它的来龙去脉.在Jdk1.6中,Thread ...
- List中的ArrayList和LinkedList源码分析
List是在面试中经常会问的一点,在我们面试中知道的仅仅是List是单列集合Collection下的一个实现类, List的实现接口又有几个,一个是ArrayList,还有一个是LinkedLis ...
- Java集合框架之接口Collection源码分析
本文我们主要学习Java集合框架的根接口Collection,通过本文我们可以进一步了解Collection的属性及提供的方法.在介绍Collection接口之前我们不得不先学习一下Iterable, ...
随机推荐
- shuoj 1 + 2 = 3? (二分+数位dp)
题目传送门 1 + 2 = 3? 发布时间: 2018年4月15日 22:46 最后更新: 2018年4月15日 23:25 时间限制: 1000ms 内存限制: 128M 描述 埃森哲是 ...
- python学习第三十天函数的形参,实参及函数文档
python函数的形参是定义函数def 函数名 小括号里面的变量,实参是调用函数时候的值,函数文档是提供函数功能的开发文档,下面 详细说明系列方法 1,函数的形参 def chan(name): pr ...
- P4542 [ZJOI2011]营救皮卡丘(Floyd+网络流)
P4542 [ZJOI2011]营救皮卡丘 乍一看似乎没啥题相似的 仔细一看,$N<=150$ 边又是双向边,似乎可以用Floyd搞 先跑一遍Floyd处理出$dis[i][j]$ 注意到走 ...
- 玩爆你的手机联系人--T9搜索(一)
自己研究了好几天联系人的T9搜索算法, 先分享出来给大家看看. 欢迎不吝赐教.假设有大神有更好的T9搜索算法, 那更好啊,大家一起研究研究,谢谢. 第一部分是比較简单的获取手机联系人. 获取 ...
- swiper轮播图设置每组显示的个数及自定义slide宽度
一.html演示代码: <div class="swiper-container"> <div class="swiper-wrapper"& ...
- json字符串处理—Json&C#
C# 转自:https://blog.csdn.net/sajiazaici/article/details/77647625# 本文是全网第二简单的方法,因为我女票也发了一篇博客说是全网最简单的方法 ...
- bzoj1495 [NOI2006]网络收费 复杂度分析+树上背包
题目传送门 https://lydsy.com/JudgeOnline/problem.php?id=1495 题解 通过观察可以发现,对于一个 \(lca\),如果 \(nA \leq nB\),那 ...
- 前端每日实战:72# 视频演示如何用纯 CSS 创作气泡填色的按钮特效
效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/eKqZjy 可交互视频 此视频是可 ...
- vue+element-ui 实现分页(根据el-table内容变换的分页)
官方例子 官方提示: 设置layout,表示需要显示的内容,用逗号分隔,布局元素会依次显示.prev表示上一页,next为下一页,pager表示页码列表,除此以外还提供了jumper和total,si ...
- java编程实战
线程池为什么要有它: 线程创建要开辟虚拟机栈,释放线程要垃圾回收的. server端要并发访问数据库的. 服务器启动有线程池放着. ----- 线程池的概念: 1.任务队列 2.拒绝策略(抛出异常,直 ...