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