算法爲王。javascript
想學好前端,先練好內功,只有內功深厚者,前端之路纔會走得更遠。html
筆者寫的 JavaScript 數據結構與算法之美 系列用的語言是 JavaScript ,旨在入門數據結構與算法和方便之後複習。前端
之因此把 計數排序、桶排序、基數排序 放在一塊兒比較,是由於它們的平均時間複雜度都爲 O(n)。java
由於這三個排序算法的時間複雜度是線性的,因此咱們把這類排序算法叫做 線性排序(Linear sort)。git
之因此能作到線性的時間複雜度,主要緣由是,這三個算法不是基於比較的排序算法,都不涉及元素之間的比較操做。github
另外,請你們帶着問題來閱讀下文,問題:如何根據年齡給 100 萬用戶排序 ?算法
桶排序是計數排序的升級版,也採用了分治思想
。數組
思想瀏覽器
好比:數據結構
桶排序利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的肯定。
爲了使桶排序更加高效,咱們須要作到這兩點:
桶排序的核心:就在於怎麼把元素平均分配到每一個桶裏,合理的分配將大大提升排序的效率。
實現
// 桶排序
const bucketSort = (array, bucketSize) => {
if (array.length === 0) {
return array;
}
console.time('桶排序耗時');
let i = 0;
let minValue = array[0];
let maxValue = array[0];
for (i = 1; i < array.length; i++) {
if (array[i] < minValue) {
minValue = array[i]; //輸入數據的最小值
} else if (array[i] > maxValue) {
maxValue = array[i]; //輸入數據的最大值
}
}
//桶的初始化
const DEFAULT_BUCKET_SIZE = 5; //設置桶的默認數量爲 5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
const buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}
//利用映射函數將數據分配到各個桶中
for (i = 0; i < array.length; i++) {
buckets[Math.floor((array[i] - minValue) / bucketSize)].push(array[i]);
}
array.length = 0;
for (i = 0; i < buckets.length; i++) {
quickSort(buckets[i]); //對每一個桶進行排序,這裏使用了快速排序
for (var j = 0; j < buckets[i].length; j++) {
array.push(buckets[i][j]);
}
}
console.timeEnd('桶排序耗時');
return array;
};
// 快速排序
const quickSort = (arr, left, right) => {
let len = arr.length,
partitionIndex;
left = typeof left != 'number' ? 0 : left;
right = typeof right != 'number' ? len - 1 : right;
if (left < right) {
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
return arr;
};
const partition = (arr, left, right) => {
//分區操做
let pivot = left, //設定基準值(pivot)
index = pivot + 1;
for (let i = index; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
swap(arr, pivot, index - 1);
return index - 1;
};
const swap = (arr, i, j) => {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
};
複製代碼
測試
const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log('原始array:', array);
const newArr = bucketSort(array);
console.log('newArr:', newArr);
// 原始 array: [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]
// 堆排序耗時: 0.133056640625ms
// newArr: [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]
複製代碼
分析
由於桶排序的空間複雜度,也即內存消耗爲 O(n),因此不是
原地排序算法。
取決於每一個桶的排序方式,好比:快排就不穩定,歸併就穩定。
由於桶內部的排序能夠有多種方法,是會對桶排序的時間複雜度產生很重大的影響。因此,桶排序的時間複雜度能夠是多種狀況的。
總的來講
最佳狀況:當輸入的數據能夠均勻的分配到每個桶中。 最差狀況:當輸入的數據被分配到了同一個桶中。
如下是桶的內部排序
爲快速排序
的狀況:
若是要排序的數據有 n 個,咱們把它們均勻地劃分到 m 個桶內,每一個桶裏就有 k =n / m 個元素。每一個桶內部使用快速排序,時間複雜度爲 O(k * logk)。 m 個桶排序的時間複雜度就是 O(m * k * logk),由於 k = n / m,因此整個桶排序的時間複雜度就是 O(n*log(n/m))。 當桶的個數 m 接近數據個數 n 時,log(n/m) 就是一個很是小的常量,這個時候桶排序的時間複雜度接近 O(n)。
最佳狀況:T(n) = O(n)。當輸入的數據能夠均勻的分配到每個桶中。 最差狀況:T(n) = O(nlogn)。當輸入的數據被分配到了同一個桶中。 平均狀況:T(n) = O(n)。
桶排序最好狀況下使用線性時間 O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,由於其它部分的時間複雜度都爲 O(n)。 很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
適用場景
動畫
思想
關鍵在於理解最後反向填充時的操做。
使用條件
實現
方法一:
const countingSort = array => {
let len = array.length,
result = [],
countArr = [],
min = (max = array[0]);
console.time('計數排序耗時');
for (let i = 0; i < len; i++) {
// 獲取最小,最大 值
min = min <= array[i] ? min : array[i];
max = max >= array[i] ? max : array[i];
countArr[array[i]] = countArr[array[i]] ? countArr[array[i]] + 1 : 1;
}
console.log('countArr :', countArr);
// 從最小值 -> 最大值,將計數逐項相加
for (let j = min; j < max; j++) {
countArr[j + 1] = (countArr[j + 1] || 0) + (countArr[j] || 0);
}
console.log('countArr 2:', countArr);
// countArr 中,下標爲 array 數值,數據爲 array 數值出現次數;反向填充數據進入 result 數據
for (let k = len - 1; k >= 0; k--) {
// result[位置] = array 數據
result[countArr[array[k]] - 1] = array[k];
// 減小 countArr 數組中保存的計數
countArr[array[k]]--;
// console.log("array[k]:", array[k], 'countArr[array[k]] :', countArr[array[k]],)
console.log('result:', result);
}
console.timeEnd('計數排序耗時');
return result;
};
複製代碼
測試
const array = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array: ', array);
const newArr = countingSort(array);
console.log('newArr: ', newArr);
// 原始 array: [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 計數排序耗時: 5.6708984375ms
// newArr: [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]
複製代碼
方法二:
const countingSort2 = (arr, maxValue) => {
console.time('計數排序耗時');
maxValue = maxValue || arr.length;
let bucket = new Array(maxValue + 1),
sortedIndex = 0;
(arrLen = arr.length), (bucketLen = maxValue + 1);
for (let i = 0; i < arrLen; i++) {
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}
for (let j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
console.timeEnd('計數排序耗時');
return arr;
};
複製代碼
測試
const array2 = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array2: ', array2);
const newArr2 = countingSort2(array2, 21);
console.log('newArr2: ', newArr2);
// 原始 array: [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 計數排序耗時: 0.043212890625ms
// newArr: [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]
複製代碼
例子
能夠認爲,計數排序實際上是桶排序的一種特殊狀況。
當要排序的 n 個數據,所處的範圍並不大的時候,好比最大值是 k,咱們就能夠把數據劃分紅 k 個桶。每一個桶內的數據值都是相同的,省掉了桶內排序的時間。
咱們都經歷太高考,高考查分數系統你還記得嗎?咱們查分數的時候,系統會顯示咱們的成績以及所在省的排名。若是你所在的省有 50 萬考生,如何經過成績快速排序得出名次呢?
分析
動畫
思想
基數排序是一種非比較型整數排序算法,其原理是將整數按位數切割成不一樣的數字,而後按每一個位數分別比較。
因爲整數也能夠表達字符串(好比名字或日期)和特定格式的浮點數,因此基數排序也不是隻能使用於整數。
例子
假設咱們有 10 萬個手機號碼,但願將這 10 萬個手機號碼從小到大排序,你有什麼比較快速的排序方法呢 ?
這個問題裏有這樣的規律:假設要比較兩個手機號碼 a,b 的大小,若是在前面幾位中,a 手機號碼已經比 b 手機號碼大了,那後面的幾位就不用看了。因此是基於位
來比較的。
桶排序、計數排序能派上用場嗎 ?手機號碼有 11 位,範圍太大,顯然不適合用這兩種排序算法。針對這個排序問題,有沒有時間複雜度是 O(n) 的算法呢 ? 有,就是基數排序。
使用條件
位
來比較;方案
按照優先從高位或低位來排序有兩種實現方案:
實現
/** * name: 基數排序 * @param array 待排序數組 * @param max 最大位數 */
const radixSort = (array, max) => {
console.time('計數排序耗時');
const buckets = [];
let unit = 10,
base = 1;
for (let i = 0; i < max; i++, base *= 10, unit *= 10) {
for (let j = 0; j < array.length; j++) {
let index = ~~((array[j] % unit) / base); //依次過濾出個位,十位等等數字
if (buckets[index] == null) {
buckets[index] = []; //初始化桶
}
buckets[index].push(array[j]); //往不一樣桶裏添加數據
}
let pos = 0,
value;
for (let j = 0, length = buckets.length; j < length; j++) {
if (buckets[j] != null) {
while ((value = buckets[j].shift()) != null) {
array[pos++] = value; //將不一樣桶裏數據挨個撈出來,爲下一輪高位排序作準備,因爲靠近桶底的元素排名靠前,所以從桶底先撈
}
}
}
}
console.timeEnd('計數排序耗時');
return array;
};
複製代碼
測試
const array = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log('原始array:', array);
const newArr = radixSort(array, 2);
console.log('newArr:', newArr);
// 原始 array: [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
// 堆排序耗時: 0.064208984375ms
// newArr: [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製代碼
分析
第一,基數排序是原地排序算法嗎 ? 由於計數排序的空間複雜度爲 O(n + k),因此不是原地排序算法。
第二,基數排序是穩定的排序算法嗎 ? 基數排序不改變相同元素之間的相對順序,所以它是穩定的排序算法。
第三,基數排序的時間複雜度是多少 ? 最佳狀況:T(n) = O(n * k) 最差狀況:T(n) = O(n * k) 平均狀況:T(n) = O(n * k) k 是待排序列最大值。
動畫
LSD 基數排序動圖演示:
回過頭來看看開篇的思考題:如何根據年齡給 100 萬用戶排序 ?
你可能會說,我用上一節講的歸併、快排就能夠搞定啊!是的,它們也能夠完成功能,可是時間複雜度最低也是 O(nlogn)。
有沒有更快的排序方法呢 ?如下是參考答案。
基數排序 vs 計數排序 vs 桶排序
基數排序有兩種方法:
這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差別:
複雜性對比
名稱 | 平均 | 最好 | 最壞 | 空間 | 穩定性 | 排序方式 |
---|---|---|---|---|---|---|
桶排序 | O(n + k) | O(n + k) | O(n2) | O(n + k) | Yes | Out-place |
計數排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | Yes | Out-place |
基數排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | Yes | Out-place |
n: 數據規模
桶排序的時間複雜度能夠是多種狀況的,取決於桶內的排序。
旨在經過交互式可視化的執行來揭示算法背後的機制。
算法可視化來源 visualgo.net/en 效果以下圖。
JavaScript 數據結構與算法之美 的系列文章。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。
文中全部的代碼及測試事例都已經放到個人 GitHub 上了。
以爲有用 ?喜歡就收藏,順便點個贊吧,你的支持是我最大的鼓勵!
參考文章: