字符串的排序可使用通用排序算法。java
下面這些排序算法比通用排序算法效率更高,它們突破了NlogN的時間下界。由於基數排序不須要直接將元素進行比較和交換,只是對元素進行「分類」。算法
算法 | 是否穩定 | 原地排序 | 運行時間 | 額外空間 | 優點領域 |
低位優先的字符串排序(LSD) | 是 | 否 | O(W (N+R) ) | N | 較短的定長字符串 |
高位優先的字符串排序(MSD) | 是 | 否 | O(W(N+R)) | N+WR | 隨機字符串 |
三向字符串快速排序 | 否 | 是 | O(NlogN) | W+logN | 通用排序算法,特別適用於含有較長公共前綴的字符串數組 |
注:字母表的長度爲R,待排序的字符串個數爲N,字符串平均長度爲w,最大長度爲W。性能
例如:一個公司有不少個部門,而後須要將員工按照部門排序。spa
使用int數組count[]計算每一個鍵出現的頻率,若是鍵爲r,則count[r+1]++; (注意爲何是r+1)..net
使用count[]數組計算每一個鍵在排序結果中的起始位置。通常來講,任意給定鍵的起始索引均爲較小鍵所出現的頻率之和,計算方法爲count[r+1] += count[r]; 從左到右將count[]數組轉化爲一張用於排序的索引表。code
將全部元素移動到一個輔助數組aux[]中進行排序。每一個元素在aux[]中對應的位置由它的鍵對應的count[]決定。在移動以後將count[]中對應的元素值加1,來保證count[r]老是下一個鍵爲r的元素在aux[]中的索引的位置。這個過程只需遍歷一次便可產生排序結果,這種實現方法具備穩定性----鍵相同的元素排序後會被彙集到一塊兒,但相對位置沒有發生改變(後面兩種排序算法就是基於此算法的穩定性來實現的)。blog
將將排序的結果複製回原數組中。排序
時間複雜度:O( N+R )遞歸
基於鍵索引記數法來實現。
低位優先的字符串排序可以穩定地將定長字符串進行排序。生活中不少狀況須要將定長字符串排序,好比車牌號、身份證號、卡號、學號......
算法思路:低位優先的字符串排序能夠經過鍵索引記數法來實現----從右至左以每一個位置的字符做爲鍵,用鍵索引記數法將字符串排序W遍(W爲字符串的長度)。稍微思考下就能夠理解,由於鍵索引記數法是穩定的,因此該方法可以產生一個有序的數組。
public class LSD { public static void sort(String[]a,int W) { int N = a.length; int R = 256; String[] aux = new String[N]; //循環W次鍵索引記數法 for(int d = W-1; d>=0;d++) { int[] count = new int[R+1]; //鍵索引記數法第一步--頻率統計 for(int i=0;i<N;i++) count[a[i].charAt(d)+1]++; //鍵索引記數法第二步--將頻率轉化爲索引 for(int r=0;r<R;r++) count[r+1]+=count[r]; //鍵索引記數法第三步--排序 for(int i=0;i<N;i++) aux[count[a[i].charAt(d)]++] = a[i]; //鍵索引記數法第四步--回寫 for(int i=0;i<N;i++) a[i]=aux[i]; } } }
從代碼能夠看出,這是一種線性時間排序算法,不管N有多大,它都只遍歷W次數據。
對於基於R個字符的字母表的N個以長爲W的字符串爲鍵的元素,低位優先字符串排序的運行時間爲NW,使用的額外空間與N+R成正比(大小爲R的count數組和大小爲N的aux數組)。
時間複雜度:O(W(N+R))
起始:
末位排序:
倒數第二位排序:
......
結束:
本算法也是基於鍵索引記數法來實現的。
高位優先字符串排序是一種遞歸算法,它從左到右遍歷字符串的字符進行排序。和快速排序同樣,高位優先字符串排序算法會將數組切分爲可以獨立進行排序的子數組進行排序,但它的切分會爲每一個首字母獲得一個子數組,而非像快速排序那樣產生固定的兩個或三個數組。
核心思想:先使用鍵索引記數法根據首字符劃分紅不一樣的子數組,而後用下一個字符做爲鍵遞歸地處理子數組。
由於是不一樣長度的字符串,因此要關注字符串末尾的處理狀況。能夠將全部字符都已經被檢查過的字符串所在的數組排在全部子數組的前面,這樣就不須要遞歸地將該數組排序。
算法實現:引入直接插入排序(處理小數組),當剩餘字符串長度小於某個設定的值時,切換到直接插入排序以保證算法性能。實現charAt( String s, int d) 方法實現獲取目標字符串的指定位置的字符。每一層遞歸用鍵索引記數法切分子數組,而後遞歸每個子數組實現排序。
public class MSD { private static int R = 256; //字符串中最多可能出現的字符的數量 private static final int M = 15; //當子字符串長度小於M時,用直接插入排序 private static String[] aux; //輔助數組 //實現本身的chatAt()方法 private static int charAt(String s, int d) { if(d<s.length())return s.charAt(d); else return -1; } public static void sort(String[] a) { int N = a.length; aux = new String[N]; sort(a,0,N-1,0); } private static void sort(String[] a,int lo, int hi, int d) { if(hi<=lo+M) { //切換爲直接插入排序 Insertion.sort(a,lo,hi,d); return; } int[] count = new int[R+2]; //鍵索引記數法第一步 for(int i=lo; i<=hi;i++) count[charAt(a[i],d)+2]++; //鍵索引記數法第二步 for(int r=0;r<R+1;r++) count[r+1]+=count[r]; //鍵索引記數法第三步 for(int i=lo;i<=hi;i++) aux[count[a[i].charAt(d)+1]++] = a[i]; //鍵索引記數法第四步 for(int i=lo;i<=hi;i++) a[i]=aux[i-lo]; //遞歸以每一個字符爲鍵進行排序 for(int r=0;r<R;r++) sort(a,lo+count[r],lo+count[r+1]-1,d+1); } }
時間複雜度:O(W(N+R))
上面的算法很是簡潔,但高位優先算法雖然簡單但可能很危險:若是使用不當,它可能消耗使人沒法忍受的時間和空間。咱們先來討論任何排序算法都要回答的三個問題:
一、小型子數組
高位優先算法可以快速地將所須要排序的數組切分紅較小的數組。但問題是咱們須要處理大量微型數組,並且處理必須快速。小型子數組對高位優先的字符串排序算法的性能相當重要(快速排序和歸併排序也是這種狀況)。這裏能夠採用在合適時候切換爲直接插入排序來改善。
二、等值鍵
第二是對於含有大量等值鍵的子數組排序會變慢。若是相同的子字符串出現過多,切換排序方法條件將不會出現,那麼遞歸方法就會檢查全部相同鍵中的每個字符。另外,鍵索引記數法沒法有效判斷字符串中的字符是否所有相同:它不只須要檢查每一個字符和移動每一個字符,還須要初始化全部頻率統計並將它們轉化爲索引等。
三、額外空間
高位優先算法使用了兩個輔助數組。aux[]的大小爲N能夠在sort()方法外建立,若是犧牲穩定性,則能夠去掉aux[]數組。但count[]所須要的空間纔是最須要關注的(由於它沒法在sort()外建立,每次循環都要從新計算count[]值)。
該算法思路與高爲優先的字符串排序算法幾乎相同,只是對高位優先的字符串排序算法作了小小的改進。
算法思想:根據鍵的首字符進行三向切分,而後遞歸地選取下一位字符將三個子數組進行排序。
算法實現:三向字符串快速排序實現並不困難,只需對三向快排代碼作些修改便可:
/** *a:要排序的字符串數組 *lo, hi:排序範圍 *d:按照哪一位的字符排序 */ private static void sort(String[] a, int lo, int hi, int d) { //數組長度小於閾值,切換到直接插入排序 if (hi <= lo + CUTOFF) { insertion(a, lo, hi, d); return; } int lt = lo, gt = hi; int v = charAt(a[lo], d); int i = lo + 1; while (i <= gt) { //從lo下一位開始日後遍歷一遍數組 int t = charAt(a[i], d); if (t < v) exch(a, lt++, i++); //小於v的所有放到左邊 else if (t > v) exch(a, i, gt--); //大於v的所有放到右邊 else i++; } sort(a, lo, lt-1, d); //左側遞歸進行本位排序 if (v >= 0) sort(a, lt, gt, d+1); //中間進行下一位排序 sort(a, gt+1, hi, d); //右側遞歸進行本位排序 }
三向字符串快排的特色: