十大经典排序算法-计数排序算法详解

十大经典排序算法
  • 十大经典排序算法-冒泡排序算法详解
  • 十大经典排序算法-选择排序算法详解
  • 十大经典排序算法-插入排序算法详解
  • 十大经典排序算法-希尔排序算法详解
  • 十大经典排序算法-快速排序算法详解
  • 十大经典排序算法-归并排序算法详解
  • 十大经典排序算法-堆排序算法详解
  • 十大经典排序算法-计数排序算法详解
  • 十大经典排序算法-桶排序算法详解
  • 十大经典排序算法-基数排序算法详解

一、什么是计数排序

1.概念

计数排序(Counting sort)是一种非基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中以达到排序的效果

2.算法原理

给定一组取值范围为0到9的无序序列:1、7、4、9、0、5、2、4、7、3、4,建立一个长度为10的计数数组,值初始化为0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-au7gjQEg-1594191848834)(./计数1.png)]
遍历无序序列,将每个序列元素值对应的计数数组下标的元素加1

如:第一个序列元素为1,则计数数组中下标为1的元素加1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAv76jgZ-1594191848836)(./计数2.png)]
第二个序列元素为7,计数数组中下标为7的元素加1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0iwSn624-1594191848838)(./计数3.png)]
继续遍历序列,当序列遍历完毕,计数数组的最终状态如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nQ2rxszh-1594191848839)(./计数4.png)]
遍历计数数组,输出计数数组下标值,元素的值是多少,就输出几次

输出结果如下:0、1、2、3、4、4、4、5、7、7、9

此时,元素已是有序的了

3.算法实现

// 计数排序
function sort(arr) {
    // 得到数列的最大值
    let max = arr[0];
    for (let item of arr) {
        if (item > max) {
            max = item;
        }
    }

    // 初始化数组
    let countArr = [];
    for (let i = 0; i <= max; i++) {
        countArr[i] = 0;
    }

    // 遍历原始数组,填充计数数组
    for (let item of arr) {
        countArr[item]++;
    }

    // 遍历计数数组,得到排序后结果
    let index = 0;
    let sortArr = [];
    for (let i = 0; i <= max; i++) {
        for (let j = 0; j < countArr[i]; j++) {
            sortArr[index++] = i;
        }
    }

    return sortArr;
}

let arr = [1, 7, 4, 9, 0, 5, 2, 4, 7, 3, 4];
let sortArr = sort(arr);
console.log(sortArr);

二、算法优化

在前面的例子中,我们以序列的最大值来确定计数数组长度,假设有一个序列83、80、88、90、88、86,那么我们创建一个长度为91的计数数组,计数数组的0到79位都浪费了

我们可以创建一个长度为90-80+1=11(最大值-最小值+1)的计数数组,计数数组的偏移量为序列的最小值80
十大经典排序算法-计数排序算法详解_第1张图片
如图:计数数组下标0对应序列80,下标1对应序列81,以此类推

此外,我们前面的例子中,我们在新建的计数数组中记录序列中每个元素的数量,如果序列有相同的元素,则在输出时,无法保证元素原来的排序,是一种不稳定的排序算法,可通过优化,将其改为稳定排序算法

以序列83、80、88、90、88、86为例,首先填充计数数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SMlp12j3-1594191848844)(./计数6.png)]
将计数数组从第二个元素开始,每个元素都加上前面所有元素的和,此时,计数数组的值表示的是元素在序列中的排序
十大经典排序算法-计数排序算法详解_第2张图片
接下来我们创建输出数组,长度与待排序序列一致,从后往前遍历待排序序列

首先,遍历最后一个元素86,我们在计数数组中找到86对应的值为3,则在输出数组的第3位(下标为2)填入86,计数数组86对应的值减1,既当前的86排序是3,下次遇到86则排序是2
十大经典排序算法-计数排序算法详解_第3张图片
接着,遍历下一个元素88,在计数数组中找到88对应的值为5,在输出数组的第5位(下标为4)填入88,计数数组88对应的值减1
十大经典排序算法-计数排序算法详解_第4张图片
然后,遍历下一个元素90,在计数数组中找到90对应的值为6,在输出数组的第6位(下标为5)填入90,计数数组90对应的值减1
十大经典排序算法-计数排序算法详解_第5张图片
继续遍历下一个元素88,在计数数组中找到88对应的值为4,在输出数组的第4位(下标为3)填入88,计数数组88对应的值减1
十大经典排序算法-计数排序算法详解_第6张图片
以此类推,将所有元素遍历完,填入输出数组
十大经典排序算法-计数排序算法详解_第7张图片
原序列第3位、第5位均为88,从上面的步骤我们可以看到,在第二步,第5位的88填入输出数组的第5位,在第四步,第3位的88填入输出数组的第4位,没有改变原序列相同元素的顺序

优化后代码如下:

// 计数排序优化
function sort(arr) {
    // 得到数列的最大值、最小值,并计算计数数组长度
    let max = arr[0];
    let min = arr[0];
    for (let item of arr) {
        if (item > max) {
            max = item;
        }
        if (item < min) {
            min = item;
        }
    }
    let length = max - min + 1;

    // 初始化计数数组
    let countArr = [];
    for (let i = 0; i < length; i++) {
        countArr[i] = 0;
    }

    // 遍历原始数组,填充计数数组
    for (let item of arr) {
        countArr[item - min]++;
    }

    // 计数数组形变,后面的元素等于前面的元素之和
    let sum = 0;
    for (let i = 0; i < length; i++) {
        sum += countArr[i];
        countArr[i] = sum;
    }
    console.log(11111111, countArr);

    // 倒序遍历计数数组,从计数数组中找到正确位置,输入到输出数组
    let sortArr = [];
    for (let i = arr.length - 1; i >= 0; i--) {
        sortArr[countArr[arr[i] - min] - 1] = arr[i];
        countArr[arr[i] - min]--;
        console.log(arr[i], countArr, sortArr);
    }

    return sortArr;
}

let arr = [83, 80, 88, 90, 88, 86];
let sortArr = sort(arr);
console.log(sortArr);

三、计数排序算法特点

1.时间复杂度

计数排序算法遍历了3次原始数组,一次计数数组,所以算法的时间复杂度是O(N+M)

2.空间复杂度

计数排序算法排序过程中新建了一个计数数组和一个输出数组,所以算法的空间复杂度是O(N+M)

3.稳定性

优化后的计数排序算法在排序过程中,相同元素的前后顺序不会发生改变,是一种稳定排序算法

4.局限性

  • 当待排序序列的最大值和最小值差值特别大时,不适合使用计数排序算法
  • 当待排序序列的值不是整数时,不适合使用计数排序算法

你可能感兴趣的:(算法,算法,排序算法,数据结构)