鍵索引計數法
我們先介紹一種適合小整數鍵的簡單排序方法,這是我們將要學習的字符串排序的基礎,舉個例子,我們希望將全班學生按組分類。如圖
姓名 |
An |
Br |
Da |
Ga |
Ha |
Ja |
Jh |
Jn |
Ma |
組號 |
2 |
3 |
3 |
4 |
1 |
3 |
4 |
3 |
1 |
姓名 |
Mb |
Mi |
Mo |
Ro |
Sm |
Ta |
Ta |
Tp |
Wh |
組號 |
2 |
2 |
1 |
2 |
4 |
3 |
4 |
4 |
2 |
姓名 |
Wl |
Ws |
|
||||||
組號 |
3 |
4 |
|
我們這里用數組a[]來存儲每個元素,其中每個元素都包含=一個名字和一個組號,a[i].key()返回元素的組號。
排序后的結果
姓名 |
Ha |
Ma |
Mo |
An |
Mb |
Mi |
Ro |
Wh |
Br |
組號 |
1 |
1 |
1 |
2 |
2 |
2 |
2 |
2 |
3 |
姓名 |
Da |
Ja |
Jn |
Ta |
Wl |
Ga |
Jh |
Sm |
Ta |
組號 |
3 |
3 |
3 |
3 |
3 |
4 |
4 |
4 |
4 |
姓名 |
Tp |
Ws |
|
||||||
組號 |
4 |
4 |
|
排序共分為四個步驟:
(一)頻率統計
組號在0-R之間,鍵為組號,用一個int數組(初始全為0)統計每個鍵出現的頻率,如果鍵為r,則count[r]++,但是在實際使用中我們用count[r+1]++,至於為什么將r加1,這是為了計算方便(下一步詳細說明)
我i們先來進行統計:
An在第二組中,count[2+1]加1,count[3]==1繼續掃描數組,Br在第三組,count[4]+1,count[4]==1,繼續掃描,Da在第三組中,count[4]+1,count[4]==2……
掃描一遍數組得到count[0]=0,count[1]=0,count[2]=3,count[3]=5,count[4]=6,count[5]=6,即鍵1,2,3,4出現的次數分別為3,5,6,6次。
將頻率轉化為索引:
用count[]來計算每個鍵在排序結果中的索引位置,例如,第一組有三個人,第二組有5個人,那么第三組的同學在排序結果數組中的位置一定是8。
如圖:下標從0開始
1 |
1 |
1 |
2 |
2 |
2 |
2 |
2 |
3(下標為8) |
…… |
即
鍵1下標開始count[1]=count[1]+count[0]=0
鍵2下標開始count[2]=count[2]+count[1]+count[0]=3.
鍵3下標開始count[3]=count[3]+count[2]+count[1]+count[0]=8
即對於每個鍵r,小於r+1的鍵的頻率之和為小於r的鍵的頻率之和在加上count[r]。
這樣我們就能直觀的看出來,計算某個鍵的起始位置,只需要計算count[這個鍵]+……count[0]即可,count[]數組的根本目的在於計算並存儲索引位置,不在於存儲鍵的頻率。
數據分類
‘在將count[]數組轉化為一張索引表后,我們將所有元素移到一個輔助數組aux[]中進行排序,每個元素在aux[]中的位置是由它的鍵(組號)決定的,在移動后將count[]中對應的元素加1,這個過程只需要遍歷一遍數組即可完成。這種排序是穩定的。
for(int i=0;i<a.length;i++)
{ aux[count[a[i].key()]++]=a[i];
}
其中a[i].key()獲取元素的組號,count[a[i].key()]++來保證下一個元素的索引位置。
步驟如圖:
分類前:
aux[]
Count[1] |
|
|
Count[2] |
|
|
|
|
Count[3] |
|
|
|
|
|
Count[4] |
|
|
|
|
|
分類中:
1 |
Count[1 |
|
2 |
2 |
Count[2] |
|
|
3 |
3 |
3 |
3 |
Count[3] |
|
4 |
4 |
Count[4] |
|
|
|
分類后:
1 |
1 |
1 |
2 |
2 |
2 |
2 |
2 |
3 |
3 |
3 |
3 |
3 |
3 |
4 |
4 |
4 |
4 |
4 |
其中count[]指向3,count[2]指向8……
回寫:
將輔助數組中的元素移動到原數組中
相關代碼:
int N=a.length;
String aux[]=new String[N];
int[] count=new int[R+1];
//計算出現的次數
for(int i=;i<N;i++)
{
count[a[i].key()+1]++;
}
//將頻率轉化為索引
for(int r=0;r<R;r++)
{
count[r+1]+=count[r];
}
//將元素分類
for(int i=;i<N;i++)
{
aux[count[a[i].key()]++]=a[i];
}
//回寫
for(int i=;i<N;i++)
{
a[i]=aux[i];
}
低優先的字符串排序
一般稱為低位優先,對於一個字符串,從右向左掃描,這個方法依賴於我們上面介紹的鍵索引記數法,非常適合排序定長的字符串,比如身份證,車牌號,IP地址等。
代碼實現:

1 public class LSD { 2 3 public static void sort(String[] a,int w) 4 { 5 int N=a.length; 6 int R=256;//使用擴展的ASCII字符集 7 String[] aux=new String[N]; 8 9 for(int d=w-1;d>=0;d--) 10 11 { 12 int[] count=new int[R+1]; 13 for(int i=0;i<N;i++) 14 { 15 16 count[a[i].charAt(d)+1]++; 17 } 18 19 for(int r=0;r<R;r++) 20 { 21 count[r+1]+=count[r]; 22 } 23 24 for(int i=0;i<N;i++) 25 { 26 aux[count[a[i].charAt(d)]++]=a[i]; 27 } 28 29 for(int i=0;i<N;i++) 30 { 31 a[i]=aux[i]; 32 } 33 } 34 } 35 36 public static void main(String[] args) { 37 String[] a= {"564","964","637","159"}; 38 System.out.println(a[1].charAt(2)); 39 sort(a,3); 40 for(String s:a) 41 { 42 System.out.println(s); 43 } 44 } 45 46 }
要將每個元素均為含有w個字符的字符串數組a[]排序,需要進行w次鍵索引計數排序;從右向左,以每個字符為鍵排序一次。
高位優先的字符串排序:
如果要處理的字符串的大小不同,我們應該考慮從左向右遍歷所有字符,例如以a開頭的字符串應該排在以b開頭的字符串前面。首先用鍵索引計數法將所有字符串按首字母排序,然后(遞歸的)將每個首字符對應的子數組排序(忽略首字母,因為每個首字母都是相同的)。和快速排序一樣,高位的字符會將數組切分為能夠獨立排序的子數組來完成排序任務,但是它產生的切分會分為每個首字母得到一個子數組,而不是像快速排序那樣產生固定的兩個或三個切分。
對字符串末尾的約定:
一個合理的做法是將所有字符已經被檢查過的字符串所在額子數組排在所有子數組的前面。這樣就不需要遞歸的將該子數組排序。我們使用charAt()方法接收兩個參數,並將字符串中的字符索引轉化為數組索引,當指定的位置超過了字符串的末尾時該方法返回-1。然后我們將所有返回值加1得到一個非負的int值並用它作為count[]的索引,即0表示字符串末尾,1表示字符串第一個字符。。。。。。所以每個字符可能產生R+1種可能的位置情況。又因為鍵索引計數法本來就需要一個額外的位置,所以count[]=new int[R+1+1];這里的R指的是字符串中使用的字符集所包含的字符個數,如:字符集為ASCII,則R=128;擴展ASCII,R=256,Unicode,R=65536。當然你也可以選擇自定義,如果字符串里只包含英文字母的大小寫,則R=26+26=52;
代碼實現:

1 public class MSD { 2 3 private static int R=256;//擴展ASCII 4 private static final int M=15;//小數組的切換閾值。 5 private static String[] aux; 6 7 public static void sort(String[] a) 8 { 9 int N=a.length; 10 aux=new String[N]; 11 sort(a,0,N-1,0); 12 } 13 private static void sort(String[] a,int begin,int end,int index) { 14 if(end<begin+M) { 15 insertion(a,begin,end,index); 16 return; 17 } 18 19 int count[]=new int[R+2]; 20 for(int i=begin;i<=end;i++) 21 { int c=charAt(a[i],index); 22 count[c+2]++; 23 /* 24 * 對於鍵r我們是存儲在count[r+1]里的,例如字符串are(假設a是字符集里的第一個字符),我們將a的頻率存在count[2] 25 中,在調用charAt()方法時,chartAt("are",0)返回的結果其實是0,因為我們實際上調用的是String類里的 26 方法,數組下標從0開始,所以這里需要加2來保證正確性。 27 */ 28 } 29 30 for(int r=0;r<R+1;r++) 31 { 32 count[r+1]+=count[r]; 33 } 34 35 for(int i=begin;i<end;i++) 36 { 37 aux[count[charAt(a[i],index)+1]++]=a[i]; 38 } 39 40 for(int i=begin;i<=end;i++) 41 { 42 a[i]=aux[i-begin]; 43 } 44 45 for(int r=0;r<R;r++) 46 { 47 sort(a,begin+count[r],begin+count[r+1]-1,index+1); 48 } 49 } 50 //插入排序 51 private static void insertion(String[] a,int begin,int end,int index) 52 { 53 for(int i=begin;i<=end;i++) 54 for(int j=i;j>begin&&less(a[j],a[j-1],index);j++) 55 { 56 exch(a,j,j-1); 57 } 58 } 59 60 private static int charAt(String s,int index) 61 { 62 if(index<s.length()) 63 return s.charAt(index); 64 65 else 66 return -1; 67 } 68 69 70 71 72 73 private static void exch(String[] a, int i, int j) { 74 String temp = a[i]; 75 a[i] = a[j]; 76 a[j] = temp; 77 } 78 79 private static boolean less(String v,String w,int index) 80 { 81 int min=Math.min(v.length(), w.length()); 82 for(int i=index;i<min;i++) 83 { 84 if(v.charAt(i)<w.charAt(i)) return true; 85 if(v.charAt(i)>w.charAt(i)) return false; 86 } 87 return v.length()<w.length();//如果指定位置的字符都相等,則比較長度 88 } 89 public static void main(String[] args) { 90 String[] a= {"159","126","654","354","681"}; 91 sort(a); 92 for(String s:a) 93 { 94 System.out.println(s); 95 } 96 } 97 98 }
算法分析:
小數組(字符串)問題:
當我們需要排序的字符串有數百萬個(ASCII集),但是大部分字符串的長度都在10左右,如果我們不對這些小型數組進行處理,那么每次排序都需要初始化258個count[]里的元素並將它們都轉化為索引,這部分需要的代價很高,因此對小型的字符串進行插入排序是很有必要的。
等值鍵:
對於含有大量等值鍵的子數組排序會非常慢,最壞二點情況就是所有的鍵都相同。在實際問題中,我們可能會遇到大量含有相同前綴的字符串。(解決的方法是可以使用三向字符串快速排序)
額外空間:
為了切分,我們使用了兩個輔助數組,aux[]和count[]。