線性排序(Linear sort),指的是 時間複雜度爲 O(n) 的排序算法。之因此時間複雜度能達到線性,是由於這種排序不是基於比較的,但它的適用場景也有很大的侷限性。html
線性排序有三種:桶排序、計數排序、基數排序。算法
排序算法 | 時間複雜度 | 空間複雜度 | 是否穩定 |
---|---|---|---|
桶排序 | O(n) | O(n) | 穩定 |
計數排序 | O(n) | O(n+k) | 穩定 |
基數排序 | O(k*n) | O(n) | 穩定 |
桶排序(Bucket sort),顧名思義,就是用不少桶來存放元素。這裏的桶實際上是一個區間,一個用來存放在某個數值範圍內的元素。好比一個 0-9 的桶表示存放數值大於等於0,小於等於9的元素。數組
咱們有 n 個元素,m 個 桶,假設數據很均勻地放到每一個桶裏面,每一個桶就是 k = n/m 個元素,每一個桶進行快速排序,時間複雜度是 O(klogk),那 m 個桶的時間複雜度是 O(m * klogk),即 O(nlog(n/m))。當 m 接近於 n 時,n/m 就是一個很小的常量,這樣時間複雜度就是 O(n) 了。bash
根據前面時間複雜度的推導,咱們會發現,能夠進行桶排序的場景要求很高。數據結構
首先,它要求數據能夠較爲均勻地分佈到每一個桶裏。假如某些桶的數據相比其它桶很是少,其實等同於將它被合併放到另外一個桶裏,本質就是少了一個桶,m 就會變小,n/m 就會接近於 n。一種極端狀況下,數據全放到一個桶裏,時間複雜度就退化爲 O(nlogn)。動畫
雖然通常狀況下,能用桶排序的場景比較少,但有一種場景就很適合桶排序,那就是 外部排序。ui
外部排序(External sorting)是指可以處理極大量數據的排序算法。一般外部排序會用到外存。在數據量很大,內存沒法一次所有讀取的狀況下,就須要用到外部排序。除了能夠用 桶排序,咱們還能夠用 歸併排序 來作外部排序。spa
假設咱們有一個很大的文件,裏面保存了不少數據,要對它們進行排序,具體作法是:code
暫無實現。htm
由於咱們須要建立多個桶,須要額外的內存空間,桶若是是用數組實現,理論上是建立要 m 個 長度爲 n 大小的數組。不過通常來講不會這樣作,咱們會考慮使用鏈表、動態數組、跳錶和紅黑樹等動態數據結構來保存。桶排序的空間複雜度咱們能夠大概說成是 O(n) 。
其實桶排序是否穩定,是取決於桶內部使用的排序算法,若是使用的是一種穩定的排序算法,那這種桶排序算法就是穩定的排序算法。
桶排序的時間複雜度是 O(n) 是有前提條件的,那就是桶的數量 m 接近於 n,且數據要儘可能均勻分佈。極端狀況下全部的數據都放到一個桶裏,此時桶排序的時間複雜度就會退化爲 桶內部使用的排序算法 的時間複雜度。
計數排序很差理解,由於腦子要拐幾個彎。
當數據有大量重複時,且都爲正整數時,咱們能夠考慮使用計數排序。
計算排序算是比較特殊的桶排序,由於它的桶只有一種值。假設一組數據的最大值爲 k,那咱們就分爲 k 個桶。
首先咱們遍歷原數組,用數組 c 統計每一個值出現的次數,而後咱們在讓數組 c 的數組元素和前面的元素累加,即 c[1] = c[0] + c[1], c[2] = c[2] + c[1], ...
。這樣獲得了新的數組 c ,此時數組元素 c[i] 表示的就是原數組中小於等於 i 的數據的個數。
而後咱們從後往前遍歷原數組,將元素的數值 v 找到數組 c 中對應索引的值。這個值 c[v] 表明原數組中小於等於 v 的數據個數。又由於咱們是從後往前遍歷數組,因此當前的原數組的元素就是排序後的第 c[v] 個元素。因而咱們就把這個元素放到一個新開的空數組 r 的第 v 個位置。另外咱們不要忘記 c[v]自減一,由於咱們已經取走了一個數,小於等於 v 的數據的個數也要跟着減1。
遍歷完後,r就是咱們要的排序好的數組了。
const countingSort = (a) => {
// 找出數組的最大的數。
let i, max = a[0];
for (let i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
let c = [];
for (i = 0; i <= max; i++) {
c[i] = 0;
}
for (i = 0; i < a.length; i++) {
c[a[i]]++;
}
console.log('數組C(值表示小於等於索引的數量):', c.toString());
// 計數累加
for (i = 1; i < c.length; i++) {
c[i] = c[i - 1] + c[i];
}
// 從後往前掃描 數組a(從後往前能夠保證是 穩定 排序)
let r = [];
for(i = a.length - 1; i >= 0; i--) {
let index = c[a[i]] - 1;
// 爲何要減1??
// 由於 c[] 記錄的是小於等於索引的元素數量,好比小於等於 3 的有 4個,
// 因此 3 必然是這第4個(索引對應3,因此要減1)。
// 你會說小於等於並非等於啊,但咱們如今是正在遍歷原數組,且確實發現有這個 3 了。
r[index] = a[i];
c[a[i]]--;
}
return r;
}
複製代碼
計數排序使用了額外的兩個數組(數組 c 和 數組 r),他們的長度分別爲 k 和 n,因此空間複雜度是 O(k+n),因此 線性排序不是原地排序算法。
不過線性排序是 穩定 的排序算法,由於它沒有進行元素的交換。
這個毫無疑問時間複雜度是 O(n),固然也能夠說時間複雜度是 O(n+k),只是前面的被省略的係數發生了變化。
另外,計數排序的侷限性仍是挺大的,首先計數排序只能用在 數據範圍不大的場景 中,由於數據範圍越大(尤爲是 k 遠大於 n 的狀況),須要建立的兩個數組長度也越長。此外由於要用到數組的索引,因此只能對 非負整數 進行排序,固然咱們能夠對不符合狀況的數據進行一些處理(不改變相對大小),轉換爲非負數。好比對於小樹,能夠乘以 10 的倍數,有負整數則能夠每一個數據加相同大小的值,使負整數變成非負整數。
較短的數值前面補0,從右往左 每一位 上的數進行排序,直到最高位。
基數排序的對比排序是基於 位的,咱們想要基數排序的時間複雜度爲 O(n),那每一位的排序都必須是線性排序,網上比較常見的是配合桶排序的基數排序(我也不知道爲何反正我配合使用的是計數排序)。
const radixSort = (a) => {
// 從低到高排序
// 位數拆分
let i, tmp = [];
const size = 11; // 正數的位數
let radix = 1;
for (let j = 0; j < size; j++) {
for (i = 0; i < a.length; i++) {
let k = Math.floor(a[i] / radix) % 10 // 獲取j位上的值。
tmp[i] = k;
}
a = cSInRadixSort(tmp, a);
radix *= 10;
// console.log(a.toString());
// 計數排序一下。(要專門爲基數排序寫一個計數排序,由於排序的是 a,而不是 tmp)
// return c
}
return a;
}
/**
* // 基數排序專用的 計數排序
* @param {Array} a 提煉出的某位上的值
* @param {Array} o 原數組
*/
const cSInRadixSort = (a, o) => {
// 找出數組的最大的數。
let i, max = a[0];
for (let i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
let c = [];
for (i = 0; i <= max; i++) {
c[i] = 0;
}
for (i = 0; i < a.length; i++) {
c[a[i]]++;
}
// console.log('數組C(值表示小於等於索引的數量):', c.toString());
// 計數累加
for (i = 1; i < c.length; i++) {
c[i] = c[i - 1] + c[i];
}
// 從後往前掃描 數組a(從後往前能夠保證是 穩定 排序)
let r = [];
for(i = a.length - 1; i >= 0; i--) {
let index = c[a[i]] - 1;
r[index] = o[i]; // 這裏 的 a[i] 改爲 o[i]
c[a[i]]--;
}
return r;
}
複製代碼
由於它要配合一種線性排序算法(桶排序或計數排序),這些線性排序算法都須要額外內存空間。
其實仍是取決於和基數排序配合使用的線性排序算法的穩定性
k 指的是數據最大值的位數。基數排序是基於 位 進行排序的,每次排序使用線性排序算法進行排序,時間複雜度是 O(n),一共進行 k 次排序,因此時間複雜度是 O(k * n)