线性排序算法分析总结

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

参考

相关文章
相关标签/搜索