排序是最最最基础了,但是老是忘记,好记性不如烂笔头!

序、算法概述

  1. 算法分类
    十种常见排序算法可以分为两大类:
    • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
    • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
  2. 算法复杂度
    排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性
    冒泡排序 O(n²) O(n²) O(n) O(1) 稳定
    快速排序 O(n㏒n) O(n²) O(n㏒n) O(n㏒n) 不稳定
    简单插入排序 O(n²) O(n²) O(n) O(1) 稳定
    希尔排序 O(n^1.3) O(n²) O(n) O(1) 不稳定
    简单选择排序 O(n²) O(n²) O(n²) O(1) 不稳定
    堆排序 O(n㏒n) O(n㏒n) O(n㏒n) O(1) 不稳定
    归并排序 O(n㏒n) O(n㏒n) O(n㏒n) O(n) 稳定
    计数排序 O(n+k) O(n+k) O(n+k) O(n+k) 稳定
    捅排序 O(n+k) O(n²) O(n) O(n+k) 稳定
    基数排序 O(n*k) O(n*k) O(n*k) O(n+k) 稳定
  3. 相关概念
    • 稳定:如果a原本在b前面,而a=b,排序之后 a 仍然在 b 的前面
    • 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面
    • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律
    • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数

一、冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,
如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

  1. 算法描述
    比较相邻的元素,如果第一个比第二个大,就交换它们两个;
    对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
    重复以上步骤,直到排序完成。
  2. 动图演示
  3. 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const bubbleSort = (arr) => {
    let len = arr.length;
    for (let i = 0; i < len - 1; i++) {
    for (let j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j+1]) { // 相邻元素两两对比
    let temp = arr[j+1]; // 元素交换
    arr[j+1] = arr[j];
    arr[j] = temp;
    }
    }
    }
    return arr;
    }

二、快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

  1. 算法描述
    快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

    从数列中挑出一个元素,称为 “基准”(pivot);
    重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
    递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

  2. 动图演示

  3. 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    // 方法一
    const quickSort = (arr) => {
    if (arr.length <= 1) {
    return arr;
    }
    //取基准点
    const midIndex = Math.floor(arr.length / 2);
    //取基准点的值,splice(index,1) 则返回的是含有被删除的元素的数组。
    const valArr = arr.splice(midIndex, 1);
    const midIndexVal = valArr[0];
    const left = []; //存放比基准点小的数组
    const right = []; //存放比基准点大的数组
    //遍历数组,进行判断分配
    for (let i = 0; i < arr.length; i++) {
    if (arr[i] < midIndexVal) {
    left.push(arr[i]); //比基准点小的放在左边数组
    } else {
    right.push(arr[i]); //比基准点大的放在右边数组
    }
    }
    //递归执行以上操作,对左右两个数组进行操作,直到数组长度为 <= 1
    return [...quickSort(left), midIndexVal, ...quickSort(right)]
    };

    // 方法二
    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;
    };

三、插入排序(Insertion Sort)

插入排序是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

  1. 算法描述
    从第一个元素开始,该元素可以认为已经被排序;
    取出下一个元素,在已经排序的元素序列中从后向前扫描;
    如果该元素(已排序)大于新元素,将该元素移到下一位置;
    重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
    将新元素插入到该位置后;
    重复步骤2~5。
  2. 动图演示
  3. 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const insertionSort = (arr) => {
    let len = arr.length;
    let preIndex, current;
    for (let i = 1; i < len; i++) {
    preIndex = i - 1;
    current = arr[i];
    while (preIndex >= 0 && arr[preIndex] > current) {
    arr[preIndex + 1] = arr[preIndex];
    preIndex--;
    }
    arr[preIndex + 1] = current;
    }
    return arr;
    }

四、希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

  1. 算法描述
    先将整个待排序的记录序列分割成为若干子序列。
    分别进行直接插入排序。
    待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序。
  2. 动图演示
  3. 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const shellSort = (arr) => {
    let len = arr.length;
    for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
    // 注意:这里和动图演示的不一样,动图是分组执行,实际操作是多个分组交替执行
    for (let i = gap; i < len; i++) {
    let j = i;
    let current = arr[i];
    while (j - gap >= 0 && current < arr[j - gap]) {
    arr[j] = arr[j - gap];
    j = j - gap;
    }
    arr[j] = current;
    }
    }
    return arr;
    }

五、选择排序(Selection Sort)

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完

  1. 算法描述
    n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

    初始状态:无序区为R[1…n],有序区为空;
    第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
    n-1趟结束,数组有序化了。

  2. 动图演示

  3. 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const selectionSort = (arr) => {
    let len = arr.length;
    let minIndex, temp;
    for (let i = 0; i < len - 1; i++) {
    minIndex = i;
    for (let j = i + 1; j < len; j++) {
    if (arr[j] < arr[minIndex]) { // 寻找最小的数
    minIndex = j; // 将最小数的索引保存
    }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
    }
    return arr;
    }

六、堆排序(Heap Sort)

堆排序是指利用堆这种数据结构所设计的一种排序算法,堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

  1. 算法描述
    将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
    将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
    由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
  2. 动图演示
  3. 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // 堆排序
    const heapSort = (array) => {
    console.time('堆排序耗时');
    // 初始化大顶堆,从第一个非叶子结点开始
    for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
    heapify(array, i, array.length);
    }
    // 排序,每一次 for 循环找出一个当前最大值,数组长度减一
    for (let i = Math.floor(array.length - 1); i > 0; i--) {
    // 根节点与最后一个节点交换
    swap(array, 0, i);
    // 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
    heapify(array, 0, i);
    }
    console.timeEnd('堆排序耗时');
    return array;
    };

    // 交换两个节点
    const swap = (array, i, j) => {
    let temp = array[i];
    array[i] = array[j];
    array[j] = temp;
    };

    // 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
    // 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
    // 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
    // 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
    // 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
    const heapify = (array, i, length) => {
    let temp = array[i]; // 当前父节点
    // j < length 的目的是对结点 i 以下的结点全部做顺序调整
    for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
    temp = array[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
    if (j + 1 < length && array[j] < array[j + 1]) {
    j++; // 找到两个孩子中较大的一个,再与父节点比较
    }
    if (temp < array[j]) {
    swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
    i = j; // 交换后,temp 的下标变为 j
    } else {
    break;
    }
    }
    };

七、归并排序(MergeSort)

归并排序是建立在归并操作上的一种有效的排序算法,采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

  1. 算法描述
    把长度为n的输入序列分成两个长度为n/2的子序列;
    对这两个子序列分别采用归并排序;
    将两个排序好的子序列合并成一个最终的排序序列。

  2. 动图演示

  3. 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    const mergeSort = arr => {
    //采用自上而下的递归方法
    const len = arr.length;
    if (len < 2) {
    return arr;
    }
    // length >> 1 和 Math.floor(len / 2) 等价
    let middle = Math.floor(len / 2),
    left = arr.slice(0, middle),
    right = arr.slice(middle); // 拆分为两个子数组
    return merge(mergeSort(left), mergeSort(right));
    };

    const merge = (left, right) => {
    const result = [];

    while (left.length && right.length) {
    // 注意: 判断的条件是小于或等于,如果只是小于,那么排序将不稳定.
    if (left[0] <= right[0]) {
    result.push(left.shift());
    } else {
    result.push(right.shift());
    }
    }

    while (left.length) result.push(left.shift());

    while (right.length) result.push(right.shift());

    return result;
    };

八、计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

  1. 算法描述
    找出待排序的数组中最大和最小的元素;
    统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
    对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
    反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
  2. 动图演示
  3. 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const countingSort = (arr, maxValue) => {
    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]--;
    }
    }

    return arr;
    }

九、桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

  1. 算法描述
    设置一个定量的数组当作空桶;
    遍历输入数据,并且把数据一个一个放到对应的桶里去;
    对每个不是空的桶进行排序;
    从不是空的桶里把排好序的数据拼接起来。
  2. 动图演示
  3. 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    const bucketSort = (arr, bucketSize) => {
    if (arr.length === 0) {
    return arr;
    }

    let i;
    let minValue = arr[0];
    let maxValue = arr[0];
    for (let i = 1; i < arr.length; i++) {
    if (arr[i] < minValue) {
    minValue = arr[i]; // 输入数据的最小值
    } else if (arr[i] > maxValue) {
    maxValue = arr[i]; // 输入数据的最大值
    }
    }

    // 桶的初始化
    let DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    let bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
    let buckets = new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
    buckets[i] = [];
    }

    // 利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
    buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }

    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
    insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
    for (let j = 0; j < buckets[i].length; j++) {
    arr.push(buckets[i][j]);
    }
    }
    return arr;
    }

十、基数排序(Radix Sort)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

  1. 算法描述
    取得数组中的最大数,并取得位数;
    arr为原始数组,从最低位开始取每个位组成radix数组;
    对radix进行计数排序(利用计数排序适用于小范围数的特点);
  2. 动图演示
  3. 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    * 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 = parseInt((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;
    };