《Algorithms Unlocked》是 《算法導論》的合著者之一 Thomas H. Cormen 寫的一本算法基礎,算是啃CLRS前的開胃菜和輔助教材。如果CLRS的厚度讓人望而生畏,這本200多頁的小讀本剛好合適帶你入門。
書中沒有涉及編程語言,直接用文字描述算法,我用 JavaScript 對書中的算法進行描述。
超越下界
之前的四個排序算法——選擇排序、插入排序、歸併排序、快速排序都是依賴於對排序關鍵字進行的比較。他們的決策依據都是“如果這個元素的排序關鍵字比另一個元素的排序關鍵字小,那麼就進行相應操作,否則,進行其他操作或者什麼也不做。”假如我們還是依賴這一規則,無論是簡單或複雜的算法或者還沒被發現的算法都無法突破這一下界(最壞情況下所需要的最小時間)。所以我們需要更改遊戲規則,不讓算法利用比較來進行排序。
計數排序
假設我們有一個數組,該數組內的元素都是 0~m-1 範圍內的整數。例如 let array = [4, 1, 5, 0, 1, 6, 5, 1, 5, 3]
。如果我們可以知道排序關鍵字爲 5 的元素有三個,並且剛好有 6 個元素的排序關鍵字小於 5,那麼三個 5 應該位於位置6、7、8上。
首先我們要計算出有多少個元素的排序關鍵字等於某個值。比如有 3 個元素的排序關鍵字等於 5。
// m:定義了數組array中元素的取值範圍 0~m-1
function countKeysEqual(array, m) {
// 創建一個空數組,長度爲m,給每個元素賦值0
// 爲什麼要有這一步,萬一哪個值array裏沒有就會變成NaN
let equal = [];
for (let i = 0; i < m; i++) {
equal[i] = 0;
};
for (let j = 0; j < array.length; j++) {
// 把array中的元素作爲equal數組的索引值
// 該索引值在equal中對應的值爲該元素在array中出現的次數
let key = array[j];
equal[key] += 1;
}
return equal;
}
接着我們計算出有多少個元素的排序關鍵字小於該值。比如有 6 個元素的排序關鍵字小於 5.
// equal 爲上個函數返回的數組
function countKeysLess(equal, m) {
let less = [];
less[0] = 0;
for (let i = 1; i < m; i++) {
// less[i] = equal[0] + equal[1] + ... + equal[i - 1]
less[i] = less[i - 1] + equal[i - 1];
}
return less;
}
一旦得到less數組,我們就可以知道每個元素應該放在哪個位置。
// 根據less可以得知元素在數組中的位置
// 重排數組
function rearrange(array, less, m) {
let arrB = [];
for (let i = 0; i < array.length; i++) {
let key = array[i];
// 有幾個小於key的元素排在key前面,則爲key值在arrB中的索引
// 比如數組[0, 1, 1, 2],有3個排序關鍵字小於2,則2的索引爲3
let index = less[key];
arrB[index] = array[i];
// 自增1,相同值的元素排在該值後一位
less[key] += 1;
}
return arrB;
}
把三個函數組合在一起構成計數排序。
// m:定義了數組array中元素的取值範圍 0~m-1
function countSort(array, m) {
let equal = countKeysEqual(array, m);
let less = countKeysLess(equal, m);
let arrB = rearrange(array, less, m);
return arrB;
}
計數排序能夠超越比較排序的下界,因爲它從來不會對排序關鍵字進行比較。反之,它將排序關鍵字作爲數組的索引,能進行這樣的操作是因爲排序關鍵字均是非常小的整數。如果排序關鍵字是帶有分數的實數,或者是字符串,那麼我們就不能使用計數排序了。