java中Comparator比较器顺序问题,源码分析


提示:

分析过程是个人的一些理解,如有不对的地方,还请大家见谅,指出错误,共同学习。

源码分析过程中由于我写的注释比较啰嗦、比较多,导致文中源代码不清晰,还请一遍参照源代码,一遍参照本文进行阅读。

 

原理:先将集合中的部分元素排列好顺序。  然后再将剩余的元素用二分法插入到已排好序(二分法的使用是建立在已排好序的前提下)的元素中去。然后得到排好序的集合。

测试代码:

 1 public class TestLambda {
 2     public static List<String> list = Arrays.asList("my","name","is","lambda","mzp");
 3     public static List<Integer> integerList = Arrays.asList(1,2,15,6,9,13,7);
 4 
 5     public static void main(String[] args) {
 6         System.out.println("排序前:");
 7         printList(integerList);
 8         oldIntegerSort();
 9         System.out.println("\noldSort排序后:");
10         printList(integerList);
11     }
12 
13 
14     /**
15      * @Author maozp3
16      * @Description: 对String类型的lis就行排序。使用老方法(外部比较器Comparator)
17      * @Date: 14:51 2019/7/5
18      * @Param []
19      * @return void
20      **/
21     public static void oldIntegerSort(){
22         //排序(匿名函数)
23         Collections.sort(integerList, new Comparator<Integer>(){
24             //使用新的排序规则。比较器排序。
25             // 原理,先确定部分元素的顺序(升序或是降序),然后把剩余的元素通过"二分插入"进行排序。
26             @Override
27             public int compare(Integer a, Integer b) {  //源码中第一个入参(a)是数组靠后面的数,第二个入参(b)是数组靠前面的数(比如这里:a=2,b=1)
28                 if(a <= b){    //由条件加上返回值来确定是升序还是降序 (如果全部返回-1的话,则实现逆序,将集合中的元素顺序颠倒)
29                     return 1;   //比如这里:原数组后面a的数小于前面的数b,返回1,1则表示这个顺序不需要调整。      
30                 }else{
31                     return -1;  //比如这里:原数组后面的数a小于前面的数b,返回-1,-1则表示数组中现在的顺序需要调整。根据我们的代码,前两个元素是1和2,判断条件if(2<=1),返回的是-1,即不满足我们的期望。 下面排序的时候就会对顺序进行调整了。
32                 }
33             }
34         });
35     }
36 
37 
38     /**
39      * @Author maozp3
40      * @Description: 打印集合元素
41      * @Date: 10:38 2019/7/8
42      * @Param [list]
43      * @return void
44      **/
45     public static <T> void printList(List<T> list){
46         Iterator<T> iterator = list.iterator();
47         while(iterator.hasNext()){
48             System.out.print(iterator.next()+",");
49         }
50     }
51 }

 

 

重写 Comparator 中的  compare 方法。自定义的比较规则:我这里自定义的比较器是:  期望得到  逆序排列

  判断条件是:当a<=b时,返回1。否则返回-1;意思就是我期望的是“后一个元素比前一个元素小(即降序)”,如果已经满足,就不需要调换顺序(返回1),如果不满足,就需要调换一下顺序(返回-1)。

源码分析时,有一个我自己定义的“理念”,对于这个“理念”,这里有一个比较好的理解方法: a和b作比较时,a是比较者(主动),b就相当于是参照物(被比较者、被动)

      下面源码分析时用到这个所谓的“理念”的时候,会用到“主动”和“被动”这两个说辞。

 

测试数据:1,2,15,6,9,13,7

 1 public static void oldIntegerSort(){
 2         //排序(匿名函数)
 3         Collections.sort(integerList, new Comparator<Integer>(){
 4             //使用新的排序规则。比较器排序。  原理,先确定部分元素的顺序(升序或是降序),然后把剩余的元素通过"二分插入"进行排序。
 5             @Override
 6             public int compare(Integer a, Integer b) {  //源码中第一个入参(a)是数组靠后面的数,第二个入参(b)是数组靠前面的数(比如这里:a=2,b=1)
 7                 if(a <= b){    //由条件加上返回值来确定是升序还是降序  (如果全部返回-1的话,则实现逆序,将集合中的元素顺序全部颠倒,下面会有说明原因)
 8                     return 1;   //比如这里:原数组后面的数小于前面的数,返回1,1则表示数组中现在的顺序不需要调整。
 9                 }else{
10                     return -1;  //比如这里:原数组后面的数小于前面的数,返回-1,-1则表示数组中现在的顺序需要调整。
11                 }
12             }
13         });
14     }

 

 先说一下个人总结的结论:

最终排序结果由判断条件(上面代码第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(); 方法,对集合进行排序

1 public static void main(String[] args) {
2         System.out.println("排序前:");
3         printList(integerList);
4         oldIntegerSort();
5         System.out.println("\noldSort排序后:");
6         printList(integerList);
7     }

2.调用 oldIntegerSort()中的,调用  Collections.sort(integerList, new Comparator<Integer>(){...} )   。进入Collections.sort(list,比较器)方法中

 1 public static void oldIntegerSort(){
 2         //排序(匿名函数)
 3         Collections.sort(integerList, new Comparator<Integer>(){
 4             //使用新的排序规则。比较器排序。
 5             // 原理,先确定部分元素的顺序(升序或是降序),然后把剩余的元素通过"二分插入"进行排序。
 6             @Override
 7             public int compare(Integer a, Integer b) {  //源码中第一个入参(a)是数组靠后面的数,第二个入参(b)是数组靠前面的数(比如这里:a=2,b=1)
 8                 if(a <= b){    //由条件加上返回值来确定是升序还是降序 (如果全部返回-1的话,则实现逆序,将集合中的元素顺序颠倒)
 9                     return 1;   //比如这里:如果原数组后面的数小于等于前面的数,返回1,1则表示这个顺序不需要调整。
10                 }else{
11                     return -1;  //比如这里:如果原数组后面的数大于前面的数,返回-1,-1则表示数组中现在的顺序需要调整。根据我们的测试数据,前两个元素是1和2,这里if(2<=1,返回的是-1,不满足我们的期望,后面排序的时候就会对这俩元素的位置进行调整了
12                 }
13             }
14         });
15     }

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这个元素)开始往后的所有元素都是要通过二分法进行插入排序了。

 1 private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
 2                                                     Comparator<? super T> c) {
 3         assert lo < hi;    //这里lo表示集合的第一个元素,也就是0; hi表示集合的大小。  assert是个断言,如果不满足就会抛出AssertionError异常,并终止执行。
 4         int runHi = lo + 1;  //runHi表示当前元素的位置。
 5         if (runHi == hi)
 6             return 1;
 7 
 8         // Find end of run, and reverse range if descending
 9       //我们重写了c.compare(a,b)方法。这里的这个if条件(第10行)就是执行排序的关键,这里就是对元素进行排序操作了。如果在我们重写的比较器中,返回了-1; 即目前集合中比较的相邻的两个元素的顺序不是我们所期望的,那么要执行调换位置了
     //这里还解释了上面的一个问题。 在compare(a,b)相邻两个元素,a表示靠后的一个,b表示靠前的一个。 即a的下标大于b的下标。 这里还有一个“理念”:不满足期望(返回-1)时,集合中a元素(主动)的位置要在b元素(被动比较)前面(下面通过位置调换实现) 10 if (c.compare(a[runHi++], a[lo]) < 0) { // Descending 这个英文注释的“降序”描述的是jdk默认的排序规则。我们重写了compare(a,b)所以可以忽略他这个英文注释。 11         //下面的这个while循环就是相邻的两个元素依次进行比较,确定的是有多少个元素不符合我们的期望(最少就是上一行(第10行)对比的那两个),下面的runHi++最后记录的那个就是第一个满足我们期望(不需要调换)的元素下标了(或者是集合元素的总个数,这种情况出现在全部元素都要调换的情况下)。 12         //这里就出现了上文提到的。如果我们在定义比较器的时候,全部返回了-1,那么这里就会认为所有元素都不符合我们的期望,runHi最后的值就是集合的长度(是总长度,比最后一个元素的下标大1),然后在下面调换顺序的时候,就会全部调换了(调换规则在下面介绍)。 13 while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0) 14 runHi++; 15         //确定了多少个元素不满足期望之后,就要对这部分元素 [0,runHi-1] 进行调换了,排列成我们期望的顺序(降序排列)。剩余的元素 [runHi,集合总长度-1] 就是要进行二分插入了 16 reverseRange(a, lo, runHi); 17 } else { // Ascending 这个默认注释的“升序”描述的是jdk默认的排序。判断条件就类似if(a>=b) return 1 ; else return -1; 而我们重写了compare(a,b),所以忽略他的英文注释 18        //如果上面第10行的if判断返回的是1,则表示集合中前两个元素的实际顺序就是我们期望的顺序,所以就不需要做调换操作了。 下面while循环也只是为了记录一共有多少个元素[0,runHi-1]满足我们的要求(最少就是上一步(第10行)对比的这两个)。剩余的元素[runHi,集合总长度-1]就是要进行二分插入了 19         while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0) 20         runHi++; 21      } 22      //这里最后返回的是一共有多少个元素已经排好顺序了。或者可以理解为从 list[runHi]元素这个开始(包含这个元素),集合中剩下的元素要进行二分法插入了。 23      return runHi - lo; 24 }
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循环,用二分法插入完成对所有元素插入操作。
    }

 到此,整个排序的过程就分析完成了。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM