基本排序 - Algorithms, Part I, week 2 ELEMENTARY SORTS

前言

上一篇:棧和隊列
下一篇:歸併排序php

排序是從新排列一系列對象以便按照某種邏輯順序排列的過程。排序在商業數據處理和現代科學計算中起着重要做用。在交易處理,組合優化,天體物理學,分子動力學,語言學,基因組學,天氣預報和許多其餘領域中的應用比比皆是。
在本章中,咱們考慮了幾種經典的排序方法和一種稱爲優先級隊列的基本數據類型的有效實現。咱們討論比較排序算法的理論基礎,並結合本章應用排序和優先級隊列算法。html

2.1 基本排序引入了選擇排序,插入排序和 shellort。
2.2 Mergesort 描述了megesort,一種保證在線性時間內運行的排序算法。
2.3 Quicksort 描述了quicksort,它比任何其餘排序算法使用得更普遍。
2.4 優先級隊列引入優先級隊列數據類型和使用二進制堆的有效實現。它還引入了 heapsort。
2.5 應用程序描述了排序的應用程序,包括使用備用排序,選擇,系統排序和穩定性java

排序介紹

進行排列咱們應該遵循哪些規則呢?讓咱們先看看典型基本排序問題。
好比,大學有不少學生檔案,對於每一個學生有一些信息,多是姓名、班級編號、成績、電話號碼、地址。程序員

圖片描述

咱們查看一個元素,那個元素有一條記錄,這個記錄就是咱們要排序的信息,準確地說,記錄中有一部分叫作關鍵字 (key),
咱們將記錄根據關鍵字進行排列,這就是排序問題。算法

下圖將數組中的 n 個元素根據元素中的定義的關鍵字 (此爲姓) 升序排列shell

圖片描述

排序的應用:
排序的應用不少,好比快遞的包裹,紙牌遊戲,手機聯繫人,圖書館的圖書編號等等。咱們的目標是可以對任何類型的數據進行排序。
來看幾個客戶端程序。express

實例:排序客戶端

例1:對字符串進行排序

public class StringSorter
{
     public static void main(String[] args)
     {
         String[] a = StdIn.readAllStrings();
         Insertion.sort(a);
         for (int i = 0; i < a.length; i++)
         StdOut.println(a[i]);
     }
}

這個例子中:編程

  1. 用 readString() 方法從文件中讀取字符串。
  2. 這個方法在咱們的 StdIn 類裏,須要一個文件做爲參數,將第一個命令行參數做爲文件名,從文件中讀取一個字符串數組,字符串以空白字符分隔,接下來又調用 Insertion.sort() 方法。
  3. Insertion.sort 這個方法以數組 a 做爲第一個實參,而後將數組中的字符串排序

這個例子中,words3.txt 有一些單詞,這個客戶端輸出的結果就是這些單詞從新按照字母表的順序排序的結果。segmentfault

% more words3.txt
bed bug dad yet zoo ... all bad yes

% java StringSorter < words3.txt
all bad bed bug dad ... yes yet zoo
[suppressing newlines]

例2. 將一些隨機實數按升序排序

public class Experiment
{
     public static void main(String[] args)
     {
         int n = Integer.parseInt(args[0]);
         Double[] a = new Double[n];
         for (int i = 0; i < n; i++)
         a[i] = StdRandom.uniform();
         //調用插入排序方法
         Insertion.sort(a);
         for (int i = 0; i < n; i++)
         StdOut.println(a[i]);
     }
}

這個客戶端程序調用插入排序方法。它從標準輸入中讀取數字,放進數組,而後調用 Insertion.sort(插入排序),最後打印輸出。
下邊的打印輸出的數字是從小到大排好序的。這看起來有點像人爲設計的輸入,也有不少應用中能夠用隨機輸入做爲好的輸入模型。數組

% java Experiment 10
0.08614716385210452
0.09054270895414829
0.10708746304898642
0.21166190071646818
0.363292849257276
0.460954145685913
0.5340026311350087
0.7216129793703496
0.9003500354411443
0.9293994908845686

例3. 對文件排序

import java.io.File;
public class FileSorter
{
     public static void main(String[] args)
     {
         File directory = new File(args[0]);
         File[] files = directory.listFiles();
         Insertion.sort(files);
         for (int i = 0; i < files.length; i++)
         StdOut.println(files[i].getName());
     }
}
% java FileSorter .
Insertion.class
Insertion.java
InsertionX.class
InsertionX.java
Selection.class
Selection.java
Shell.class
Shell.java
ShellX.class
ShellX.java

這個例子中,給定目錄中的文件名,咱們要對文件排序。此次又用到了Java的File文件類。

  1. 咱們用這個類中的 listFiles() 方法得到包含給定目錄中全部文件名的數組。
  2. Insertion.sort() 使用這個數組做爲第一實參。
  3. 一樣,程序對這些文件名進行了排序,而後依次將文件名以字母表的順序打印輸出

這是三個不一樣的客戶端,對應三種徹底不一樣類型的數據。
任務的第一條規則:咱們要考慮如何才能完成實現一個排序程序,能夠被三個不一樣的客戶端用來對三種不一樣數據類型排序。
這裏採起的方式是一種叫作回調的機制。

回調機制 Callbacks

咱們的基本問題是:在沒有元素關鍵字類型的任何信息的狀況下如何比較全部這些數據。
答案是咱們創建了一個叫作回調的機制

Callback = 對可執行代碼的引用

  • 客戶端將對象數組傳遞給排序函數sort()
  • sort() 方法根據須要調用 object 的比較函數 compareTo()

實現回調的方式:

有不少實現回調函數的辦法,和具體編程語言有關。不一樣的語言有不一樣的機制。核心思想是將函數做爲實參傳遞給其餘函數
涉及到函數式編程思想,須要更深的理論,能夠追溯到圖靈和徹奇。

・Java: interfaces.
・C: function pointers.
・C++: class-type functors.
・C#: delegates.
・Python, Perl, ML, Javascript: first-class functions.

Java中,有一種隱含的機制,只要任何這種對象數組具備 compareTo() 方法。排序函數就會在須要比較兩個元素時,回調數組中的對象對應的compareTo()方法。

回調:Java 接口

對於Java,由於要在編譯時檢查類型,使用了叫作接口的特殊方法。
接口 interfaces:一種類型,裏頭定義了類能夠提供的一組方法

public interface Comparable<Item>
{
   //能夠看做是一種相似於合同的形式,這個條款規定:這種方法(和規定的行爲)
   public int compareTo(Item that);
}

實現接口的類:必須實現全部接口方法

public class String implements Comparable<String>//String 類承諾履行合同的條款
{
     ...
     public int compareTo(String that)
     {
     // 類遵照合約
     ...
     }
}

"簽署合同後的影響":

  • 能夠將任何 String 對象視爲 Comparable 類型的對象
  • 在Comparable對象上,能夠調用(僅調用)compareTo() 方法。
  • 啓用回調。

後面咱們會詳細介紹如何用Java接口實現回調。這個比較偏向編程語言的細節,可是確實值得學習,由於它使咱們可以以類型安全的方式使用爲任何類型數據開發的排序算法。

回調:路線圖

圖片描述
注:key point: no dependence on type of data to be sorted 關鍵點:不依賴於要排序的數據類型

咱們已經看了一些客戶端程序。這是那個對字符串進行排序的客戶端程序 (上邊的例1)。

  1. 客戶端以某類型對象數組做爲第一實參(Comparable[] a),直接調用 sort() 方法。
  2. Java中內置了一個叫作Comparable(可比較的)的接口 ( java.lang.Comparable interface )
  3. Comparable 接口規範要求實現 Comparable 的數據類型要有一個compareTo()方法。這個方法是泛化的,會對特定類型的元素進行比較
    public interface Comparable<Item>{public int compareTo(Item that);}
  4. 當咱們實現要排序的對象(這裏爲String )時咱們就實現 Comparable 接口
    public class String implements Comparable<String>

由於排序是在不少情形中要使用的操做,Java標準庫中會用到排序的類型不少都實現了Comparable接口,意味着,這些數據類型具備實現 compareTo()方法的實例方法。它將當前對象 (a[j]) 與參數表示的對象 (a[j-1]) 相比較,根據具體的一些測試返回比較的結果,好比
返回 -1 表示小於;返回 +1 表示大於;返回0表示相等,排序算法的實現就只須要這麼一個compareTo()方法。
在函數聲明的時候,它要求參數必須是 Comparable 類型數組 (Comparable[] a),這意味着數組中的對象須要實現 Comparable 接口,或者說對象必須有compareTo() 方法,而後排序代碼直接使用 compareTo() 對一個對象實例 (a[j]) 調用這個方法, 以另外一個對象實例 (a[j-1]) 做爲實參。
在這個例子中測試第一個是否小於第二個。關鍵在於排序實現與數據類型無關,具體的比較由 Comparable 接口處理,不一樣類型的 Comparable 數組最終會以相同的方式排序,依賴於接口機制,回調到實際的被排序對象類型 (String) 的 compareTo() 代碼。

全序關係 total order

compareTo() 方法實現的是全序關係(total order)
全序關係總體來講就是元素在排序中可以按照特定順序排列。

全序關係是一種二元關係 ≤ 知足:

  • 反對稱性 Antisymmetry :v ≤ w 而且 w ≤ v 則這種狀況成立的惟一多是 v = w
  • 傳遞性 Transitivity:v ≤ w 而且 w ≤ x,則 v ≤ x
  • 徹底性 Totality:要麼 v ≤ w ,要麼 w ≤ v,要麼 v = w (沒有其餘狀況了)

有幾條很天然的規則,有三個性質:

咱們通常考慮做爲排序關鍵字的不少數據類型具備天然的全序關係,如整數、天然數、實數、字符串的字母表順序、日期或者時間的前後順序等等

但不是全部的有序關係都是全序關係。
好比石頭、剪刀、布是不具備傳遞性。若是已知 v ≤ w,w ≤ x,你並不必定知道 v 是否 ≤ x
還有食物鏈也是,違反了反對稱性

圖片描述

圖片描述

Surprising but true. The <= operator for double is not a total order. (!)

Comparable API

按照 Java 中的規定咱們須要實現 compareTo() 方法,使得 v 和 w 的比較是全序關係。
並且按照規定:

  • 若是是小於,返回負整數
  • 若是相等返回0
  • 若是當前對象大於做爲參數傳入的對象則返回正整數。
  • 若是對象類型不相容,或者其中一個是空指針,compareTo() 會拋出異常

圖片描述

Java 內置的可比類型:Java中不少數字、日期和文件等等標準類型按照規定都實現了 compareTo() 方法
自定義可比類型:若是咱們本身實現的類型要用於比較,就要根據這些規則,本身去實現 Comparable 接口

Comparable 接口的實現

實現通常是直截了當的。這裏有個例子,這是Java中實現的 Date 日期數據類型的簡化版,咱們用來演示實現Comparable接口

//在類聲明以後,咱們寫implements Comparable 而後在泛型類型填上類名,由於咱們後面只容許日期類型與其餘日期類型比較
public class Date implements Comparable<Date>
{
     //Date類有三個實例變量: month,day,year
     private final int month, day, year;
     //構造函數經過參數設置這些變量
     public Date(int m, int d, int y)
     {
         month = m;
         day = d;
         year = y;
     }
     public int compareTo(Date that)
     {
         if (this.year < that.year ) return -1;
         if (this.year > that.year ) return +1;
         if (this.month < that.month) return -1;
         if (this.month > that.month) return +1;
         if (this.day < that.day ) return -1;
         if (this.day > that.day ) return +1;
         return 0;
     }
}

若是想要比較兩個不一樣的日期,首先是檢查 this.year 是否小於 that.year, 當前日期對象和做爲參數的日期對象的年份進行對比, 若是爲「真」那麼就是小於,返回-1。若是 this.year 更大,返回+1 不然,年份就是相同的,那麼咱們就必須檢查月份來進行比較, 這樣一直比較到日期。只有三個變量徹底相同才返回0.
這個例子實現了 Comparable 接口, 實現了compareTo()方法,能夠將日期按照你指望的順序排列。

兩個有用的排序抽象方式

Java語言爲咱們提供了Comparable接口的機制,使咱們可以對任何類型數據排序。當咱們後續實現排序算法時,咱們實際上將這個機制隱藏在咱們的實現下面。

咱們採用的方式是將引用數據的兩個基本操做:比較交換封裝爲靜態方法

Less. Is item v < w ?
private static boolean less(Comparable v, Comparable w)
{ return v.compareTo(w) < 0; }

方法 less() 以兩個 Comparable 對象做爲參數,返回 v.compareTo(w) < 0.

Exchange. Swap item in array a[] at index i with the one at index j.
private static void exch(Comparable[] a, int i, int j)
{
    Comparable swap = a[i];
    a[i] = a[j];
    a[j] = swap;
}

當咱們對數組中的元素進行排序時另外一個操做是 swap,將給定索引 i 的對象與索引 j 的對象交換.
這個操做是每一個程序員學習賦值語句的入門語句,將 a[i] 保存在變量 swap 中,a[j] 放進 a[i],而後 swap 放回到 a[j]

咱們的排序方法引用數據時只須要使用這兩個靜態方法。這麼作有個很充分的理由。
舉個例子,假設咱們想檢驗數組是不是有序的。這個靜態方法中若是數組有序,則返回「真」,無序則返回「假」。
這個方法就是從頭到尾過一遍數組,檢查每一個元素是否小於前一個元素。若是有一個元素比前一個元素小,那麼數組就是無序的,返回「假」。若是直到數組結尾也沒有檢測到,那麼數組是有序的。很是簡單的代碼。

選擇排序

第一個基本排序方法很簡單,叫作選擇排序。

算法介紹

選擇排序的思想以下:從未排序數組開始,咱們用這些撲克牌舉例,在第 i 次迭代中,咱們在數組剩下的項中找到最小的,這個狀況下,2 是全部項中最小的,而後,咱們將它和數組中的第一項交換,這一步就完成了。
選擇排序就是基於這樣的思想迭代操做。

基本的選擇排序方法是在第 i 次迭代中,在數組中第i項右邊剩下的或者索引比 i 更大的項中找到最小的一項,而後和第 i 項交換。
開始 i 是 0,從最左端開始掃描全部右邊剩下的項,最小的是2,右起第3項,那麼咱們把第 i 項和最小項交換,這是第一步。
i左邊部分的數組就是排過序的。而後 i + 1,繼續重複的操做。
i + 1 爲了尋找最小的項都要掃描所有剩下的項,但一旦找到以後,只須要交換兩張牌,這就是選擇排序的關鍵性質。

圖片描述
圖片描述

最後 8 是最小的,這時,咱們知道已是有序的了,可是程序不知道,因此必須檢查而且作決定。i 和 min 相同,本身和本身交換,最後一次迭代。這個過程結束後,咱們知道整個數組已是最終狀態,是有序的了。

選擇排序的完整動態演示點此

理解算法工做方式的一個辦法是考慮其不變性。
對於選擇排序,咱們有個指針,變量 i,從左到右掃描。 假設咱們用箭頭表示這個指針,以下圖, 不變式就是

  • 箭頭左邊的項不會再變了,它們已是升序了
  • 箭頭右邊的項都比箭頭左邊的項大,這是咱們確立的機制

算法經過找到右邊最小的項,並和箭頭所指的右邊下一項交換來維持不變性。

圖片描述

Java實現

爲了維持算法的不變式,咱們須要:

  • 向右移動指針 i 加 1
  • 在指針的右邊找到最小的索引
  • 交換最小索引與當前指針的值

向右移動指針 i 加1後,不變式有可能被破壞,由於有可能在指針右邊有一個元素比指針所指的元素小致使不變式被破壞,咱們要作的是找到最小項的索引,而後交換,一旦咱們完成了交換,咱們又一次保留了不變式。這時指針左邊元素不會再變了,右邊也沒有更小的元素,這也就給出了實現選擇排序的代碼。

基礎實現

實現不變性的代碼以下:

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;

public class Selection {
    public static void sort(Comparable[] a) {
        int n = a.length;
        for (int i = 0; i < n; i++) {
        //在指針右邊找到最小項
            int min = i;
            for (int j = i + 1; j < n; j++)
                if (less(a[j], a[min]))
                    min = j;
            //交換最小索引與當前指針的值
            exch(a, i, min);
        }
    }

    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w) < 0;
    }

    private static void exch(Object[] a, int i, int j) {
        Object swap = a[i];
        a[i] = a[j];
        a[j] = swap;
    }
    
    private static void show(Comparable[] a) {
        for (int i = 0; i < a.length; i++) {
            StdOut.print(a[i]);
        }
    }
    
    //寫一個超簡單的客戶端
    public static void main(String[] args) {
        String[] a = {"1","5","3","8","4","1","4","5"};
        Selection.sort(a);
        show(a);
    }
}

咱們將數組的長度記爲 n, for循環遍歷數組中每一個元素變量, min用來存儲指針 i 右邊最小元素的索引, 內層的 j 的for循環,若是找到更小的值,則重設min 一旦檢查完 i 右側全部的元素,將最小的和第 i 項交換, 這就是選擇排序的完整實現。

泛型方法

值得注意的是當咱們嘗試編譯的時候會出現以下警告:

圖片描述

發生的緣由:

實質上,此警告表示 Comparable 對象沒法與任意對象進行比較。 Comparable <T> 是一個泛型接口,其中類型參數 T 指定能夠與此對象進行比較的對象的類型。

所以,爲了正確使用Comparable <T>,須要使排序列表具備通用性,以表達一個約束,即列表存儲的對象能夠與同個類型的對象相互比較,以下所示:

public class SortedList<T extends Comparable<? super T>> {
    public void add(T obj) { ... }
    ...
}

因此咱們的代碼要改爲沒有編譯警告的類型安全的版本:

圖片描述

算法分析

爲選擇排序的開銷創建數學模型很是容易.
命題:選擇排序使用大約 n^2 / 2 個比較以及,整 n 次交換。

  • (n – 1) + (n – 2) + ... + 1 + 0 ~ n^2 / 2

只要看看這個選擇排序運行的跟蹤記錄,這就是這個命題的圖形證實。
圖中:

  • 黑色的項是每次爲了尋找最小項檢查的項
  • 最小項是紅色的
  • 灰色的項是未檢查的項,已經在最終位置了

圖片描述

你能夠看到這基本就是 n x n 的正方形,其中大約一半的元素是黑色的,即大約 n^2 / 2,你也能看出準確的表達式(n – 1) + (n – 2) + ... + 1 + 0, 就是總共比較的次數。而後在變量 i 的這 n 個取值各有一次交換,因此這是交換次數的開銷。

  1. 關於選擇排序的這個命題說明了頗有意思的一點,就是它和輸入的序列自己的順序無關
  2. 選擇排序須要平方時間由於它總要查看全部的項尋找最小項
  3. 另外一個性質就是要完成排序這已是移動開銷最小的了,選擇排序只須要線性次數的交換
    每一項都是僅僅一次交換就放在了最終位置。

選擇排序指針由左至右掃描,每次掃描找到右邊最小的項,交換到它最終的位置上。若是數組一部分已經排好序了,這對選擇排序不影響,依然要一遍一遍掃描,即便是徹底有序的,依然要掃描右邊的項來找最小的元素。這就是選擇排序,咱們第一個基本排序方法

Q. 若是數組已經排好序,那麼插入排序比較須要多少次?

  • 對數級
  • 線性級
  • 平方級
  • 立方級別

A. 查看附錄

插入排序

插入排序,這是另一種基本排序方法,有趣的是 相比選擇排序插入排序具備至關不一樣的性能。

算法介紹

下邊是一個插入排序的演示。對於插入排序,咱們要作的和以前同樣,從左到右移動索引 i,但如今,在第 i 個迭代中 咱們將會把 a[ i ] 移動到其左側的位置,讓咱們用牌的示例來看看這是怎麼工做的。
如今咱們從初始化 i 爲第一張牌開始,咱們的想法是 i 的左邊的全部紙牌將會被排序,右邊的紙牌,咱們所有先都不去看
因此,i 左側全部的紙牌是升序,右側全部的紙牌咱們如今還沒檢查過,第二步咱們增長 i ,在這種狀況下指針左邊已經排好序了,咱們什麼也不用作。
當 i 是數組中的第三項時,此時咱們從索引 j 開始,而後,j 從 i 開始向左邊移動,咱們要作的是將5與它左邊更大的元素交換,那麼,首先與10交換,依然沒有到最終位置,因此再和7交換,如今已經到數組最前面了,一旦咱們檢查完左側全部項或者找到一個更小的元素,i 左邊全部項就排好序了

圖片描述

插入排序完整演示在此

一旦完成以後,從 i 開始它左側的數組就是升序的,i 左邊就都排好序了,因此這個情形中咱們用更少的工做量就完成了排序,並不老是須要一直檢查到數組的開頭

Java 實現

咱們再從不變式的角度來看插入排序

  • 指針依然是從左至右掃描,
  • 指針左邊的全部元素,包括指針指向的元素,都是排好序的
  • 而右邊的元素都還徹底沒有檢查過

圖片描述

咱們來看隨着指針遞增維持不變式的代碼

將指針向右側移動,增長 1,由於指針指向的元素沒排過序,因此破壞了不變式,那麼爲了維持不變式, 要將它排序,須要將它和左邊每一個更大的元素交換。下面的代碼完成的就是這個, 索引 j 從 i 開始,逐漸變小, j 指向的元素與左邊的元素交換, a[j] 與左邊的元素 a[j-1] 交換, 只要a[j]小於 a[j-1] 而且 j > 0 就一直交換, 咱們就立刻獲得了插入排序的代碼。

import edu.princeton.cs.algs4.StdOut;

public class InsertionPedantic {

    public static <Key extends Comparable<Key>> void sort(Comparable[] a) {
        int n = a.length;
       
        for (int i = 0; i < n; i++)
            for (int j = i; j > 0; j--)
            // a[j] 與左邊的元素 a[j-1] 交換, 只要a[j]小於 a[j-1] 而且 j > 0 就一直交換
                if (less(a[j], a[j - 1]))
                    exch(a, j, j - 1);
                else break;
    }

    // 如下代碼與選擇排序同樣
    private static <Key extends Comparable<Key>> boolean less(Key v, Key w) {
        return v.compareTo(w) < 0;
    }

    private static void exch(Object[] a, int i, int j) {
        Object swap = a[i];
        a[i] = a[j];
        a[j] = swap;
    }

        private static void show(Comparable[] a) {
        for (int i = 0; i < a.length; i++) {
            StdOut.print(a[i]);
        }
    }

    public static void main(String[] args) {
        String[] a = {"1", "5", "3", "8", "4", "1", "4", "5"};
        InsertionPedantic.sort(a);
        show(a);
    }
}

與選擇排序的代碼相似,並且同樣簡單,有兩個嵌套的for循環,選擇排序也是同樣,循環中須要進行一次檢查,一次比較大小,和一次交換。這是基本排序方法的一個良好的實現。

算法分析

插入排序更復雜一些,咱們的命題是:
對具備不一樣關鍵值的隨機序列排序,
Average case 平均狀況:插入排序平均須要使用大約 1/4 n^2 次比較, 與大約相同的 1/4 n^2 的交換次數。
這個要證實的話更復雜一些,和隨機順序的數組有關。和選擇排序的證實同樣,從這個 nxn 的算法步驟中, 你能夠找到命題來源的思路。
黑色的元素依然是咱們比較的,實際上,也是進行交換的。紅色的是到達的最終位置。

圖片描述

你能夠看到對於隨機順序的大數組,要移動到最終位置平均要移動大約一半的位置,這意味着對角線如下的元素,平均一半是黑色的 對角線如下的元素有1/2 n^2 個 一半就是1/4 n^2 精確的分析比這個詳細不了多少,這個步驟更多,下圖再次顯示排序過程當中對比和交換的操做涉及到對角線下大約一半的元素。

圖片描述

由於 1/4 n^2 和 1/2 n^2 相比小一半, 插入排序的速度大約是選擇排序的兩倍, 因此相同時間內演示中咱們可以對大約兩倍的元素進行排序

插入排序運行時間取決於數據開始的順序。
咱們來看看最好與最壞的狀況,固然這些都是異常的狀況:

Best case:
若是數組剛好已經排好序了,插入排序實際上只須要驗證每一個元素比它左邊的元素大,因此不用進行交換,只須要 n-1 次比較就能完成排序工做。

Worst case:
若是數組是降序排列的,而且不存在重複值,每一個元素都移動到數組開頭那麼就須要進行1/2 n^2 次比較與1/2 n^2 次交換

因此第一種狀況下,插入排序比選擇排序快得多, 是線性時間的而不是平方時間的, 而第二種情形中,比選擇排序慢,由於須要同樣的比較次數,可是多得多的交換次數。元素降序排列的狀況,每次獲得一個新元素,都必須一直交換到最開頭。這是實際應用中咱們不想見到的最壞的狀況。

但也有好的狀況,在不少實際應用中咱們都在利用這一點,就是數組已經部分有序的狀況,用定量的方法考慮問題。

部分有序數組

咱們定義:一個「逆序對」(inversion)是數組中亂序的關鍵值對

例如:
A E E L M O T R X P S
其中有6個逆序對:T-R T-P T-S R-P X-P X-S

  • T和R是亂序的,由於R應該在T的前面 T和P是亂序的,等等

咱們定義:若是一個數組的逆序對數量是線性的(或者說逆序對的數量 ≤ cn, 其中 c 表明一個常數),那麼這個數組是部分有序的。

部分有序的數組在實際應用中常常遇到,例若有一個大數組是有序的,只有最後加上的幾個元素是無序的,那麼這個數組就是部分有序的;
或者另外的狀況,只有幾個項不在最終位置,這個數組也是部分有序的。
實際應用中常常出現這樣的狀況,插入排序有意思的地方在於對於部分有序的數組。

咱們定義:插入排序在部分有序數組上的運行時間是線性的
證實:

  1. 就是交換的次數與逆序對的個數相等 (沒交換一次,逆序對就減小一個)
  2. 比較的次數 ≤ 交換的次數 + (n-1) (可能除了最後一個元素,在每次迭代中,一次比較會觸發一次交換)

算法改進

Binary insertion sort
使用二分查找來找出插入點

圖片描述

這就是插入排序 咱們學習的第二個基本排序方法。

Q. 若是數組已是升序排好的,那麼插入排序將進行多少次比較?

  • 常數次
  • 對數次
  • 線性次
  • 平方級次

A. 見附錄

Shellsort 希爾排序

算法介紹

希爾排序的出發點是插入排序。插入排序有時之因此效率低下是由於每一個元素每次只向前移動一個位置,即便咱們大概知道那些元素還須要移動很遠。
希爾排序的思想在於每次咱們會將數組項移動若干位置(移動 h 個位置),這種操做方式叫作對 數組進行 h-sorting (h - 排序)。
因此h-sorted array h-有序的 數組 包含 h 個不一樣的交叉的有序子序列。
例如,這裏 h = 4,若是從 L 開始,檢查每第四個元素 - M,P,T - 這個子數組(L M P T)是有序的,
從第二個位置 E 開始,檢查每第四個元素,- H, S, S - 是有序的...

圖片描述

這裏一共有4個交叉的序列,這個數組 是通過 h-sorting 後的 h-sorted 數組,這裏便是數組 {L E E A M H L E P S O L T S X R} 是通過 4-排序 後的 4-有序 的數組。

咱們想用一系列遞減 h 值的 h-排序 實現一種排序方法,這種排序方法由希爾(Shell)於1959年發明,是最先的排序方法之一。

又一例子:

圖片描述

這個例子中,從這裏所示的輸入 {S H E L L S O R T E X A M P L E} 開始,首先進行13-排序,移動幾個項,而後是 4-排序,移動的項多了一些,最後,1-排序。
這種算法的思想在於每次排序的實現基於前面排過序的序列,只須要進行少數幾回交換。

Q. 那麼首先咱們怎樣對序列進行 h-排序呢?
A. 實際上很簡單, 直接用插入排序,可是以前是每次獲取新的項往回走一個,如今往回走 h 個,因此代碼和插入排序是同樣的,只不過順着數組往回查看的時候以前每次只退1個,如今跳 h 個。這就是對數組進行h-排序的方法。

這裏咱們使用插入排序的緣由基於咱們對插入排序原理的理解有兩點:

  1. 首先是若是增量 h 很大。那麼進行排序的子數組長度就很小,包括插入排序在內的任何排序方法都會有很好的性能
  2. 另外一點是若是增量小,由於咱們以前已經用更大的h值進行了 h-排序,數組是部分有序的,插入排序就會很快

用選擇排序做爲 h-排序 的基礎就不行,由於不管序列是什麼順序,它總須要平方時間,數組是否有序對選擇排序沒有任何影響。

咱們看一個希爾排序的例子,增量是七、三、1
下圖每一行的標註了紅色的項就是本次迭代中發生過移動的項

  1. 咱們從 input 序列開始,先對它進行7-排序,進行的就是插入排序,只不過每次回退7。若是是增量是 7,也就是兩個元素間隔 6 個元素這麼取子序列,有4個子序列,各只包含2個元素。
  2. 而後進行3-排序。由於已經進行過7-排序,進行 3-排序 的元素要麼已經在最終位置,要麼只須要移動幾步。這個例子中,只有 A 移動了兩步。
  3. 而後進行 1-排序,由於數組已經通過 7-排序 和 3-排序,須要進行 1-排序 時,數組已經基本有序了,大多數的項只移動一兩個位置。

圖片描述

因此咱們只須要進行幾回額外的高增量排序,可是每一個元素都只向它們的最終位置移動了幾回,這是希爾排序的高效之處。實際上一旦進行了 1-排序,就是進行了插入排序,因此最終總能獲得正確排序的結果。惟一的區別就是能有多高效

對希爾排序更直觀的理解能夠經過數學證實。若是序列通過 h-排序的,用另外一個值 g 進行 g-排序,序列仍然是 h-有序的。
一個 h-有序的 數組是 h 交錯排序後的子序列。

咱們的命題:h-有序的 數組通過 g-排序 後依然是 h-有序的

以下圖:
3-sort[] = {A E L E O P M S X R T} 是數組 a[] = {S O R T E X A M P L E} 通過 7-排序 後,再通過 3-排序 後的數組,
3-sort[] 確定是 7-有序的 數組,固然也是 3-有序的,對7,3排序後得的序列 {A E L E O P M S X R T} 進行觀察, 每向右移動7位:
{A-S},{E-X},{L-R},{E-T}都是升序的,因此 3-sort[] 是 7-有序的 數組.
這就是那些看起來很顯然可是若是你試着證實它,會比你想的複雜一些的命題之一 -_-||, 而大多數人將這一點認定爲事實,這是希爾排序高效之處。

圖片描述

步長序列

另外一個問題就是對於希爾排序咱們應當使用哪一種步長序列.

首先能想到的想法多是試試2的冪, 1, 2, 4, 8, 16, 32, ...實際上這個行不通,由於它在進行1-排序以前不會將偶數位置的元素和奇數位置的元素進行比較,這意味着性能就會不好。

希爾本身的想法是嘗試使用2的冪減1序列,1, 3, 7, 15, 31, 63, …這是行得通的。

Knuth在60年代提出用 3x+1 的增量序列,如 一、四、1三、40、12一、364等,這也不錯

咱們使用希爾排序的時候,咱們首先找到小於待排序數組長度最大的增量值,而後依照遞減的增量值進行排序。可是尋找最好的增量序列是一個困擾了人們至關長時間的研究問題。

這是 Sedgewick 教授(這門課的主講老師之一)通過大概一年的研究得出的增量序列,1, 5, 19, 41, 109, 209, 505, 929, 2161, 3905, …
(該序列的項來自 9 x 4^i - 9 x 2^i + 1 和 2^{i+2} x (2^{i+2} - 3) + 1 這兩個算式。這項研究也代表 「 在希爾排序中是最主要的操做是比較,而不是交換。」
用這樣步長序列的希爾排序比插入排序要快,甚至在小數組中比快速排序和堆排序還快,可是在涉及大量數據時希爾排序仍是比快速排序慢。這個步長序列性能也不錯,可是沒法得知是不是最好的

Java 實現

這是用Java實現的希爾排序,使用 Knuth 的 3x+1 增量序列

import edu.princeton.cs.algs4.StdOut;

public class Shell {

    /**
     * 對數組進行升序排序
     * @param 須要排序的數組
     */
    public static <Key extends Comparable<Key>> void sort(Key[] a) {
        int n = a.length;

        // 3x+1 increment sequence:  1, 4, 13, 40, 121, 364, 1093, ...
        int h = 1;
        while (h < n/3) h = 3*h + 1;// 至於爲何是 h < n/3 請查看附錄

        while (h >= 1) {
            // 對數組進行 h-排序 (基於插入排序)
            for (int i = h; i < n; i++) {
                for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) {
                    exch(a, j, j-h);
                }
            }
            assert isHsorted(a, h);
            // 計算下一輪排序使用的增量值
            h /= 3;
        }
        /**
         * assert [boolean 表達式]
         * 若是[boolean表達式]爲true,則程序繼續執行。
         * 若是爲false,則程序拋出AssertionError,並終止執行。
         * assert [boolean 表達式]:'expression'
         */
        assert isSorted(a);
    }

    // is v < w ?
    private static <Key extends Comparable<Key>> boolean less(Key v, Key w) {
        return v.compareTo(w) < 0;
    }

    // exchange a[i] and a[j]
    private static void exch(Object[] a, int i, int j) {
        Object swap = a[i];
        a[i] = a[j];
        a[j] = swap;
    }


    // 檢查數組是否已排好序
    private static <Key extends Comparable<Key>> boolean isSorted(Key[] a) {
        for (int i = 1; i < a.length; i++)
            if (less(a[i], a[i-1])) return false;
        return true;
    }

    // 檢查數組是不是 h-有序的?
    private static <Key extends Comparable<Key>>  boolean isHsorted(Key[] a, int h) {
        for (int i = h; i < a.length; i++)
            if (less(a[i], a[i-h])) return false;
        return true;
    }

    // 打印數組到標準輸出
    private static void show(Comparable[] a) {
        for (int i = 0; i < a.length; i++) {
            StdOut.print(a[i]);
        }
    }

    // 簡單客戶端
    public static void main(String[] args) {
        String[] a = {"1","5","3","8","4","1","4","5"};
        Shell.sort(a);
        show(a);
    }
}

咱們直接計算小於 n/3 的最大增量, 而後以那個值開始,好比從 364 開始,須要計算下一個增量時,直接 364 整除 3 等於 121,121 整數除 3 等於 40 等。這句 h = h / 3 計算下一輪排序使用的增量值。

實現就是基於插入排序。進行插入時 i 從 h 開始,而後 j 循環,每次 j 減少 h,否則代碼就和插入排序如出一轍了。因此,只須要給 h-排序 加上額外的循環計算插入排序的增量,代碼變得稍微複雜了一些,可是對於大數組運行起來,Shell排序的效率要比插入排序高得多。
隨着h值遞減,每次 h-排序 後數組愈來愈有序

算法分析

對於 3x+1 的增量序列最壞狀況下比較的次數是 O(N^3/2),實際應用中比這個小得多。
問題是沒有精確的模型可以描述使用任何一種有效的增量序列的希爾排序須要進行比較的次數。
下圖是經過 Doubling hypothesis 方法,簡單說就是翻倍輸入的方法對希爾排序的性能試驗得出的結果 與 推斷的函數模型計算值的對比

  • N:原始輸入數據的大小;compares:對應的輸入須要經過屢次比較獲得徹底有序數組;
    N^1.289: 對應輸入大小的1.289次冪;2.5 N lg N:對應輸入的對數計算值

圖片描述

希爾排序的比較次數是 n 乘以增量的若干倍,即 n 乘以 logn 的若干倍,可是沒人可以構建精確的模型對使用有效的增量序列的希爾排序證實這一點。

那咱們爲何還對這個算法感興趣呢?由於這個算法的思想很簡單,並且能得到巨大的性能提高。它至關快,因此在實際中很是有用除了巨大的數組會變得慢,對於中等大小的數組,它甚至能夠賽過經典的複雜方法。代碼量也不大,一般應用於嵌入式系統,或者硬件排序類的系統,由於實現它只須要不多的代碼。

還有就是它引出了不少有趣的問題。這就涉及到了開發算法的智力挑戰。若是你以爲咱們已經研究了這麼長時間的東西很平凡,能夠去試着找一個更好的增量序列。嘗試一些方法發現一個,而且試着就希爾排序的通常狀況的性能得出一些結論。人們已經嘗試了50年,並無得到多少成果。
咱們要學到的是咱們不須要不少的代碼就能開發出很好的算法和實現,而依然有一些等待被發現,也許存在某個增量序列使得希爾排序比其餘任何適用於實際序列大小的排序方法都快,咱們並不可否認這一點。這就是希爾排序,第一個不平凡的排序方法。

洗牌算法 Shuffling

洗牌與洗牌算法介紹

接下來咱們將一塊兒看一個排序的簡單應用, 這個應用叫作洗牌.
假設你有一副撲克牌, 你可能會想要作的事之一就是隨機地進行擺放卡牌(目標), 這就是洗牌。

圖片描述

咱們有一種利用排序來進行洗牌的方法,雖然排序彷佛正好與洗牌相反。
這種方法的構想是爲一個數組元素產生一個隨機實數,而後利用這些隨機數做爲排序依據。

圖片描述

這是一種頗有效的洗牌方法,而且咱們能夠證實這種方法在輸入中沒有重複值,而且你在能夠產生均勻隨機實數的狀況下,就可以產生一個均勻的隨機排列。若是每種可能的撲克牌排列方式都有相同的出現機率,那就說明這種洗牌方法是正確的。

正確當然好,但這種方法須要進行一次排序,彷佛排序對於這個問題來講有些累贅。如今的問題是咱們可否作得更好。咱們能找到一種更快的洗牌方法嗎? 咱們真的須要付出進行一次完整排序的代價嗎? 這些問題的答案是否認的。
實際上有一種很是簡單的方法,能夠產生一副均勻隨機排列的撲克牌,它只須要線性的時間來完成工做。這種方法的理念是將序數 i 從左到右地遍歷數組,i 從 0 到 n 增量。咱們從一個已經有序的數組開始洗牌,實際上數組的初始狀況並不影響洗牌,每次咱們都均勻隨機地從 0 和 i 之間挑選一個整數,而後將 a[i] 與這個數表明的元素交換。

洗牌動態演示連接

  • 開始時咱們什麼也不作,只把第一個元素和它本身交換位置,
  • i 變成了2或者說 i 指向了第二張牌,咱們隨機生成一個 r (在 0 和 i 之間的整數,由於 r 是隨機均勻生產的,因此 r 有可能等於 i,i 和 r 的值相同就不用進行交換), 而後咱們將這 i 位置和 r 位置的兩張牌
  • 遞增 i 的值,而後生成一個隨機整數 r,再交換

一直這樣繼續進行交換位置。對於每個 i 的值,咱們都正好進行一次交換, 可能有些牌經歷了不止一次交換, 但這並不存在問題, 重點是在第
i 張牌左邊的牌都是被均勻地洗過去的,在最後咱們就會得到一副洗好的撲克牌。
這是一個利用隨機性的線性時間洗牌算法,它在很早以前就被證實是正確的,那時甚至電腦實現還未被髮明。若是你使用這種方法的話,你會在線性時間內獲得一個均勻隨機的排列,因此,這絕對是一種簡單的洗牌方法。

Java 實現

  • 在每次迭代中,隨機均勻地選擇 0 和 i 之間的整數 r
  • 交換 a[i] 和 a[r].
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.StdRandom;

public class Shuffling {

    public static void shuffle(int[] a) {
        int n = a.length;
        for (int i = 0; i < n; i++) {
            int r = StdRandom.uniform(i + 1);     // [0,i+1) = between 0 and i
            int temp = a[i];
            a[i] = a[r];
            a[r] = temp;
        }
    }

    private static void show(int[] a) {
        for (int i = 0; i < a.length; i++) {
            StdOut.print(a[i]);
        }
    }

    // simple client
    public static void main(String[] args) {

        int[] a = {1,2,3,4,5,6,7,8,9};
        shuffle(a);
        show(a);
    }
}

分別進行三次洗牌:

圖片描述

它實現起來也很簡單,生成的隨機數均勻分佈在 0 和 i 之間 (相當重要!)。
你會常常看到程序員們自覺得他們實現了一個洗牌應用,實際上他們常常只是爲每一個數組位置選擇了一個隨機數組位置與之交換,這種方法實際上並不能實現真正的洗牌。你能夠對編號爲 i 和 n-1 之間的那些你尚未看到過的牌進行操做,但這種方法並不能洗出一副均勻隨機的卡牌。
下面是一個關於軟件安全的例子,在軟件安全領域有不少難度高而且深層次的問題,可是有一件事是咱們能夠作的那就是確保咱們的算法和宣傳中中說的同樣好。
這裏有一個在線撲克遊戲的實現案例在此, 下面就是你能夠在網頁上找到的洗牌算法案例的代碼

圖片描述

Bugs:

  1. 隨機數 r 永遠不會等於 52 ⇒ 這意味着最後一張牌會始終在最後一位出現
  2. 這樣洗出的牌不是均勻的 應該在 1 到 i 或者 i+1 和 52 之間隨機挑牌交換
  3. 另外一個問題是在這種實現方式中使用一個 32 位數字生成隨機數。若是你這麼作的話並不能涵蓋所有可能的洗牌方式。若是共有52張牌,可能的洗牌方法一共有 52 的階乘那麼多種,這可比 2 的 32 次冪大得多,因此這種方法根本沒法產生均勻隨機的牌組
  4. 另外一個漏洞則是生成隨機數的種子是從午夜到如今這段時間經歷的毫秒數,這使得可能的洗牌方式變得更少了。事實上,並不須要多少黑客技巧,一我的就能從 5 張牌中看出系統時鐘在幹什麼。你能夠在一個程序裏實時計算出全部未來的牌。

(關於這個理解,能夠查看edu.princeton.cs.algs4.StdRandom :

private static Random random;    // pseudo-random number generator
    private static long seed;        // pseudo-random number generator seed

    // static initializer
    static {
        // this is how the seed was set in Java 1.4
        seed = System.currentTimeMillis();
        random = new Random(seed);
    }

    public static void setSeed(long s) {
        seed   = s;
        random = new Random(seed);
    }

如今的 jdk 已經再也不使用這種方式去定義seed了,正如以前所說的,這會是個bug

)

若是你在作一個在線撲克應用的話 這是一件很是糟糕的事情,由於你確定但願你的程序洗牌洗得像廣告裏說的那麼好。有許多關於隨機數的評論,其中頗有名的一句是 "The generation of random numbers is too important to be left to chance -- Robert R. Coveyou" 隨機數的生成太太重要。

人們嘗試了各類洗牌方法來保證其隨機性, 包括使用硬件隨機數生成器,或者用不少測試來確認它們的確實是隨機的。因此若是你的業務依賴於洗牌, 你最好使用好的隨機洗牌代碼,洗牌並無我想象的那麼簡單,一不當心就會出現不少問題。這是咱們的第一個排序應用。

Comparators 比較器

程序員常常須要將數據進行排序,並且不少時候須要定義不一樣的排序順序,好比按藝術家的姓名排序音樂庫,按歌名排序等。

圖片描述

在Java中,咱們能夠對任何類型實現咱們想要的任何排序算法。Java 提供了兩種接口:

  • Comparable (java.lang.Comparable)
  • Comparator (java.util.Comparator)

使用 Comparable 接口和 compareTo() 方法,咱們可使用字母順序,字符串長度,反向字母順序或數字進行排序。 Comparator 接口容許咱們以更靈活的方式執行相同操做。

不管咱們想作什麼,咱們只須要知道如何爲給定的接口和類型實現正確的排序邏輯。

在文章的最開始咱們就談論過,Java 標準庫中會用到排序的類型經過實現 Comparable 接口,也就是這些數據類型實現 compareTo() 方法的實例方法,來實現排序功能。實現此接口的對象列表(和數組)能夠經過 Collections.sort(和 Arrays.sort)進行自動排序。

Comparable 接口:回顧

Comparable 接口對實現它的每一個類的對象進天然排序,compareTo() 方法被稱爲它的天然比較方法。所謂天然排序(natural order)就是實現Comparable 接口設定的排序方式。排序時若不指定 Comparator (專用的比較器), 那麼就以天然排序的方式來排序。

考慮一個具備一些成員變量,如歌曲名,音樂家名,發行年份的 Musique (法語哈哈哈,同 Music) 類。 假設咱們但願根據發行年份對歌曲列表進行排序。 咱們可讓 Musique 類實現Comparable 接口,並覆蓋 Comparable 接口的 compareTo() 方法。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @program: algo
 * @description: Exemple to implement Comparable interface for a natural order
 * @author: Xiao~
 * @create: 2019-03-28 14:35
 **/

public class Musique implements Comparable<Musique> {

    private final String song;
    private final String artist;
    private final int year;


    public Musique(String song, String artist, int year) {
        this.song = song;
        this.artist = artist;
        this.year = year;
    }

    /**
     *
     * @param musique
     * @return natural order by year
     * -1 : <
     * +1 : >
     * 0 : ==
     */
    @Override
    public int compareTo(Musique musique) {

        return this.year - musique.year;
    }

    @Override
    public String toString() {
        return "Musique{" +
                "song='" + song + '\'' +
                ", artist='" + artist + '\'' +
                ", year=" + year +
                '}';
    }

    // simple client
    public static void main(String[] args){
        List<Musique> list = new ArrayList<>();
        // 暴露歌單系列
        list.add(new Musique("You're On My Mind","Tom Misch",2018));
        list.add(new Musique("Pumped Up Kicks","Foster The People",2011));
        list.add(new Musique("Youth","Troye Sivan",2015));
        // 經過 Collections.sort 進行自動排序
        Collections.sort(list);

        list.forEach(System.out::println);
    }
}

運行結果

圖片描述

如今,假設咱們想要按照歌手和歌名來排序咱們的音樂清單。 當咱們使一個集合元素具備可比性時(經過讓它實現Comparable接口),咱們只有一次機會來實現compareTo()方法。解決方案是使用 Comparator 接口

Comparator 接口

Comparator 接口對實現它的每一個類的對象進輪流排序 (alternate order)

實現 Comparator 接口意味着實現 compare() 方法

jdk 8:

public interface Comparator<T> {
  
    int compare(T o1, T o2);
    ...
}

特性要求:必須是全序關係
寫圖中若是前字母相同就會比較後一個字母,以此類推動行排序

圖片描述

與 Comparable 接口不一樣,Comparable 接口將比較操做(代碼)嵌入須要進行比較的類的自身中,而 Comparator 接口則在咱們正在比較的元素類型以外進行比較,即在獨立的類中實現比較。
咱們建立多個單獨的類(實現 Comparator)以便由不一樣的成員進行比較。
Collections 類有兩個 sort() 方法,其中一個 sort() 使用了Comparator,調用 compare() 來排序對象。

圖片描述

Comparator 接口: 系統排序

若是要使用 Java 系統定義的 Comparator 比較器,則:

  • 建立 Comparator 對象。
  • 將第二個參數傳遞給Arrays.sort() 或者 Collections.sort()
String[] a;
...
// 這般如此使用的是天然排序
Arrays.sort(a);
...
/**
* 如下這般這般這般都是使用Comparator<String> object定義的輪流排序
**/
Arrays.sort(a, String.CASE_INSENSITIVE_ORDER);
...
Arrays.sort(a, Collator.getInstance(new Locale("es")));
...
Arrays.sort(a, new BritishPhoneBookOrder());
...

Comparator 接口: 使用自定義的 sorting libraries

在咱們自定義的排序實現中支持 Comparator 比較器:

  • 將 Comparator 傳遞給 sort() 和less(),並在less() 中使用它。
  • 使用 Object 而不是 Comparable

請參考:這個Insertion 和 這個InsertionPedantic

圖片描述

import java.util.Comparator;

public class InsertionPedantic {

    // 使用的是 Comparable 接口和天然排序
    public static <Key extends Comparable<Key>> void sort(Key[] a) {
        int n = a.length;
        for (int i = 1; i < n; i++)
            for (int j = i; j > 0 && less(a[j], a[j-1]); j--)
                exch(a, j, j-1);
    }

    // 使用的是 Comparator 接口實現的是客戶自定義的排序
    public static <Key> void sort(Key[] a, Comparator<Key> comparator) {
        int n = a.length;
        for (int i = 1; i < n; i++)
            for (int j = i; j > 0 && less(comparator, a[j], a[j-1]); j--)
                exch(a, j, j-1);
    }
    
        // is v < w ?
    private static <Key extends Comparable<Key>> boolean less(Key v, Key w) {
        return v.compareTo(w) < 0;
    }

    // is v < w?
    private static <Key> boolean less(Comparator<Key> comparator, Key v, Key w) {
        return comparator.compare(v, w) < 0;
    }
    ...
}

Comparator 接口: 實現

實現 Comparator :

  • 定義一個(嵌套)類實現 Comparator 接口
  • 實現 compare() 方法
  • 爲 Comparator 提供客戶端訪問權限

下邊爲咱們的音樂列表實現按歌名排序的比較器:
這裏我改了一下,把按歌名排序做爲天然排序,而後爲按歌手和發行年份都建立了兩個單獨的,嵌入的,實現 Comparator 接口的類
而且提供客戶端訪問這些內部類

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @program: algo
 * @description: Exemple to implement Comparable interface for a natural order
 * @author: Xiao~
 * @create: 2019-03-28 14:35
 **/

public class Musique implements Comparable<Musique> {

    public static final Comparator<Musique> ARTIST_ORDER = new ArtistOrder();
    public static final Comparator<Musique> YEAR_ORDER = new YearOrder();

    private final String song;
    private final String artist;
    private final int year;


    public Musique(String song, String artist, int year) {
        this.song = song;
        this.artist = artist;
        this.year = year;
    }

    /**
     * @param musique
     * @return natural order: order by song name
     */
    @Override
    public int compareTo(Musique musique) {

        return this.song.compareTo(musique.song);
    }

    // comparator to music by artist name
    private static class ArtistOrder implements Comparator<Musique> {

        @Override
        public int compare(Musique o1, Musique o2) {
            // Sting class has implemented Comparable interface, we use his native compareTo()
            return o1.artist.compareTo(o2.artist);
        }
    }

    // comparator to music by year published
    private static class YearOrder implements Comparator<Musique> {

        @Override
        public int compare(Musique o1, Musique o2) {
            // this trick works here (since no danger of overflow)
            return o1.year - o2.year;
        }
    }

    /**
     * Returns a comparator for comparing music in lexicographic order by artist name.
     *
     * @return a {@link Comparator} for comparing music in lexicographic order by artist name
     */
    public static Comparator<Musique> byArtistName() {
        return new ArtistOrder();
    }

    /**
     * Returns a comparator for comparing music order by year published.
     *
     * @return a {@link Comparator} for comparing music by year published.
     */
    public static Comparator<Musique> byYear() {
        return new YearOrder();
    }


    @Override
    public String toString() {
        return "Musique{" +
                "song='" + song + '\'' +
                ", artist='" + artist + '\'' +
                ", year=" + year +
                '}';
    }

    // simple client
    public static void main(String[] args) {
        List<Musique> list = new ArrayList<>();

        list.add(new Musique("You're On My Mind", "Tom Misch", 2018));
        list.add(new Musique("Pumped Up Kicks", "Foster The People", 2011));
        list.add(new Musique("Youth", "Troye Sivan", 2015));
        list.add(new Musique("Royals", "Lorde", 2013));
        list.add(new Musique("Atlas", "Coldplay", 2013));
        list.add(new Musique("Sugar Roses", "Elsa Kopf", 2013));

        Collections.sort(list);
        System.out.println("\nOrder by Song name (natural order):");
        list.forEach(System.out::println);

        System.out.println("\nOrder by artist name:");
        Collections.sort(list,Musique.byArtistName());
        list.forEach(System.out::println);

        System.out.println("\nOrder by year published:");
        Collections.sort(list,Musique.byYear());
        list.forEach(System.out::println);
    }
}

穩定性

典型的應用:
首先,簡化下咱們的測試客戶端:先進行歌手排序; 而後按年份排序。

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

        list.add(new Musique("You're On My Mind", "Tom Misch", 2018));
        list.add(new Musique("Pumped Up Kicks", "Foster The People", 2011));
        list.add(new Musique("Youth", "Troye Sivan", 2015));
        list.add(new Musique("Royals", "Lorde", 2013));
        list.add(new Musique("Atlas", "Coldplay", 2013));
        list.add(new Musique("Sugar Roses", "Elsa Kopf", 2013));

        System.out.println("\nOrder by artist name:");
        Collections.sort(list,Musique.byArtistName());
        list.forEach(System.out::println);

        System.out.println("\nOrder by year published:");
        Collections.sort(list,Musique.byYear());
        list.forEach(System.out::println);
    }

運行結果:

圖片描述

進行年排序的時候歌手名排序任然保留,排序是穩定的。

一個穩定的排序保留具備相同鍵值的項的相對順序。也就是說,一旦你按歌手名排列了以後,接着你想按第二項排列,上邊是按照年份,而且對於全部在第二項具備相同鍵關鍵字的記錄保持按歌手名排列。實際上,不是全部的排列都保留那個性質,這就是所謂的穩定性。

Q. 那插入排序和選擇排序,他們都是穩定的排序嗎?
A. 插入排序是穩定的,相同的元素在比較的過程不會再互相互換位置,可是選擇排序是會的,因此選擇排序並不穩定

下邊是使用選擇排序的運行結果:
首先按名字排序,而後再按 section(第二列) 排序

圖片描述

如圖能夠看到進行section排序的時候,上一次的按名字排序的順序再也不保留,選擇排序並不穩定,shellsort 也不穩定

這裏有另外一個典型的例子,人們想買一場搖滾演唱會的門票,咱們有一個按照時間排列的序列,而後將這個序列再按地點排列,咱們所但願的是這些按地點排列的序列同時能保持時間順序 ,如圖是一個不穩定的排序,按地點排序完了以後它不會保持按時間的排序,若是他們想用其中一個記錄,他們還得從新排列。但若是他們用的是穩定的排序,這些記錄還保持着按時間的排序,因此對不少的應用都但願排序算法有穩定性。

clipboard.png

值得注意的是,咱們在查看代碼去判斷排序算法是不是穩定的時候要仔細看下比較邏輯使用的是 "<" 仍是 "<=".
這些操做是否穩定取決於咱們的代碼怎麼寫。在咱們的代碼中,若是幾個個鍵是相等的,好比咱們有以下序列:

  • B1 A1 A2 A3 B2 , 其中 A1 = A2 =A3; B1 = B2

咱們要保證排序的時候結果是:A1 A2 A3 B1 B2 , 而不會出現:A3 A1 A3 B1 B2 或者其餘狀況。
在插入排序中,當咱們獲得 A1,而後排完順序後,在這種狀況下,它數組中就是在起始位置,而當咱們獲得第二個 A,也就是 A2,只要找到不小於 A2 的記錄就中止排列,A1,A2 這倆是相等的,是不小於的,咱們就中止排序。因此排序從不越過相同值的記錄。若是這裏是小於等於,那麼它是不穩定的,或者若是咱們用另外一種方法並據此運行,在代碼中讓相同值的記錄從不越過彼此,那麼排序就是穩定的。

具體能夠查看插入排序和選擇排序的源碼。

至今爲止咱們看到的排序中插入排序是穩定的,後邊的歸併排序也是穩定的。

Convex hull 凸包

如今咱們將經過一個有趣的計算幾何領域的例子來了解排序的應用

定義

假設如今平面上有一個 n 個點構成的集合, 從幾何角度咱們能夠找到一個「凸包」, 也就是能包含全部點的最小的凸多邊形.

圖片描述

由點所構成的集合都有凸包, 凸包還有不少等價的定義方式:
凸包是包含全部點的最小的凸狀集合
凸包是最小的能圈起全部點的凸多邊形
凸包是最小的包含全部點, 而且頂點也都屬於這個集合的凸多邊形

咱們要作的就是編寫一個程序, 對於給定的點集生成它的凸包。
那麼這個程序的輸出應該是什麼呢?咱們要用怎樣的函數呢?
爲了這個結果能夠更明確易用,這個程序應該輸出這個凸包的頂點序列。可是若是集合中的某些點位於凸包的邊上,但不是凸包的頂點,那麼這些點就不該該被包含在輸出序列中。這也例證了計算幾何每每是很是困難的。

圖片描述

由於在編程過程當中處理這種共線性的狀況是很困難的,這門課程將不會耗費不少時間在這個問題上,可是咱們必需要能意識到在這類問題中,當咱們試着運用一些簡單算法時,實際狀況可能會變得比預想的複雜不少。

凸包的機械算法:在每一個點上紮上圖釘,而後用一根帶子將全部的釘子圍起來收緊,這樣咱們就獲得了這個點集的凸包。
算法連接

圖片描述

咱們不會用電腦去編寫這樣一個程序,可是這表示其實咱們能夠很好地解決這個問題。

應用

移動規劃

如今咱們有一個電腦程序來計算凸包,假設有一個機器人想從s點去t點,可是這中間有一個多邊形的障礙物,你想要繞過障礙物抵達t點,最短的路徑必定是如下兩種狀況之一:
s到t的直線,或者是這個點集的凸包的一部分。

圖片描述

相距最遠的兩點

若是你想找到這個點集中相距最遠的兩點,有些時候這對於統計計算,或者其餘一些應用都是很是重要的。這兩個點在凸包上。

圖片描述

若是咱們已經知道了這個點集的凸包,那麼這個問題就會變得很是簡單,由於這兩個點就會是凸包上的兩個端點。所以咱們能夠利用凸包的不少幾何特性來編寫算法。

凸包的特性

這裏有兩個特性:

  1. 只能經過逆時針轉動來穿過凸包
  2. 凸包的頂點相對於具備最低 y 座標的點 p 以極角的遞增順序出現

圖片描述

  • 第一,如今你只能用逆時針,或者說是左轉的方式來穿過凸包。

    • 咱們從 p 點到 1 號點,再從 1 號點左轉到 5 號點,或者說是逆時針轉到 5 號點,而後咱們再到 9 號、12號點,最終回到起始點
  • 第二,若是你選擇在y軸上座標最小的點,也就是最低點,做爲 p 點,那麼咱們來看一下接下來各點的極角,對比於點 p 的極角,你能夠發現 從 x 軸的 p 點指向每一個點,這些向量的極角值是遞增的,這也是顯而易見的事實。

咱們將要學習的算法:葛立恆掃描法,就是基於以上這兩個事實。

  • 咱們將p點做爲起始點,也就是y軸座標值最小的點
  • 按照以 p (0) 爲起點的極角從小到大的順序,將其它全部的點進行排序 (1-12)
  • 而後咱們直接捨棄那些沒法產生逆時針旋轉的點

咱們將p點做爲起始點, 按照極角從小到大的順序將全部的點排序, 若是咱們選擇一條向量, 朝着逆時針方向掃描, 這條向量碰到這些點的順序是怎樣的呢?

圖片描述

而後咱們就完成了這個計算過程。經過葛立恆掃描法找到了凸包,在實現這個算法的時候有一些難點,咱們不會去細究它們,由於這幾節課是講排序算法的,而不是計算幾何學。可是這些說明即便咱們有了很好的排序算法,咱們也可能須要作一些額外的工做才能在應用中真正地解決問題。

葛立恆掃描法:實現中的挑戰

  • 咱們如何來找到擁有最小y座標值的點呢?

    • 咱們能夠經過排序(全序排序),咱們按照 y 座標值的大小,將各個點排序(下個內容會涉及)
  • 如何根據極角的大小對點進行排序?

    • 一樣的咱們要定義如何比較這些點(經過定義全序排序,下個內容會涉及)
  • 如何根據不一樣的屬性對這些點進行排序?

    • 葛立恆掃描法是一個完美的例子,咱們不只僅要學會如何排序,和不只僅要根據定義和比較來排序,還要能對一樣的對象進行不一樣方式的排序。 葛立恆掃描法這個例子能夠很好地幫助咱們學習這一點,如何判斷兩點間是不是逆時針旋轉,這是幾何學的一個小知識點,請查看下方的代碼實現
  • 咱們應該如何更高效地排序呢?

    • 咱們可使用希爾排序,可是接下來教程,咱們會用經典的排序法,包括歸併排序快速排序。這個例子很好地向咱們闡釋了高效的排序算法讓凸包算法也更高效。這一點對於設計更好的算法是很是重要的原則。一旦咱們有了一個好算法,當咱們遇到另一個問題時,咱們就能夠想想咱們可不能夠用它來解決新問題。對於凸包計算,咱們有一個好的排序算法就能夠獲得一個好的凸包算法,由於計算凸包最主要的部分就是排序。

然而在不少現實問題中,由於各類共線問題,實現凸包計算將會面臨不少困難。這些在接下來的內容都會涵蓋。如今來簡短地講解一下有一個凸包計算的主要部分:
假設平面上有三個點, 點 a、點 b 和點 c, 你須要按照逆時針旋轉的方式從點 a 走到點 b 再抵達點 c
在這個例子中咱們能夠看到只有兩個是按照逆時針走的, 其它不是。

圖片描述

咱們如今須要一種計算方法來區分這種左轉和右轉,若是咱們不考慮共線的狀況,實現這種計算將會很簡單。可是若是這些點在同一個直線上,或者斜率是無限大的,咱們該如何計算。因此咱們要將這種狀況也考慮進來,因而咱們的編程過程就不像以前想象的那樣簡單了。咱們須要處理共線現象以及浮點數的精確度,可是那些計算幾何學的研究者已經解決了這些問題,而且最終的執行代碼並無那麼多。

實現 ccw

數學過程

感興趣能夠研究下~~
CCW: 給定三個點:a, b 和 c, a --> b --> c 是不是逆時針方向?

這個計算的基本思想是計算 a 與 b 連線的斜率,和 b 與 c 連線的斜率,比較這二者後,肯定轉向結果是逆時針,或者是順時針。
這是詳細的數學過程。

圖片描述

・If signed area > 0, then a → b → c is counterclockwise 逆時針方向.
・If signed area < 0, then a → b → c is clockwise 順時針方向.
・If signed area = 0, then a → b → c are collinear 共線的.

圖片描述

因此若是咱們用平面上的點做爲數據來實現這一幾何計算,咱們能夠直接用 ccw() 這個函數計算 (b.x-a.x)(c.y-a.y) - ( b.y-a.y)(c.x-a.x)

public class Point2D
{
    private final double x;
    private final double y;
    
    public Point2D(double x, double y)
    {
        this.x = x;
        this.y = y;
    }
...
    public static int ccw(Point2D a, Point2D b, Point2D c)
    {
        // 這裏可能會由於浮點數的四捨五入而引發錯誤
        double area2 = (b.x-a.x)*(c.y-a.y) - (b.y-a.y)*(c.x-a.x);
        if (area2 < 0) return -1; // clockwise 順時針方向
        else if (area2 > 0) return +1; // counter-clockwise 逆時針方向
        else return 0; // collinear 共線的
    }
}

而後咱們就會看到這裏立刻能夠告訴你這個轉彎是逆時針、順時針仍是沿着直線。這部分代碼並很少,這個函數是葛立恆掃描法的基本部分。

葛立恆掃描法用兩種方式對點進行排序,而後將它們放入棧中。這裏將每個點放進棧,直到遍歷全部的點,而後對於用極角排序的棧,咱們比較最上面的兩個點和第三個點,看看它們的連線是否構成了一個逆時針的轉彎。若是不是逆時針 咱們就將這個點推出繼續尋找下一個點。
能夠看到在已有排序算法的狀況下咱們只須要不多的代碼完成凸包算法。
咱們有不少現成的排序應用 咱們也會用更高效的排序 來寫一些新的算法來提升效率。

課後問題

Q. 給定兩個數組 a [] 和 b [],每一個數組在平面中包含 n 個不一樣的 2D 點,設計一個複雜度爲 ~(n^2) 的算法來計算數組 a [] 和 數組 b [] 中包含的點數
A. 用 shellsort 或者其餘複雜度 ~(n^2) 的算法對 2D 點進行排序(先 x 再 y),排序後,同時對每一個數組進行掃描(~ n)

Q. 給定兩個大小爲 n 的整數數組,設計一個複雜度爲 ~(n^2) 的算法來肯定一個數組是不是另外一個數組的置換矩陣。 也就是說,它們是否包含徹底相同的元素,只是順序不一樣。
A. 對兩個數組進行排序而後判斷就行

Q.荷蘭國旗問題
A.這是一個很經典的關於排序的算法問題,網上也有不少的解釋能夠查到 (連接 Dutch national flag等更新)

附錄

Q. 若是數組已經排好序,那麼插入排序比較須要多少次?
A. 平方級
由於選擇排序所須要的對比次數與數組是否排好序無關

Q. 若是數組已是升序排好的,那麼插入排序將進行多少次比較?
A. 線性級別
除了第一個元素,其它每一個元素都和它左邊的元素進行一次比較(除此以外再也不有比較),因此 n 個元素,就有 n−1 次比較.

Q. 爲何使用 3x + 1 步長的希爾排序在程序中去構建每次步長時用:while (h < n/3)?
A.

  1. 這個步長序列值來自於 3x + 1 < N/3 即 h < N/3
    能夠參考維基百科:Shellsort中關於步長序列列表的 Gap sequences-A003462 已證實的結果 (3^k-1)/2 not greater than [N/3]
    更詳細的請查看 Sedgewick 教授的證實 :全英+須要必定的數學及數學分析,沒有足夠的基礎,不用深究,這些證實交給數學家或者理論學家就好
  2. 爲何不用 h = (h - 1)/3,由於 h / 3 是一個整數除法,結果會丟掉餘數,也就是說若是 h = 7 則 7 / 3 = 2,與 6 / 3 = 2 是同樣的結果
相關文章
相關標籤/搜索