簡介
衆所周知,常見的排序算法例如快速排序,歸併排序等都是基於比較的排序算法。正是因爲它們基於比較的特性,這些算法在時間複雜度方面無法做到比O(n*logn)
更好。關於這些排序算法的細節在本文中不做討論,請參考作者之前的一篇文章搜索和排序算法總結。
早在1887年,基數排序算法就已經由Herman Hollerith提出,用來解決編表機上的數字排序問題。它實際上是最早被提出的排序算法。
基數排序是一種非比較排序算法。這裏所謂的非比較,指的是該算法並非基於比較數組中的每個元素來決定它們的相對位置。以整數排序爲例,基數排序的工作原理大致是分別比較整數數組中每一個整數中每一位的數字,然後基於比較結果不斷調整順序。這樣一來,如果我們從較低的數位(也就是個位)向較高的數位隨着比較不斷調整排排序,最終我們就會得到一個排序好的數組。同樣的算法也可以運用到實數排序或者字符串排序中,因爲這些元素也是由一位一位的數字或字符構成的。
基數排序還有一個非常好的特性:基數排序是一種穩定排序算法(stable sort)。
根據比較數位的順序,基數排序可以分爲從最高位開始排序(Most Significant Digit)和從最低位開始排序(Least Significant Digit)兩種。
最低位起始基數排序法(LSD)
簡而言之,最低位起始基數排序法流程如下:首先按數字(或字符串等其他元素)長短進行排序,較長的靠後,較短的靠前;接着對於長度一樣的元素,再按位比較排序。這樣排序過後我們就能得到正常的從小到大排序的數組。例如對於輸入數字數組[1, 2, 10, 3]
其排序結果爲[1, 2, 3, 10]
。
最高位起始基數排序法(MSD)
最高位起始基數排序法排序的結果是所謂的字典序排序(lexicographical order)。其流程大致是:對於所有輸入元素,從最高位開向最低位(也就是從左向右)不斷按位排序。對於較短元素,我們默認在其後補空白字符(例如數字補0
)。例如對於一個輸入字符串數組["b", "c", "d", "ba"]
,其排序結果爲["b", "ba", "c", "d"]
。對於一個輸入數字數組[1,2,3,10]
其排序結果爲[1, 10, 2, 3]
。
算法
我們以LSD算法爲例,如下所示:
- 找出數組中的最大值
- 設當前步驟所比較的基數位爲第一位。
- 如果當前基數小於數組中最大值的位數長度,則取當前數組中每一元素的當前基數位數字,並基於這一數字,利用計數排序(counting sort)比較並重新排序整個數組。(我們也可以如上文所述首先按照長度排序整個數組。但這一操作並不影響整個算法的多項式時間複雜度,顧在此不做考慮)。
- 基數位前移一位。
- 以上一步中排序過後的數組作爲輸入,重複過程2-3。
上面過程中利用計數排序比較並重新排序整個數組的算法如下:
- 初始化一個數組
count[]
,長度與當前數組所包含數字的進制相同。例如對於整數排序,數組長度應該是10。 - 從前到後計算輸入數組中當前數位數字對應的數量。
- 根據上一步中得到的
count[]
數組計算累積彙總求和。經過這個步驟,則count[i]
代表當前數位爲i
的數字在輸出的排序過的數組中所在的最後一個位置的座標。 -
從後到前,將輸入數組中對應數位數字爲
i
的數字放入輸出數組中--count[i]
的位置。
時間複雜度
O(n*w)
。其中n
爲輸入數組的長度。w
爲輸入的數字中最長數字的長度。
空間複雜度
O(n)
Java實現
下面的代碼實現了LSD基數排序以及MSD基數排序,供讀者朋友們參考。這裏實現的兩個算法都能正確的處理輸入數組中有重複的問題。
import java.util.Arrays;
public class RadixSort {
// the radix, or base of the input
public int radix;
// the exponential to get the current digit
public int exp;
public RadixSort(int radix, int exp) {
this.radix = radix;
this.exp = exp;
}
public RadixSort() {}
public int[] lsdRadixSort(int[] nums) {
int max = Arrays.stream(nums).max().getAsInt();
int[] aux = new int[nums.length];
while (max / exp > 0) {
int[] count = new int[radix]; // counting sort for each radix
// get counts of each digit
for (int i = 0; i < nums.length; i++) {
count[(nums[i] / exp) % radix]++;
}
// cumulative sum for each count
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// counting sort
for (int i = nums.length - 1; i >= 0; i--) {
int idx = --count[(nums[i] / exp) % radix];
aux[idx] = nums[i];
}
// copy back the sorted array
for (int i= 0; i < aux.length; i++) {
nums[i] = aux[i];
}
exp *= radix;
}
return nums;
}
public String[] msdRadixSort(String[] arr) {
int idx = 0;
int maxLen = Integer.MIN_VALUE;
for (String ele : arr) {
maxLen = Math.max(maxLen, ele.length());
}
String[] aux = new String[arr.length];
while (idx < maxLen) {
// English characters
int[] count = new int[26];
for (int i = 0; i < arr.length; i++) {
count[getCharAt(idx, arr[i]) - 'a']++;
}
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
for (int i = arr.length - 1; i >= 0; i--) {
int pos = --count[getCharAt(idx, arr[i]) - 'a'];
aux[pos] = arr[i];
}
for (int i = 0; i < arr.length; i++) {
arr[i] = aux[i];
}
idx++;
}
return aux;
}
private char getCharAt(int idx, String str) {
if (str.length() <= idx) {
return 'a';
}
return str.charAt(idx);
}
}