線性排序算法分析總結

線性排序(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

  1. 掃描大文件的數據獲得數值範圍,肯定合適的桶的數量(保證 n/m 的數據量能夠一次讀入到內存中)。這裏的桶會對應一個個小文件。
  2. 從頭日後掃描大文件,將數據按照範圍放到小文件中。
  3. 每一個小文件的數據所有讀取到內存中,進行排序。這裏注意使用的排序算法是否爲原地排序,且是否爲穩定排序。具體根據實際狀況進行選擇合適的排序算法。另外,若是小文件裏的數據(通常來講是數據分佈不均勻致使的)仍不能一次讀取到內存中,能夠對小文件繼續進行拆分,獲得第二小文件。
  4. 按順序合併多個小文件爲一個大的文件。

代碼實現

暫無實現。htm

算法分析

1. 桶排序不是原地排序

由於咱們須要建立多個桶,須要額外的內存空間,桶若是是用數組實現,理論上是建立要 m 個 長度爲 n 大小的數組。不過通常來講不會這樣作,咱們會考慮使用鏈表、動態數組、跳錶和紅黑樹等動態數據結構來保存。桶排序的空間複雜度咱們能夠大概說成是 O(n) 。

2. 桶排序是穩定的排序

其實桶排序是否穩定,是取決於桶內部使用的排序算法,若是使用的是一種穩定的排序算法,那這種桶排序算法就是穩定的排序算法。

3. 桶排序的時間複雜度是 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;
}
複製代碼

算法分析

1. 計數排序不是原地排序算法

計數排序使用了額外的兩個數組(數組 c 和 數組 r),他們的長度分別爲 k 和 n,因此空間複雜度是 O(k+n),因此 線性排序不是原地排序算法

2. 計數排序是穩定的排序算法

不過線性排序是 穩定 的排序算法,由於它沒有進行元素的交換。

3. 計數排序的時間複雜度是 O(n)

這個毫無疑問時間複雜度是 O(n),固然也能夠說時間複雜度是 O(n+k),只是前面的被省略的係數發生了變化。

另外,計數排序的侷限性仍是挺大的,首先計數排序只能用在 數據範圍不大的場景 中,由於數據範圍越大(尤爲是 k 遠大於 n 的狀況),須要建立的兩個數組長度也越長。此外由於要用到數組的索引,因此只能對 非負整數 進行排序,固然咱們能夠對不符合狀況的數據進行一些處理(不改變相對大小),轉換爲非負數。好比對於小樹,能夠乘以 10 的倍數,有負整數則能夠每一個數據加相同大小的值,使負整數變成非負整數。

基數排序(Radix sort)

較短的數值前面補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;
}
複製代碼

算法分析

1. 基數排序不是原地排序算法

由於它要配合一種線性排序算法(桶排序或計數排序),這些線性排序算法都須要額外內存空間。

2. 基數排序是穩定的排序算法

其實仍是取決於和基數排序配合使用的線性排序算法的穩定性

3. 基數排序的時間複雜度是 O(k*n)

k 指的是數據最大值的位數。基數排序是基於 進行排序的,每次排序使用線性排序算法進行排序,時間複雜度是 O(n),一共進行 k 次排序,因此時間複雜度是 O(k * n)

參考

相關文章
相關標籤/搜索