你能手撕10种经典排序算法吗?

1,208 阅读9分钟

Javascript实现十大经典排序算法,并附上动态图演示。

一、10种排序算法简介

1. 冒泡排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
冒泡排序O( n^2 )O( n^2 )O( n^2 )O(1)稳定
  • 思路

    1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
    2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
    3. 针对所有的元素重复以上的步骤,除了最后一个。
    4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
  • 动图演示

  • 实现代码
/**
 * 冒泡排序
 * @param {number[]} arr
 * @return {number[]} 
 */
function bubbleSort(arr) {
    const len = arr.length;
    for (let i = 0; i < len; i++) {
        for (let j = 1; j < len - i; j++) {
            if (arr[j] < arr[j - 1]) {
                let temp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

2. 选择排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
选择排序O( n^2 )O( n^2 )O( n^2 )O(1)稳定
  • 思路

    1. 在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列。
    2. 从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。
    3. 以此类推,直到所有元素均排序完毕。
  • 动图演示

  • 实现代码
/**
 * 选择排序
 * @param {number[]} arr
 * @return {number[]} 
 */
function selectionSort(arr) {
    let i = 0, j = 0;
    for (let gap = arr.length / 2; gap > 0; gap = gap >> 1) {
        // 以此处理每个分组
        for (i = gap; i < arr.length; i++) {
            const value = arr[i];
            // 处理分组里面的
            for (j = i - gap; j >= 0 && arr[j] > value; j -= gap) {
                arr[j + gap] = arr[j];
            }
            arr[j + gap] = value;
        }
    }
    return arr;
}

3. 直接插入排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
插入排序O( n^2 )O( n^2 )O( n )O(1)稳定
  • 思路

    1. 从第一个元素开始,该元素可以认为已经被排序
    2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
    3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
    4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
    5. 将新元素插入到该位置后
    6. 重复步骤2~5
  • 动图演示

img

  • 实现代码
/**
 * 插入排序
 * @param {number[]} arr
 * @return {number[]} 
 */
function insertionSort(arr) {
    let i = 0, j = 0;
    for (i = 1; i < arr.length; i++) {
        const value = arr[i];
        for (j = i - 1; j >= 0 && arr[j] > value; j--) {
            arr[j + 1] = arr[j];
        }
        arr[j + 1] = value;
    }
    return arr;
}

4. 希尔排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
希尔排序O(n log2 n)O( n^2 )O( nlog(n) )O(1)不稳定
  • 简介

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的: 1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率; 2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

  • 思路

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序

  • 图片演示

/**
 * 希尔排序
 * @param {number[]} arr 
 * @return {number[]}
 */
function shellSort(arr) {
    let i = 0, j = 0;
    for (let gap = arr.length / 2; gap > 0; gap = gap >> 1) {
        // 以此处理每个分组
        for (i = gap; i < arr.length; i++) {
            const value = arr[i];
            // 处理分组里面的
            for (j = i - gap; j >= 0 && arr[j] > value; j -= gap) {
                arr[j + gap] = arr[j];
            }
            arr[j + gap] = value;
        }
    }
    return arr;
}

5. 归并排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
归并排序O( nlog(n)O( nlog(n)O( nlog(n)O(n)稳定
  • 思路

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。

步骤如下

1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4. 重复步骤 3 直到某一指针达到序列尾;
5. 将另一序列剩下的所有元素直接复制到合并序列尾。
  • 动图演示

  • 实现代码
/**
 * 归并排序
 * @param {number[]} arr
 * @return {number[]}
 */
function mergeSort(arr) {
    if (arr.length <= 1) { return arr; }
    const center = arr.length >> 1;
    const left = arr.slice(0, center);
    const right = arr.slice(center, arr.length);
    return merge(mergeSort(left), mergeSort(right));
    function merge(left, right) {
        let i = j = 0;
        const mergeArr = [];
        while (i !== left.length && j !== right.length) {
            const value = left[i] < right[j] ? left[i++] : right[j++];
            mergeArr.push(value);
        }
        const restLeft = left.slice(i, left.length);
        const rightLeft = right.slice(j, right.length);
        return mergeArr.concat(restLeft, rightLeft);
    }
}

6. 快速排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
选择排序O( n^2 )O( n^2 )O( n^2 )O(1)稳定
  • 思路 快速排序使用分治策略来把一个序列分为两个子序列,又称为分区交换排序

步骤为:

1. 从序列中挑出一个元素,作为`轴值`2. 把所有比轴值小的元素放在轴值前面,所有比轴值大的元素放在轴值的后面,这个称为分区操作。
3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。 

快速排序的最坏运行情况是 O(n²),平均期望时间是 O(nlogn),且 O(nlogn) 隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序,是一种处理大数据最快的算法之一。

  • 动图演示

  • 实现代码
/**
 * 快速排序 — 阮一峰 - 易懂 - 额外O(n)空间消耗
 * @param {number[]} arr
 * @return {number[]} 
 */
function quickSort(arr) {
    const len = arr.length;
    if (len <= 1) { return arr; }
    const center = arr[len >> 1];
    const left = [], right = [];
    for (let i = 0; i < len; i ++) {
        const value = arr[i];
        if (value < center) {
            left.push(value);
        } else if (value > center) {
            right.push(value);
        }
    }
    return [...quickSort(left), center, ...quickSort(right)];
}

/**
 * 快速排序 - 原地排序 - 无需额外空间
 * @param {number[]} arr
 * @return {number[]} 
 */
function quickSort(arr) {
    return innerQuickSort(arr, 0, arr.length - 1);
    // 带起止索引位置的排序函数
    function innerQuickSort(arr, start, end) {
        if (start < end) {
            let i = start, j = end;
            while(i < j) {
                while(i < j && arr[i] < arr[j]) {
                    j--;
                }
                if (i < j) {
                    swap(arr, i, j);
                    i++;
                }
                while(i < j && arr[i] < arr[j]) {
                    i++;
                }
                if (i < j) {
                    swap(arr, i, j);
                    j--;
                }
            }
            innerQuickSort(arr, start, i - 1);
            innerQuickSort(arr, i + 1, end);
        }
        return arr;
    }
    // 数组两元素置换
    function swap(arr, i, j) {
        const temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

7. 堆排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
堆排序O( nlog(n)O( nlog(n)O( nlog(n)O(1)不稳定

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
大顶堆: 每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆: 每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

  • 思路

    1. 首先将无序数组构造成一个大根堆(父节点比左右子节点大)
    2. 固定一个最大值(放在数组最后一位),将剩余的数重新构造成一个大根堆(每次找出一个最大值),重复这样的过程
  • 动图演示

  • 实现代码
/**
 * 堆排序
 * @param {number[]} arr 
 * @return {number[]}
 */
function heapSort(arr) {
    let len = arr.length;
    buildMaxHeap();

    for (var i = arr.length - 1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        sort(arr, 0);
    }
    return arr;

    // 构建大顶堆
    function buildMaxHeap() {
        for (let i = len >> 1; i >= 0; i--) {
            sort(arr, i);
        }
    }
    // 调整大顶堆,使父节点大于左右子节点
    function sort(arr, index) {
        let left = 2 * index + 1, right = left + 1, min = index;
        if (left < len && arr[left] > arr[min]) {
            min = left;
        }
        if (right < len && arr[right] > arr[min]) {
            min = right;
        }
        if (min !== index) {
            swap(arr, min, index);
            sort(arr, min);
        }
    }
    // 调换数组两元素位置
    function swap(arr, i, j) {
        const temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

8. 计数排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
计数排序O( n+k )O( n+k )O( n+k )O(n + k)稳定
  • 思路

计数排序数组中元素必须为非负整数

  • 动图演示

  • 实现代码
/**
 * 计数排序
 * @param {number[]} arr
 * @return {number[]} 
 */
function countingSort(arr) {
    let countArr = [], index = 0;
    for (let i = 0; i < arr.length; i ++) {
        const count = countArr[arr[i]] || 0;
        countArr[arr[i]] = count + 1;
    }
    for (let i = 0; i < countArr.length; i ++) {
        while(countArr[i] > 0) {
            arr[index++] = i;
            countArr[i]--;
        }
    }
    return arr;
}

9. 桶排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
桶排序O( n+k )O( n^2 )O( n+k )O(n + k)稳定

将输入数据均匀地分配到有限数量的桶中,然后对每个桶再分别排序,对每个桶再使用插入排序算法,最后将每个桶中的数据有序的组合起来。

  • 思路

    1. 初始化装入连续区间元素的n个桶,每个桶用来装一段区间中的元素。
    2. 遍历待排序的数据,将其映射到对应的桶中,保证每个桶中的元素都在同一个区间范围中。
    3. 对每个桶进行排序,最终将所有桶中排好序的元素连起来。
  • 动图演示

先将元素分布在连续区间不同范围的桶中:

然后,元素在每个桶中进行插入排序:

最后,按顺序组合起来就得到了已排序的数组。

  • 实现代码
/**
 * 桶排序
 * @param {number[]} arr 
 * @param {number} size 
 * @return {number[]}
 */
function bucketSort(arr, size = 4) {
    const len = arr.length;
    size = len >= size * 2 ? size : len;
    // 将元素分配到桶中
    const buckets = [], result = [];
    for (let i = 0; i < len; i ++) {
        const value = arr[i];
        const index = parseInt(value / size);
        if (buckets[index]) {
            buckets[index].push(value)
        } else {
            buckets[index] = [value];
        }
    }
    // 对每个桶进行插入排序
    for (let j = 0; j < buckets.length; j ++) {
        const array = insertionSort(buckets[j]);
        result.push(...array);
    }
    return result;
}

10. 基数排序

排序算法时间复杂度-平均时间复杂度-最差时间复杂度-最好空间复杂度稳定性
基数排序O( k * n )O( k * n )O( k * n )O(n + k)稳定

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

  • 思路

按照个位桶排序一次,按照十位桶排序一次,如此执行一直到最大位执行完,则数组有序。

  • 动图演示

  • 实现代码
/**
 * 基数排序
 * @param {number[]} arr 
 * @param {number} maxDigit 最大位数
 * @return {number[]}
 */
function radixSort(arr, maxDigit = 4) {
    const buckets = [];
    let mod = 10, unit = 1;
    for (let i = 0; i < maxDigit; i ++) {
        for (let j = 0; j < arr.length; j ++) {
            const index = parseInt((arr[j] % mod) / unit);
            if (buckets[index]) {
                buckets[index].push(arr[j]);
            } else {
                buckets[index] = [arr[j]];
            }
        }
        let pos = 0;
        for (let n = 0; n < buckets.length; n ++) {
            const bucket = buckets[n];
            if (!bucket) { continue; }
            let value = bucket.shift();
            while(value !== undefined) {
                arr[pos++] = value;
                value = bucket.shift();
            }
        }
        mod *= 10;
        unit *= 10;
    }
    return arr;
}

二、综合对比表

排序算法平均最差最好空间复杂度稳定性是否原地
冒泡排序O( n^2 )O( n^2 )O( n^2 )O(1)稳定原地
选择排序O( n^2 )O( n^2 )O( n^2 )O(1)稳定原地
插入排序O( n^2 )O( n^2 )O( n )O(1)稳定原地
希尔排序O(nlog2n)O( n^2 )O( nlog(n) )O(1)不稳定原地
归并排序O( nlog(n)O( nlog(n)O( nlog(n)O(n)稳定非原地
快速排序O( nlog(n) )O( n^2 )O( nlog(n) )O(1)不稳定原地
堆排序O( nlog(n)O( nlog(n)O( nlog(n)O(1)不稳定原地
计数排序O( n+k )O( n+k )O( n+k )O(n + k)稳定非原地
桶排序O( n+k )O( n^2 )O( n+k )O(n + k)稳定非原地
基数排序O( k * n )O( k * n )O( k * n )O(n + k)稳定非原地

三、对比 & 分类

1. 时间复杂度

平方阶 (O(n^2)) 排序 各类简单排序:

  • 冒泡排序
  • 直接选择
  • 直接插入

线性对数阶 (O(nlog2n)) 排序

  • 快速排序
  • 堆排序
  • 归并排序

O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数

  • 希尔排序

线性阶 (O(n)) 排序

  • 基数排序
  • 桶、箱排序

2. 稳定性

稳定的排序算法:

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 归并排序
  • 稳定
  • 桶排序
  • 基数排序

不稳定的排序算法:

  • 快速排序
  • 希尔排序
  • 堆排序

3. 分类

比较类

  1. 交换类排序:快速排序、冒泡排序
  2. 插入类排序:简单插入排序、希尔排序
  3. 选择类排序:简单选择排序、堆排序
  4. 归并排序:二路归并排序、多路归并排序

非比较类

  1. 计数排序
  2. 基数排序
  3. 桶排序

对数组元素的要求

  1. 计数排序、桶排序: 非负整数
  2. 基数排序:整数

四、排序可视化

  • 点击 排序可视化 可查看多种排序方法的可视化排序过程,帮助理解排序思想。

五、参考