zoukankan      html  css  js  c++  java
  • 【转】java comparator 升序、降序、倒序从源码角度理解

    原文链接:https://blog.csdn.net/u013066244/article/details/78997869

    环境
    jdk:1.7+

    前言
    之前我写过关于comparator的理解,但是都理解错了。

    java 自定义排序【Comparator升序降序的记法】

    特别是 上面这篇,完全理解错了,排序的真正的意思。

    最近通过查看源码,打断点的方式,一步步的查看、演算。算是明白了!

    当时我心里的疑惑是:
    ① -1到底表示不表示倒序;
    ② -1、0、1这三个值真的需要同时使用吗?能不能只使用其中某个就行了。
    ③-1是不是就是表示不调整顺序,其他都是要调整顺序。

    真正正确的理解:
    ① jdk官方默认是升序,是基于:

    < return -1
    = return 0
    > return 1
    1
    2
    3
    官方的源码就是基于这个写的;可以理解为硬性规定。
    也就是说,排序是由这三个参数同时决定的。

    如果要降序就必须完全相反:

    < return 1
    = return 0
    > return -1
    1
    2
    3
    为什么呢?这个只能通过源码的方式去看了。

    测试代码
    首先,我写了如下的测试代码:

    public static void main(String[] args) {
    List<Integer> re = new ArrayList<>();

    re.add(1);
    re.add(2);
    re.add(6);
    re.add(5);
    re.add(8);
    re.add(8);
    re.add(4);

    Collections.sort(re, new Comparator<Integer>() {

    @Override
    public int compare(Integer o1, Integer o2) {
    //下面这么写,结果是降序
    if(o1 < o2){
    return 1;
    }else if(o1 > o2){
    return -1;
    }
    return 0;
    }

    });

    System.out.println(re);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    降序
    开始debug测试:

    第一步: 程序先调用如下方法:

    @SuppressWarnings({"unchecked", "rawtypes"})
    public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
    }
    1
    2
    3
    4
    第二步: 而list.sort(c)源码:
    这里调用的是ArrayList类的方法:

    @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);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    第三步:调用Arrays.sort(a, (Comparator) c);方法:

    public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
    sort(a);
    } else {
    if (LegacyMergeSort.userRequested)
    legacyMergeSort(a, c);
    else
    //接下来会走这个方法,上面不会走;
    //未来jdk会弃用legacyMergeSort方法。
    TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    第四步:TimSort.sort(a, 0, a.length, c, null, 0, 0);这个方法很长,我先贴出主要核心的:

    if (nRemaining < MIN_MERGE) {
    int initRunLen =
    //这个方法就大致决定是顺序
    countRunAndMakeAscending(a, lo, hi, c);
    binarySort(a, lo, hi, lo + initRunLen, c);
    return;
    }
    1
    2
    3
    4
    5
    6
    7
    第五步:countRunAndMakeAscending方法:

    private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi, Comparator<? super T> c) {
    // lo 是数组起始位置 也就是 0
    assert lo < hi;
    // runHi = 1,这个值会随着循环而改变,表示当前元素的位置
    int runHi = lo + 1;
    // hi是数组长度
    if (runHi == hi)
    return 1;

    // Find end of run, and reverse range if descending
    //这里c.compare()调用就是我们重写的方法
    if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
    runHi++;
    reverseRange(a, lo, runHi);
    } else {
    // Ascending -- 英文的注释,默认是升序;不用管这个注释
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
    runHi++;
    }

    return runHi - lo;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    这个方法就是关键;

    我上面创建了一个数组:

    1 2 6 5 8 8 4
    //其中
    < 1
    = 0
    > -1
    1
    2
    3
    4
    5
    if (c.compare(a[runHi++], a[lo]) < 0) — 这句代码,对我的测试代码而言:if (c.compare(2, 1) < 0)中c.compare(2,1)得到的就是-1。接着就是执行:

    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
    runHi++;
    reverseRange(a, lo, runHi);
    1
    2
    3
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)中c.compare(a[runHi], a[runHi - 1]) < 0)就是c.compare(6, 2) < 0),而c.compare(6, 2)返回的是-1,所以会接着循环执行,runHi++后,此时runHi=2。就我的测试代码就会去判断c.compare(5, 6),其返回的是1,循环结束,接着执行reverseRange(a, lo, runHi);。这个是个反转方法。
    效果就是:

    数组:1 2 6 5 8 8 4
    反转后:6 2 1 5 8 8 4
    1
    2
    可以看出,前面三个数字顺序已经好了,后面的5 8 8 4,会在执行binarySort(a, lo, hi, lo + initRunLen, c);这个方法时来进行二分插入排序。

    第六步:执行binarySort(a, lo, 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; 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) {
    //这里相当于除以2
    int mid = (left + right) >>> 1;
    if (c.compare(pivot, a[mid]) < 0)
    right = mid;
    else
    left = mid + 1;
    }
    //当left等于right时,就说明找到位置了。
    //assert是断言,要是为false会直接报错
    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
    switch (n) {
    case 2: a[left + 2] = a[left + 1];
    case 1: a[left + 1] = a[left];
    break;
    //要是移动的位数大于2,就执行如下方法;
    default: System.arraycopy(a, left, a, left + 1, n);
    }
    a[left] = pivot;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    例子中的数组:

    6 2 1 5 8 8 4
    //循环执行binarySort方法后,
    //会依次把 5 8 8 4 插入到相应的位置
    //最终的结果为:
    // 8 8 6 5 4 2 1
    1
    2
    3
    4
    5
    升序
    这是,jdk默认的顺序,例子:

    < -1 > 1 =0
    1 2 6 5 8 8 4
    1
    2
    执行步骤和上面降序是一样的,我就直接分析核心部分了:

    // Find end of run, and reverse range if descending
    if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
    runHi++;
    reverseRange(a, lo, runHi);
    } else { // Ascending
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
    runHi++;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    当执行到这里时,c.compare(a[runHi++], a[lo]) < 0就是c.compare(2, 1) < 0,而`c.compare(2, 1)返回的是1,那么程序就会进入else的部分:

    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
    runHi++;
    1
    2
    代码c.compare(a[runHi], a[runHi - 1])就是c.compare(6, 2)返回的是1符合条件(大于0),
    runHi++,此时runHi=3。c.compare(a[runHi], a[runHi - 1])就是c.compare(5, 6),其返回的是-1,不符合条件。循环结束,数组结果为:

    //可以看出什么都没有变
    1 2 6 5 8 8 4
    //但是方法的`return runHi - lo;`这个返回的结果就是3
    //这个返回值,会在`binarySort(a, lo, hi, lo + initRunLen, c);`中用到。
    1
    2
    3
    4
    下一步:执行binarySort(a, lo, hi, lo + initRunLen, c);其中initRunLen = 3;
    在执行二分插入时,就会从数组下标为3开始;

    1 2 6 5 8 8 4
    //从下标为3,开始二分插入排序;即从5开始。
    1 2 5 6 8 8 4
    接着是8
    1 2 5 6 8 8 4
    接着是第二个8
    1 2 5 6 8 8 4
    接着是4
    1 2 4 5 6 8 8
    1
    2
    3
    4
    5
    6
    7
    8
    9
    通过升序和降序,我们基本可以知道排序步骤:
    ①countRunAndMakeAscending这个方法确定是顺序还是降序,并且将数组的一部分排列好。并返回未排列的起始位置
    ②将未排列的起始位置传递给binarySort进行二分插入排序。

    倒序
    我们先来看看倒序的结果:

    1 2 6 5 8 8 4
    倒序后:
    4 8 8 5 6 2 1
    //怎么做到呢?
    //不管大于、小于和等于 都返回 -1
    1
    2
    3
    4
    5
    从源码上看countRunAndMakeAscending方法:

    f (c.compare(a[runHi++], a[lo]) < 0) { // Descending
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
    runHi++;
    reverseRange(a, lo, runHi);
    } else { // Ascending
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
    runHi++;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    c.compare()得到的永远都是-1,所以其会将下面这段代码执行完毕:

    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
    runHi++;
    1
    2
    循环完毕后,此时runHi就是数组的长度7。
    接着执行reverseRange(a, lo, runHi);,将整个数组进行倒序。
    该方法完全执行完成后,返回值就是数组长度。
    此时再执行binarySort方法时,for ( ; start < hi; start++)中的start是刚刚传进来的值,也就是数组长度,而hi也是数组长度,所以二分插入方法什么都没有做,只是调用了下。

    0 到底是什么作用
    假设不管大于、小于、等于,我们都返回0 ,会发现顺序没有变;而且你会发现,要是都返回1的话,顺序也是没有变的!

    从countRunAndMakeAscending方法中可以得出结论:

    // Find end of run, and reverse range if descending
    if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
    runHi++;
    reverseRange(a, lo, runHi);
    } else {
    //走这个循环
    while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
    runHi++;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    当不管大于、小于、等于时,我们都返回一个值时,0和1效果是一样的,就是不排序;-1就是倒序。

    可以要是 是如下写法:

    public int compare(Integer o1, Integer o2) {
    if(o1 < o2){
    return 1;
    }/*else if(o1 > o2){
    return 1;
    }*/
    return -1;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    也就是 我们把等于和大于都返回-1,小于返回1。发现也是可以降序的,或者反过来,就是升序。视乎觉得0好像是多余的。

    其实0表示的是,相同元素不排序,要是我们把等于返回为-1,那么两个相同的元素会交互顺序;

    1 2 6 5 8 8 4
    //也就是这里面两个8 会交换顺序
    1
    2
    对数字而言交换顺序没有关系,但是里面要是是Map对象的话,那就有关系,因为有时我们是希望相同元素不进行顺序调整的。

    要是我们把等于返回为1效果和0是一样的都是不排序。

    总结
    排序其实是由三个数字同时决定的;

    升序(默认,即官方定义,毕竟代码实现就是基于这个写的):

    < -1
    = 0 //或者 1效果是一样的;-1相同元素会发生位置调整
    > 1
    1
    2
    3
    降序:

    < 1
    = 0 //或者 1效果是一样的;-1相同元素会发生顺序调整
    > -1
    1
    2
    3
    倒序:

    //直接
    return -1;
    1
    2
    不改变顺序:

    //直接
    return 0或者1;
    1
    2
    底层做法是:先确定局部顺序,再利用二分查找法,进行后续排序:

    数组:1 2 6 5 8 8 4
    反转后:6 2 1 5 8 8 4
    1
    2
    这里先确定了6 2 1的顺序,后面5 8 8 4的位置就是利用二分查找法来确定的!
    ---------------------
    作者:山鬼谣me
    来源:CSDN
    原文:https://blog.csdn.net/u013066244/article/details/78997869
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    C语言学习_C如何在一个文件里调用另一个源文件中的函数
    C语言学习_include<>与include""的区别
    C语言学习_一个简单程序的解释与C学习方法概括
    python_list和tuple互转
    C语言学习_恶搞小程序
    Linux命令(基础1)
    Linux基础配置
    面向对象知识补充1
    linux系统centOS在虚拟机下的自定义安装
    sql注入
  • 原文地址:https://www.cnblogs.com/hubavyn/p/10309690.html
Copyright © 2011-2022 走看看