淺入淺出 Java 排序算法

Java String 源碼的排序算法

1、前言

Q:什麼是選擇問題?
選擇問題,是假設一組 N 個數,要肯定其中第 K 個最大值者。好比 A 與 B 對象須要哪一個更大?又好比:要考慮從一些數組中找出最大項?java

解決選擇問題,須要對象有個能力,即比較任意兩個對象,並肯定哪一個大,哪一個小或者相等。找出最大項問題的解決方法,只要依次用對象的比較(Comparable)能力,循環對象列表,一次就能解決。git

那麼 JDK 源碼如何實現比較(Comparable)能力的呢?github

2、java.lang.Comparable 接口

file

Comparable 接口,從 JDK 1.2 版本就有了,歷史算悠久。Comparable 接口強制了實現類對象列表的排序。其排序稱爲天然順序,其 compareTo 方法,稱爲天然比較法。 算法

該接口只有一個方法 public int compareTo(T o); ,能夠看出數組

  • 入參 T o :實現該接口類,傳入對應的要被比較的對象
  • 返回值 int:正數、負數和 0 ,表明大於、小於和等於

對象的集合列表(Collection List)或者數組(arrays) ,也有對應的工具類能夠方便的使用:數據結構

  • java.util.Collections#sort(List) 列表排序
  • java.util.Arrays#sort(Object[]) 數組排序

那 String 對象如何被比較的?socket

3、String 源碼中的算法

String 源碼中能夠看到 String JDK 1.0 就有了。那麼應該是 JDK 1.2 的時候,String 類實現了 Comparable 接口,而且傳入須要被比較的對象是 String。對象如圖:ide

file

String 是一個 final 類,沒法從 String 擴展新的類。從 114 行,能夠看出字符串的存儲結構是字符(Char)數組。先能夠看看一個字符串比較案例,代碼以下:函數

/**
 * 字符串比較案例
 *
 * Created by bysocket on 19/5/10.
 */
public class StringComparisonDemo {
    
    public static void main(String[] args) {
        String foo = "ABC";
        
        // 前面和後面每一個字符徹底同樣,返回 0
        String bar01 = "ABC";
        System.out.println(foo.compareTo(bar01));
        
        // 前面每一個字符徹底同樣,返回:後面就是字符串長度差
        String bar02 = "ABCD";
        String bar03 = "ABCDE";
        System.out.println(foo.compareTo(bar02)); // -1 (前面相等,foo 長度小 1)
        System.out.println(foo.compareTo(bar03)); // -2 (前面相等,foo 長度小 2)
        
        // 前面每一個字符不徹底同樣,返回:出現不同的字符 ASCII 差
        String bar04 = "ABD";
        String bar05 = "aABCD";
        System.out.println(foo.compareTo(bar04)); // -1  (foo 的 'C' 字符 ASCII 碼值爲 67,bar04 的 'D' 字符 ASCII 碼值爲 68。返回 67 - 68 = -1)
        System.out.println(foo.compareTo(bar05)); // -32 (foo 的 'A' 字符 ASCII 碼值爲 65,bar04 的 'a' 字符 ASCII 碼值爲 97。返回 65 - 97 = -32)
        
        String bysocket01 = "泥瓦匠";
        String bysocket02 = "瓦匠";
        System.out.println(bysocket01.compareTo(bysocket02));// -2049 (泥 和 瓦的 Unicode 差值)
    }
}

運行結果以下:工具

0
-1
-2
-1
-32
-2049

能夠看出, compareTo 方法是按字典順序比較兩個字符串。具體比較規則能夠看代碼註釋。比較規則以下:

  • 字符串的每一個字符徹底同樣,返回 0
  • 字符串前面部分的每一個字符徹底同樣,返回:後面就是兩個字符串長度差
  • 字符串前面部分的每一個字符存在不同,返回:出現不同的字符 ASCII 碼的差值
    • 中文比較返回對應的 Unicode 編碼值(Unicode 包含 ASCII)
    • foo 的 'C' 字符 ASCII 碼值爲 67
    • bar04 的 'D' 字符 ASCII 碼值爲 68。
    • foo.compareTo(bar04),返回 67 - 68 = -1
    • 常見字符 ASCII 碼,如圖所示

file

再看看 String 的 compareTo 方法如何實現字典順序的。源碼如圖:

file

源碼解析以下:

  • 第 1156 行:獲取當前字符串和另外一個字符串,長度較小的長度值 lim
  • 第 1161 行:若是 lim 大於 0 (較小的字符串非空),則開始比較
  • 第 1164 行:當前字符串和另外一個字符串,依次字符比較。若是不相等,則返回兩字符的 Unicode 編碼值的差值
  • 第 1169 行:當前字符串和另外一個字符串,依次字符比較。若是均相等,則返回兩個字符串長度的差值

因此要排序,確定先有比較能力,即實現 Comparable 接口。而後實現此接口的對象列表(和數組)能夠經過 Collections.sort(和 Arrays.sort)進行排序。

還有 TreeSet 使用樹結構實現(紅黑樹),集合中的元素進行排序。其中排序就是實現 Comparable 此接口

另外,若是沒有實現 Comparable 接口,使用排序時,會拋出 java.lang.ClassCastException 異常。詳細看《Java 集合:3、HashSet,TreeSet 和 LinkedHashSet比較》https://www.bysocket.com/archives/195

4、小結

上面也說到,這種比較其實有必定的弊端:

  • 默認 compareTo 不忽略字符大小寫。若是須要忽略,則從新自定義 compareTo 方法
  • 沒法進行二維的比較決策。好比判斷 2 * 1 矩形和 3 * 3 矩形,哪一個更大?
  • 好比有些類沒法實現該接口。一個 final 類,也沒法擴展新的類。其也有解決方案:函數對象(Function Object)

方法參數:定義一個沒有數據只有方法的類,並傳遞該類的實例。一個函數經過將其放在一個對象內部而被傳遞。這種對象一般叫作函數對象(Funtion Object)

在接口方法設計中, T execute(Callback callback) 參數中使用 callback 相似。好比在 Spring 源碼中,能夠看出不少設計是:聚合優先於繼承或者實現。這樣能夠減小不少繼承或者實現。相似 SpringJdbcTemplate 場景設計,能夠考慮到這種 Callback 設計實現。

那插入排序呢?

文章工程:

  • JDK 1.8
  • 工程名:algorithm-core-learning
  • 工程地址:https://github.com/JeffLi1993/algorithm-core-learning

1、前言

上面 Java String 源碼的排序算法,講了什麼是選擇問題,什麼是比較能力。

選擇問題,是假設一組 N 個數,要肯定其中第 K 個最大值者。算法是爲求解一個問題。

那什麼是算法?
算法是某種集合,是簡單指令的集合,是被指定的簡單指令集合。肯定該算法重要的指標:

  • 第一是否能解決問題;
  • 第二算法運行時間,即解決問題出結果須要多少時間;
  • 還有所需的空間資源,好比內存等。

不少時候,寫一個工做程序並不夠。由於遇到大數據下,運行時間就是一個重要的問題。

算法性能用大 O 標記法表示。大 O 標記法是標記相對增加率,精度是粗糙的。好比 2N 和 3N + 2 ,都是 O(N)。也就是常說的線性增加,還有常說的指數增加等

典型的增加率

file

典型的提供性能作法是分治法,即分支 divide and conquer 策略:

  1. 將問題分紅兩個大體相等的子問題,遞歸地對它們求解,這是分的部分;
  2. 治階段將兩個子問題的解修補到一塊兒,並可能再作些少許的附加工做,最後獲得整個問題的解。

file

2、排序

file

排序問題,是古老,但一直流行的問題。從 ACM 接觸到如今工做,每次涉及算法,或品讀 JDK 源碼中一些算法,常常會有排序的算法出現。

排序算法是爲了將一組數組(或序列)從新排列,排列後數據符合從大到小(或從小到大)的次序。這樣數據從無序到有序,會有什麼好處?

  • 應用層面:解決問題。
    • 最簡單的是能夠找到最大值或者最小值
    • 解決"一塊兒性"問題,即相同標誌元素連在一塊兒
    • 匹配在兩個或者更多個文件中的項目
    • 經過鍵碼值查找信息
  • 系統層面:減小系統的熵值,增長系統的有序度
    (Donald Knuth 的經典之做《計算機程序設計藝術》(The Art of Computer Programming)的第三卷)

經過維基百科查閱資料獲得:
在主內存中完成的排序叫作,內部排序。那須要在磁盤等其餘存儲完成的排序,叫作外部排序 external sorting。資料地址:https://en.wikipedia.org/wiki/External_sorting

上一篇《Java String 源碼的排序算法》,講到了 java.lang.Comparable 接口。那麼接口是一個抽象類型,是抽象方法(compareTo)的集合,用 interface 來聲明。所以被排序的對象屬於 Comparable 類型,即實現 Comparable 接口,而後調用對象實現的 compareTo 方法進行比較後排序。

在這些條件下的排序,叫做基於比較的排序(comparison-based sorting)

3、插入排序

白話文:熊大(一)、熊2、熊三... 按照身高從低到高排隊(排序)。這時候熊 N 加入隊伍,它從隊伍尾巴開始比較。若是它比前面的熊身高低,則與被比較的交換位置,依次從尾巴到頭部進行比較 & 交換位置。最終換到了應該熊 N 所在的位置。這就是插入排序的原理。

插入排序(insertion sort)

  • 最簡單的排序之一。ps: 冒泡排序看看就好,不推薦學習
  • 由 N - 1 次排序過程組成。
    • 若是被排序的這樣一個元素,就不須要排序。即 N =1 (1 - 1 = 0)
    • 每一次排序保證,從第一個位置到當前位置的元素爲已排序狀態。
  • 如圖:每一個元素往前進行比較,並終止於本身所在的位置

file

/**
 * 插入排序案例
 * <p>
 * Created by 泥瓦匠@bysocket.com on 19/5/15.
 */
public class InsertionSortingDemo {
    
    /**
     * 插入排序
     *
     * @param arr 能比較的對象數組
     * @param <T> 已排序的對象數組
     */
    public static <T extends Comparable> void insertionSort(T[] arr) {
        int j;
        
        // 從數組第二個元素開始,向前比較
        for (int p = 1; p < arr.length; p++) {
            T tmp = arr[p];
            // 循環,向前依次比較
            // 若是比前面元素小,交換位置
            for (j = p; (j > 0) && (tmp.compareTo(arr[j - 1]) < 0); j--) {
                arr[j] = arr[j - 1];
            }
            // 若是比前面元素大或者相等,那麼這就是元素的位置,交換
            arr[j] = tmp;
        }
    }
    
    public static void main(String[] args) {
        Integer[] intArr = new Integer[] {2, 3, 1, 4, 3};
        
        System.out.println(Arrays.toString(intArr));
        insertionSort(intArr);
        System.out.println(Arrays.toString(intArr));
    }
}

代碼解析以下:

  • 從數組的第二個元素,向前開始比較。比第一個元素小,則交換位置
  • 若是第二個元素比較完畢,那就第三個,第四個... 以此類推
  • 比較到最後一個元素時,完成排序

時間複雜度是 O(N^2),最好情景的是排序已經排好的,那就是 O(N),由於知足不了循環的判斷條件;最極端的是反序的數組,那就是 O(N^2)。因此該算法的時間複雜度爲 O(N^2)

運行 main 方法,結果以下:

[2, 3, 1, 4, 3]
[1, 2, 3, 3, 4]

再考慮考慮優化,會怎麼優化呢?
插入排序優化版 不是往前比較 。往前的一半比較,二分比較會更好。具體代碼,能夠自行試試

4、Array.sort 源碼中的插入排序

上面用本身實現的插入算法進行排序,其實 JDK 提供了 Array.sort 方法,方便排序。案例代碼以下:

/**
 * Arrays.sort 排序案例
 * <p>
 * Created by 泥瓦匠@bysocket.com on 19/5/28.
 */
public class ArraysSortDemo {
    
    public static void main(String[] args) {
    
        Integer[] intArr = new Integer[] {2, 3, 1, 4, 3};
    
        System.out.println(Arrays.toString(intArr));
        Arrays.sort(intArr);
        System.out.println(Arrays.toString(intArr));
    }
}

運行 main 方法,結果以下:

[2, 3, 1, 4, 3]
[1, 2, 3, 3, 4]

那 Arrays.sort 是如何實現的呢?JDK 1.2 的時候有了 Arrays ,JDK 1.8 時優化了一版 sort 算法。大體以下:

  • 若是元素數量小於 47,使用插入排序
  • 若是元素數量小於 286,使用快速排序
  • Timsort 算法整合了歸併排序和插入排序

file

源碼中咱們看到了 mergeSort 裏面整合了插入排序算法,跟上面實現的殊途同歸。這邊就不一行一行解釋了。

5、小結

算法是解決問題的。因此不必定一個算法解決一個問題,可能多個算法一塊兒解決一個問題。達到問題的最優解。插入排序,這樣就這麼簡單

代碼示例

本文示例讀者能夠經過查看下面倉庫的中: StringComparisonDemo 字符串比較案例案例:

參考資料

  • 《數據結構與算法分析:Java語言描述(原書第3版)》
  • https://en.wikipedia.org/wiki/Unicode
  • https://www.cnblogs.com/vamei/tag/%E7%AE%97%E6%B3%95/
  • https://www.bysocket.com/archives/2314/algorithm
相關文章
相關標籤/搜索