JavaScript 数据结构与算法之美 - 十大经典排序算法汇总

1. 前言
算法为王。

想学好前端,先练好内功,内功不行,就算招式练的再花哨,终究成不了高手;只有内功深厚者,前端之路才会走得更远。

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

文中包含了 十大经典排序算法 的思想、代码实现、一些例子、复杂度分析、动画、还有算法可视化工具。

这应该是目前最全的 JavaScript 十大经典排序算法 的讲解了吧。

2. 如何分析一个排序算法
复杂度分析是整个算法学习的精髓。

时间和空间复杂度的详解,请看 JavaScript 数据结构与算法之美 - 时间和空间复杂度。

学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。

分析一个排序算法,要从 执行效率、内存消耗、稳定性 三方面入手。

2.1 执行效率
1. 最好情况、最坏情况、平均情况时间复杂度

我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。

2. 时间复杂度的系数、常数 、低阶

我们知道,时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。

但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。

3. 比较次数和交换(或移动)次数

这一节和下一节讲的都是基于比较的排序算法。基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。

所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

2.2 内存消耗
也就是看空间复杂度。

还需要知道如下术语:

2.3 稳定性


3. 十大经典排序算法
3.1 冒泡排序(Bubble Sort)

冒泡
思想

特点

实现

// 冒泡排序(未优化)const bubbleSort = arr => {    console.time('改进前冒泡排序耗时');    const length = arr.length;    if (length <= 1) return;    // i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。    for (let i = 0; i < length - 1; i++) {        // j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。        for (let j = 0; j < length - i - 1; j++) {            if (arr[j] > arr[j + 1]) {                const temp = arr[j];                arr[j] = arr[j + 1];                arr[j + 1] = temp;            }        }    }    console.log('改进前 arr :', arr);    console.timeEnd('改进前冒泡排序耗时');};
    console.time('改进前冒泡排序耗时');
    const length = arr.length;
    if (length <= 1) return;
    // i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。
    for (let i = 0; i < length - 1; i++) {
        // j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。
        for (let j = 0; j < length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                const temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    console.log('改进前 arr :', arr);
    console.timeEnd('改进前冒泡排序耗时');
};
优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。

// 冒泡排序(已优化)const bubbleSort2 = arr => {    console.time('改进后冒泡排序耗时');    const length = arr.length;    if (length <= 1) return;    // i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。    for (let i = 0; i < length - 1; i++) {        let hasChange = false; // 提前退出冒泡循环的标志位        // j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。        for (let j = 0; j < length - i - 1; j++) {            if (arr[j] > arr[j + 1]) {                const temp = arr[j];                arr[j] = arr[j + 1];                arr[j + 1] = temp;                hasChange = true; // 表示有数据交换            }        }        if (!hasChange) break; // 如果 false 说明所有元素已经到位,没有数据交换,提前退出    }    console.log('改进后 arr :', arr);    console.timeEnd('改进后冒泡排序耗时');};
    console.time('改进后冒泡排序耗时');
    const length = arr.length;
    if (length <= 1) return;
    // i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。
    for (let i = 0; i < length - 1; i++) {
        let hasChange = false; // 提前退出冒泡循环的标志位
        // j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。
        for (let j = 0; j < length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                const temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                hasChange = true; // 表示有数据交换
            }
        }

        if (!hasChange) break; // 如果 false 说明所有元素已经到位,没有数据交换,提前退出
    }
    console.log('改进后 arr :', arr);
    console.timeEnd('改进后冒泡排序耗时');
};
测试

// 测试const arr = [7, 8, 4, 5, 6, 3, 2, 1];bubbleSort(arr);// 改进前 arr : [1, 2, 3, 4, 5, 6, 7, 8]// 改进前冒泡排序耗时: 0.43798828125msconst arr2 = [7, 8, 4, 5, 6, 3, 2, 1];bubbleSort2(arr2);// 改进后 arr : [1, 2, 3, 4, 5, 6, 7, 8]// 改进后冒泡排序耗时: 0.318115234375ms
bubbleSort(arr);
// 改进前 arr : [1, 2, 3, 4, 5, 6, 7, 8]
// 改进前冒泡排序耗时: 0.43798828125ms

const arr2 = [7, 8, 4, 5, 6, 3, 2, 1];
bubbleSort2(arr2);
// 改进后 arr : [1, 2, 3, 4, 5, 6, 7, 8]
// 改进后冒泡排序耗时: 0.318115234375ms
分析

动画


冒泡排序动画
3.2 插入排序(Insertion Sort)

插入排序又为分为 直接插入排序 和优化后的 拆半插入排序 与 希尔排序,我们通常说的插入排序是指直接插入排序。

一、直接插入

思想

一般人打扑克牌,整理牌的时候,都是按牌的大小(从小到大或者从大到小)整理牌的,那每摸一张新牌,就扫描自己的牌,把新牌插入到相应的位置。

插入排序的工作原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

步骤

实现

// 插入排序const insertionSort = array => {    const len = array.length;    if (len <= 1) return    let preIndex, current;    for (let i = 1; i < len; i++) {        preIndex = i - 1; //待比较元素的下标        current = array[i]; //当前元素        while (preIndex >= 0 && array[preIndex] > current) {            //前置条件之一: 待比较元素比当前元素大            array[preIndex + 1] = array[preIndex]; //将待比较元素后移一位            preIndex--; //游标前移一位        }        if (preIndex + 1 != i) {            //避免同一个元素赋值给自身            array[preIndex + 1] = current; //将当前元素插入预留空位            console.log('array :', array);        }    }    return array;};
    const len = array.length;
    if (len <= 1) return

    let preIndex, current;
    for (let i = 1; i < len; i++) {
        preIndex = i - 1; //待比较元素的下标
        current = array[i]; //当前元素
        while (preIndex >= 0 && array[preIndex] > current) {
            //前置条件之一: 待比较元素比当前元素大
            array[preIndex + 1] = array[preIndex]; //将待比较元素后移一位
            preIndex--; //游标前移一位
        }
        if (preIndex + 1 != i) {
            //避免同一个元素赋值给自身
            array[preIndex + 1] = current; //将当前元素插入预留空位
            console.log('array :', array);
        }
    }
    return array;
};
测试

// 测试const array = [5, 4, 3, 2, 1];console.log("原始 array :", array);insertionSort(array);// 原始 array:    [5, 4, 3, 2, 1]// array:           [4, 5, 3, 2, 1]// array:           [3, 4, 5, 2, 1]// array:          [2, 3, 4, 5, 1]// array:           [1, 2, 3, 4, 5]
console.log("原始 array :", array);
insertionSort(array);
// 原始 array:    [5, 4, 3, 2, 1]
// array:           [4, 5, 3, 2, 1]
// array:           [3, 4, 5, 2, 1]
// array:          [2, 3, 4, 5, 1]
// array:           [1, 2, 3, 4, 5]
分析

动画


insertion-sort.gif

 


二、拆半插入

插入排序也有一种优化算法,叫做拆半插入。

思想

折半插入排序是直接插入排序的升级版,鉴于插入排序第一部分为已排好序的数组,我们不必按顺序依次寻找插入点,只需比较它们的中间值与待插入元素的大小即可。

步骤

注:x >> 1 是位运算中的右移运算,表示右移一位,等同于 x 除以 2 再取整,即 x >> 1 == Math.floor(x/2) 。

// 折半插入排序const binaryInsertionSort = array => {    const len = array.length;    if (len <= 1) return;    let current, i, j, low, high, m;    for (i = 1; i < len; i++) {        low = 0;        high = i - 1;        current = array[i];        while (low <= high) {            //步骤 1 & 2 : 折半查找            m = (low + high) >> 1; // 注: x>>1 是位运算中的右移运算, 表示右移一位, 等同于 x 除以 2 再取整, 即 x>>1 == Math.floor(x/2) .            if (array[i] >= array[m]) {                //值相同时, 切换到高半区,保证稳定性                low = m + 1; //插入点在高半区            } else {                high = m - 1; //插入点在低半区            }        }        for (j = i; j > low; j--) {            //步骤 3: 插入位置之后的元素全部后移一位            array[j] = array[j - 1];            console.log('array2 :', JSON.parse(JSON.stringify(array)));        }        array[low] = current; //步骤 4: 插入该元素    }    console.log('array2 :', JSON.parse(JSON.stringify(array)));    return array;};
    const len = array.length;
    if (len <= 1) return;

    let current, i, j, low, high, m;
    for (i = 1; i < len; i++) {
        low = 0;
        high = i - 1;
        current = array[i];

        while (low <= high) {
            //步骤 1 & 2 : 折半查找
            m = (low + high) >> 1; // 注: x>>1 是位运算中的右移运算, 表示右移一位, 等同于 x 除以 2 再取整, 即 x>>1 == Math.floor(x/2) .
            if (array[i] >= array[m]) {
                //值相同时, 切换到高半区,保证稳定性
                low = m + 1; //插入点在高半区
            } else {
                high = m - 1; //插入点在低半区
            }
        }
        for (j = i; j > low; j--) {
            //步骤 3: 插入位置之后的元素全部后移一位
            array[j] = array[j - 1];
            console.log('array2 :', JSON.parse(JSON.stringify(array)));
        }
        array[low] = current; //步骤 4: 插入该元素
    }
    console.log('array2 :', JSON.parse(JSON.stringify(array)));
    return array;
};
测试

const array2 = [5, 4, 3, 2, 1];console.log('原始 array2:', array2);binaryInsertionSort(array2);// 原始 array2:  [5, 4, 3, 2, 1]// array2 :     [5, 5, 3, 2, 1]// array2 :     [4, 5, 5, 2, 1]// array2 :     [4, 4, 5, 2, 1]// array2 :     [3, 4, 5, 5, 1]// array2 :     [3, 4, 4, 5, 1]// array2 :     [3, 3, 4, 5, 1]// array2 :     [2, 3, 4, 5, 5]// array2 :     [2, 3, 4, 4, 5]// array2 :     [2, 3, 3, 4, 5]// array2 :     [2, 2, 3, 4, 5]// array2 :     [1, 2, 3, 4, 5]
binaryInsertionSort(array2);
// 原始 array2:  [5, 4, 3, 2, 1]
// array2 :     [5, 5, 3, 2, 1]
// array2 :     [4, 5, 5, 2, 1]
// array2 :     [4, 4, 5, 2, 1]
// array2 :     [3, 4, 5, 5, 1]
// array2 :     [3, 4, 4, 5, 1]
// array2 :     [3, 3, 4, 5, 1]
// array2 :     [2, 3, 4, 5, 5]
// array2 :     [2, 3, 4, 4, 5]
// array2 :     [2, 3, 3, 4, 5]
// array2 :     [2, 2, 3, 4, 5]
// array2 :     [1, 2, 3, 4, 5]
注意:和直接插入排序类似,折半插入排序每次交换的是相邻的且值为不同的元素,它并不会改变值相同的元素之间的顺序,因此它是稳定的。

三、希尔排序

希尔排序是一个平均时间复杂度为 O(n log n) 的算法,会在下一个章节和 归并排序、快速排序、堆排序 一起讲,本文就不展开了。

3.3 选择排序(Selection Sort)
思路

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

步骤

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

实现

const selectionSort = array => {    const len = array.length;    let minIndex, temp;    for (let i = 0; i < len - 1; i++) {        minIndex = i;        for (let j = i + 1; j < len; j++) {            if (array[j] < array[minIndex]) {                // 寻找最小的数                minIndex = j; // 将最小数的索引保存            }        }        temp = array[i];        array[i] = array[minIndex];        array[minIndex] = temp;        console.log('array: ', array);    }    return array;};
    let minIndex, temp;
    for (let i = 0; i < len - 1; i++) {
        minIndex = i;
        for (let j = i + 1; j < len; j++) {
            if (array[j] < array[minIndex]) {
                // 寻找最小的数
                minIndex = j; // 将最小数的索引保存
            }
        }
        temp = array[i];
        array[i] = array[minIndex];
        array[minIndex] = temp;
        console.log('array: ', array);
    }
    return array;
};
测试

// 测试const array = [5, 4, 3, 2, 1];console.log('原始array:', array);selectionSort(array);// 原始 array:  [5, 4, 3, 2, 1]// array:           [1, 4, 3, 2, 5]// array:           [1, 2, 3, 4, 5]// array:          [1, 2, 3, 4, 5]// array:           [1, 2, 3, 4, 5]
console.log('原始array:', array);
selectionSort(array);
// 原始 array:  [5, 4, 3, 2, 1]
// array:           [1, 4, 3, 2, 5]
// array:           [1, 2, 3, 4, 5]
// array:          [1, 2, 3, 4, 5]
// array:           [1, 2, 3, 4, 5]
分析

动画


selection-sort.gif


3.4 归并排序(Merge Sort)
思想

排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

归并排序采用的是分治思想。

分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。


merge-sort-example.png
注:x >> 1 是位运算中的右移运算,表示右移一位,等同于 x 除以 2 再取整,即 x >> 1 === Math.floor(x / 2) 。

实现

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;};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;
};
测试

// 测试const arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];console.time('归并排序耗时');console.log('arr :', mergeSort(arr));console.timeEnd('归并排序耗时');// arr : [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]// 归并排序耗时: 0.739990234375ms
const arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.time('归并排序耗时');
console.log('arr :', mergeSort(arr));
console.timeEnd('归并排序耗时');
// arr : [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
// 归并排序耗时: 0.739990234375ms
分析

动画


merge-sort.gif


3.5 快速排序 (Quick Sort)
快速排序的特点就是快,而且效率高!它是处理大数据最快的排序算法之一。

思想

特点:快速,常用。

缺点:需要另外声明两个数组,浪费了内存空间资源。

实现

方法一:

const quickSort1 = 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 quickSort1(left).concat(midIndexVal, quickSort1(right));};const array2 = [5, 4, 3, 2, 1];console.log('quickSort1 ', quickSort1(array2));// quickSort1: [1, 2, 3, 4, 5]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 quickSort1(left).concat(midIndexVal, quickSort1(right));
};
const array2 = [5, 4, 3, 2, 1];
console.log('quickSort1 ', quickSort1(array2));
// quickSort1: [1, 2, 3, 4, 5]
方法二:

// 快速排序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;};
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;
};
测试

// 测试const array = [5, 4, 3, 2, 1];console.log('原始array:', array);const newArr = quickSort(array);console.log('newArr:', newArr);// 原始 array:  [5, 4, 3, 2, 1]// newArr:     [1, 4, 3, 2, 5]
const array = [5, 4, 3, 2, 1];
console.log('原始array:', array);
const newArr = quickSort(array);
console.log('newArr:', newArr);
// 原始 array:  [5, 4, 3, 2, 1]
// newArr:     [1, 4, 3, 2, 5]
分析

动画


quick-sort.gif


解答开篇问题

快排和归并用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢 ?


快速排序与归并排序
可以发现:

3.6 希尔排序(Shell Sort)
思想

过程

举个易于理解的例子:[35, 33, 42, 10, 14, 19, 27, 44],我们采取间隔 4。创建一个位于 4 个位置间隔的所有值的虚拟子列表。下面这些值是 { 35, 14 },{ 33, 19 },{ 42, 27 } 和 { 10, 44 }。


栗子
我们比较每个子列表中的值,并在原始数组中交换它们(如果需要)。完成此步骤后,新数组应如下所示。


栗子
然后,我们采用 2 的间隔,这个间隙产生两个子列表:{ 14, 27, 35, 42 }, { 19, 10, 33, 44 }。


栗子
我们比较并交换原始数组中的值(如果需要)。完成此步骤后,数组变成:[14, 10, 27, 19, 35, 33, 42, 44],图如下所示,10 与 19 的位置互换一下。


image.png
最后,我们使用值间隔 1 对数组的其余部分进行排序,Shell sort 使用插入排序对数组进行排序。


栗子
实现

const shellSort = arr => {    let len = arr.length,        temp,        gap = 1;    console.time('希尔排序耗时');    while (gap < len / 3) {        //动态定义间隔序列        gap = gap * 3 + 1;    }    for (gap; gap > 0; gap = Math.floor(gap / 3)) {        for (let i = gap; i < len; i++) {            temp = arr[i];            let j = i - gap;            for (; j >= 0 && arr[j] > temp; j -= gap) {                arr[j + gap] = arr[j];            }            arr[j + gap] = temp;            console.log('arr  :', arr);        }    }    console.timeEnd('希尔排序耗时');    return arr;};arr => {
let len = arr.length,
        temp,
        gap = 1;
console.time('希尔排序耗时');
while (gap < len / 3) {
//动态定义间隔序列
        gap = gap * 3 + 1;
    }
for (gap; gap > 0; gap = Math.floor(gap / 3)) {
for (let i = gap; i < len; i++) {
            temp = arr[i];
let j = i - gap;
for (; j >= 0 && arr[j] > temp; j -= gap) {
                arr[j + gap] = arr[j];
            }
            arr[j + gap] = temp;
console.log('arr  :', arr);
        }
    }
console.timeEnd('希尔排序耗时');
return arr;
};
测试

// 测试const array = [35, 33, 42, 10, 14, 19, 27, 44];console.log('原始array:', array);const newArr = shellSort(array);console.log('newArr:', newArr);// 原始 array:   [35, 33, 42, 10, 14, 19, 27, 44]// arr      :   [14, 33, 42, 10, 35, 19, 27, 44]// arr      :   [14, 19, 42, 10, 35, 33, 27, 44]// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]// 希尔排序耗时: 3.592041015625ms// newArr:     [10, 14, 19, 27, 33, 35, 42, 44]
const array = [35, 33, 42, 10, 14, 19, 27, 44];
console.log('原始array:', array);
const newArr = shellSort(array);
console.log('newArr:', newArr);
// 原始 array:   [35, 33, 42, 10, 14, 19, 27, 44]
// arr      :   [14, 33, 42, 10, 35, 19, 27, 44]
// arr      :   [14, 19, 42, 10, 35, 33, 27, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// 希尔排序耗时: 3.592041015625ms
// newArr:     [10, 14, 19, 27, 33, 35, 42, 44]
分析

动画


shell-sort.gif

 


3.7 堆排序(Heap Sort)
堆的定义

堆其实是一种特殊的树。只要满足这两点,它就是一个堆。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作大顶堆。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆。


区分堆、大顶堆、小顶堆
其中图 1 和 图 2 是大顶堆,图 3 是小顶堆,图 4 不是堆。除此之外,从图中还可以看出来,对于同一组数据,我们可以构建多种不同形态的堆。

思想

将初始待排序关键字序列 (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,则整个排序过程完成。

实现

// 堆排序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;        }    }};
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;
        }
    }
};
测试

const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];console.log('原始array:', array);const newArr = heapSort(array);console.log('newArr:', newArr);// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]// 堆排序耗时: 0.15087890625ms// newArr:     [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log('原始array:', array);
const newArr = heapSort(array);
console.log('newArr:', newArr);
// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]
// 堆排序耗时: 0.15087890625ms
// newArr:     [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]
分析

动画

微信 gif 图的限制,堆的动画请点击原文

heap-sort2.gif

 


3.8 桶排序(Bucket Sort)
桶排序是计数排序的升级版,也采用了分治思想。

思想

比如:


桶排序利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

为了使桶排序更加高效,我们需要做到这两点:

桶排序的核心:就在于怎么把元素平均分配到每个桶里,合理的分配将大大提高排序的效率。

实现

// 桶排序const bucketSort = (array, bucketSize) => {  if (array.length === 0) {    return array;  }  console.time('桶排序耗时');  let i = 0;  let minValue = array[0];  let maxValue = array[0];  for (i = 1; i < array.length; i++) {    if (array[i] < minValue) {      minValue = array[i]; //输入数据的最小值    } else if (array[i] > maxValue) {      maxValue = array[i]; //输入数据的最大值    }  }  //桶的初始化  const DEFAULT_BUCKET_SIZE = 5; //设置桶的默认数量为 5  bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;  const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;  const buckets = new Array(bucketCount);  for (i = 0; i < buckets.length; i++) {    buckets[i] = [];  }  //利用映射函数将数据分配到各个桶中  for (i = 0; i < array.length; i++) {    buckets[Math.floor((array[i] - minValue) / bucketSize)].push(array[i]);  }  array.length = 0;  for (i = 0; i < buckets.length; i++) {    quickSort(buckets[i]); //对每个桶进行排序,这里使用了快速排序    for (var j = 0; j < buckets[i].length; j++) {      array.push(buckets[i][j]);    }  }  console.timeEnd('桶排序耗时');  return array;};// 快速排序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;};
const bucketSort = (array, bucketSize) => {
if (array.length === 0) {
return array;
  }

console.time('桶排序耗时');
let i = 0;
let minValue = array[0];
let maxValue = array[0];
for (i = 1; i < array.length; i++) {
if (array[i] < minValue) {
      minValue = array[i]; //输入数据的最小值
    } else if (array[i] > maxValue) {
      maxValue = array[i]; //输入数据的最大值
    }
  }

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

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

  array.length = 0;
for (i = 0; i < buckets.length; i++) {
    quickSort(buckets[i]); //对每个桶进行排序,这里使用了快速排序
for (var j = 0; j < buckets[i].length; j++) {
      array.push(buckets[i][j]);
    }
  }
console.timeEnd('桶排序耗时');

return array;
};

// 快速排序
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;
};
测试

const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];console.log('原始array:', array);const newArr = bucketSort(array);console.log('newArr:', newArr);// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]// 堆排序耗时:   0.133056640625ms// newArr:       [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log('原始array:', array);
const newArr = bucketSort(array);
console.log('newArr:', newArr);
// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]
// 堆排序耗时:   0.133056640625ms
// newArr:       [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]
分析

桶排序最好情况下使用线性时间 O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 O(n)。

很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

适用场景

动画


bocket-sort.gif

 


3.9 计数排序(Counting Sort)
思想

关键在于理解最后反向填充时的操作。

使用条件

实现

方法一:

const countingSort = array => {    let len = array.length,        result = [],        countArr = [],        min = (max = array[0]);    console.time('计数排序耗时');    for (let i = 0; i < len; i++) {        // 获取最小,最大 值        min = min <= array[i] ? min : array[i];        max = max >= array[i] ? max : array[i];        countArr[array[i]] = countArr[array[i]] ? countArr[array[i]] + 1 : 1;    }    console.log('countArr :', countArr);    // 从最小值 -> 最大值,将计数逐项相加    for (let j = min; j < max; j++) {        countArr[j + 1] = (countArr[j + 1] || 0) + (countArr[j] || 0);    }    console.log('countArr 2:', countArr);    // countArr 中,下标为 array 数值,数据为 array 数值出现次数;反向填充数据进入 result 数据    for (let k = len - 1; k >= 0; k--) {        // result[位置] = array 数据        result[countArr[array[k]] - 1] = array[k];        // 减少 countArr 数组中保存的计数        countArr[array[k]]--;        // console.log("array[k]:", array[k], 'countArr[array[k]] :', countArr[array[k]],)        console.log('result:', result);    }    console.timeEnd('计数排序耗时');    return result;};array => {
let len = array.length,
        result = [],
        countArr = [],
        min = (max = array[0]);
console.time('计数排序耗时');
for (let i = 0; i < len; i++) {
// 获取最小,最大 值
        min = min <= array[i] ? min : array[i];
        max = max >= array[i] ? max : array[i];
        countArr[array[i]] = countArr[array[i]] ? countArr[array[i]] + 1 : 1;
    }
console.log('countArr :', countArr);
// 从最小值 -> 最大值,将计数逐项相加
for (let j = min; j < max; j++) {
        countArr[j + 1] = (countArr[j + 1] || 0) + (countArr[j] || 0);
    }
console.log('countArr 2:', countArr);
// countArr 中,下标为 array 数值,数据为 array 数值出现次数;反向填充数据进入 result 数据
for (let k = len - 1; k >= 0; k--) {
// result[位置] = array 数据
        result[countArr[array[k]] - 1] = array[k];
// 减少 countArr 数组中保存的计数
        countArr[array[k]]--;
// console.log("array[k]:", array[k], 'countArr[array[k]] :', countArr[array[k]],)
console.log('result:', result);
    }
console.timeEnd('计数排序耗时');
return result;
};
测试

const array = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];console.log('原始 array: ', array);const newArr = countingSort(array);console.log('newArr: ', newArr);// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]// 计数排序耗时:   5.6708984375ms// newArr:       [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array: ', array);
const newArr = countingSort(array);
console.log('newArr: ', newArr);
// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 计数排序耗时:   5.6708984375ms
// newArr:       [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]

测试结果
方法二:

const countingSort2 = (arr, maxValue) => {    console.time('计数排序耗时');    maxValue = maxValue || arr.length;    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]--;        }    }    console.timeEnd('计数排序耗时');    return arr;};(arr, maxValue) => {
console.time('计数排序耗时');
    maxValue = maxValue || arr.length;
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]--;
        }
    }
console.timeEnd('计数排序耗时');
return arr;
};
测试

const array2 = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];console.log('原始 array2: ', array2);const newArr2 = countingSort2(array2, 21);console.log('newArr2: ', newArr2);// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]// 计数排序耗时:   0.043212890625ms// newArr:       [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array2: ', array2);
const newArr2 = countingSort2(array2, 21);
console.log('newArr2: ', newArr2);
// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 计数排序耗时:   0.043212890625ms
// newArr:       [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]
例子

可以认为,计数排序其实是桶排序的一种特殊情况。

当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。

我们都经历过高考,高考查分数系统你还记得吗?我们查分数的时候,系统会显示我们的成绩以及所在省的排名。如果你所在的省有 50 万考生,如何通过成绩快速排序得出名次呢?

分析

动画


counting-sort.gif

 


3.10 基数排序(Radix Sort)
思想

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。

例子

假设我们有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序,你有什么比较快速的排序方法呢 ?

这个问题里有这样的规律:假设要比较两个手机号码 a,b 的大小,如果在前面几位中,a 手机号码已经比 b 手机号码大了,那后面的几位就不用看了。所以是基于位来比较的。

桶排序、计数排序能派上用场吗 ?手机号码有 11 位,范围太大,显然不适合用这两种排序算法。针对这个排序问题,有没有时间复杂度是 O(n) 的算法呢 ?有,就是基数排序。

使用条件

方案

按照优先从高位或低位来排序有两种实现方案:

实现

/**    * 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 = ~~((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;};
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 = ~~((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;
};
测试

const array = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];console.log('原始array:', array);const newArr = radixSort(array, 2);console.log('newArr:', newArr);// 原始 array:  [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]// 堆排序耗时:   0.064208984375ms// newArr:       [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log('原始array:', array);
const newArr = radixSort(array, 2);
console.log('newArr:', newArr);
// 原始 array:  [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
// 堆排序耗时:   0.064208984375ms
// newArr:       [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
分析

动画

LSD 基数排序动图演示:


radixSort.gif

 


4. 复杂度对比
十大经典排序算法的 时间复杂度与空间复杂度 比较。

名称    平均    最好    最坏    空间    稳定性    排序方式
冒泡排序    O(n2)    O(n)    O(n2)    O(1)    Yes    In-place
插入排序    O(n2)    O(n)    O(n2)    O(1)    Yes    In-place
选择排序    O(n2)    O(n2)    O(n2)    O(1)    No    In-place
归并排序    O(n log n)    O(n log n)    O(n log n)    O(n)    Yes    Out-place
快速排序    O(n log n)    O(n log n)    O(n2)    O(logn)    No    In-place
希尔排序    O(n log n)    O(n log2 n)    O(n log2 n)    O(1)    No    In-place
堆排序    O(n log n)    O(n log n)    O(n log n)    O(1)    No    In-place
桶排序    O(n + k)    O(n + k)    O(n2)    O(n + k)    Yes    Out-place
计数排序    O(n + k)    O(n + k)    O(n + k)    O(k)    Yes    Out-place
基数排序    O(n * k)    O(n * k)    O(n * k)    O(n + k)    Yes    Out-place
名词解释:

 

5. 算法可视化工具
6. 系列文章
JavaScript 数据结构与算法之美 系列文章,暂时写了如下的 11 篇文章,后续还有想写的内容,再补充。

所写的内容只是数据结构与算法内容的冰山一角,如果你还想学更多的内容,推荐学习王争老师的 数据结构与算法之美。

从时间和空间复杂度、基础数据结构到排序算法,文章的内容有一定的关联性,所以阅读时推荐按顺序来阅读,效果更佳。

标题    链接
1. 时间和空间复杂度    https://github.com/biaochenxuying/blog/issues/29
2. 线性表(数组、链表、栈、队列)    https://github.com/biaochenxuying/blog/issues/34
3. 实现一个前端路由,如何实现浏览器的前进与后退 ?    https://github.com/biaochenxuying/blog/issues/30
4. 栈内存与堆内存 、浅拷贝与深拷贝    https://github.com/biaochenxuying/blog/issues/35
5. 递归    https://github.com/biaochenxuying/blog/issues/36
6. 非线性表(树、堆)    https://github.com/biaochenxuying/blog/issues/37
7. 冒泡排序、选择排序、插入排序    https://github.com/biaochenxuying/blog/issues/39
8. 归并排序、快速排序、希尔排序、堆排序    https://github.com/biaochenxuying/blog/issues/40
9. 计数排序、桶排序、基数排序    https://github.com/biaochenxuying/blog/issues/41
10. 十大经典排序算法汇总    https://github.com/biaochenxuying/blog/issues/42
11. 强烈推荐 GitHub 上值得前端学习的数据结构与算法项目    https://github.com/biaochenxuying/blog/issues/43

 

你可能感兴趣的:(JavaScript 数据结构与算法之美 - 十大经典排序算法汇总)