十大经典排序算法 - JavaScript

135 阅读9分钟
排序算法说明

1. 排序的定义: 对一序列对象根据某个关键字进行排序。

2. 评述算法优劣术语的说明:

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面。
  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度: 运行完一个程序所需内存的大小。

4abde1748817d7f35f2bf8b6a058aa40.png

1.冒泡排序

基本思想: 每次比较两个相邻的元素,如果他们的顺序错误就把他们交换过来。

具体算法描述如下:

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成

f427727489dff5fcb0debdd69b478ecf.gif

代码实现:

function bubble (arr) {
    let n = arr.length;
    var exchange;
    for (let i = 0; i < n - 1; i++) {
        exchange = false;
        for (let j = 0; j < n - i - 1; j++) {
            console.log('9999')
            if (arr[j] > arr[j + 1]) {
                exchange = true;
                // let p = arr[j + 1];
                // arr[j + 1] = arr[j];
                // arr[j] = p
                arr[j + 1] = arr[j + 1] + arr[j];
                arr[j] = arr[j + 1] - arr[j];
                arr[j + 1] = arr[j + 1] - arr[j];
            }
        }
        if (!exchange) {
            break;
        }
    }
    console.log(arr);
    return arr;
}
2.快速排序

基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

具体算法描述如下:

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

1676a474906ee8f6.gif

分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。

6 1 2 7 9 3 4 5 10 8

5 7

6 1 2 (5) 9 3 4 (7) 10 8

6 1 2 5 (4) 3 (9) 7 10 8

(3)1 2 5 4 (6) 9 7 10 8

代码实现:

const quickSort = (array) => {
    const sort = (arr, left = 0, right = arr.length - 1) => {
        if (left >= right) {//如果左边的索引大于等于右边的索引说明整理完毕
            return
        }
        let i = left
         let j = right
        const baseVal = arr[j] // 取无序数组最后一个数为基准值
        while (i < j) {//把所有比基准值小的数放在左边 大的数放在右边
            while (i < j && arr[i] <= baseVal) { //找到一个比基准值大的数交换
                i++
            }
            arr[j] = arr[i] // 将较大的值放在右边 如果没有比基准值大的数就是将自己赋值给自己(i 等于 j)
            while (j > i && arr[j] >= baseVal) { //找到一个比基准值小的数交换
                j--
            }
            arr[i] = arr[j] // 将较小的值放在左边如果没有找到比基准值小的数就是将自己赋值给自己(i 等于 j)
        }
        arr[j] = baseVal // 将基准值放至中央位置完成一次循环(这时候 j 等于 i )
        sort(arr, left, j - 1) // 将左边的无序数组重复上面的操作
        sort(arr, j + 1, right) // 将右边的无序数组重复上面的操作
    }
    const newArr = array.concat() // 为了保证这个函数是纯函数拷贝一次数组
    sort(newArr)
    console.log(newArr)
    return newArr
}
3.插入排序

基本思想: 它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

具体算法描述如下:

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

f0e1e3b7f95c3888ab2791b6abbfae41.gif

// 插入排序
function insertSort (arr) {
    let len = arr.length;
    if (len < 2) {
        return arr;
    }
    for (let i = 1; i < len; i++) {
        let value = arr[i]
        let j = i - 1;
        for (j; j >= 0; j-- ) {
            if (arr[j] > value) {
                arr[j + 1] = arr[j]
            }else {
                break
            }
        }
        arr[j + 1] = value
    }
    console.log(arr)
    return arr;
}
insertSort([6,5,4,3,2,1])
4.选择排序

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

具体算法描述如下:

  • 初始状态:无序区为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趟结束,数组有序化了。

138a44298f3693e3fdd1722235e72f0f.gif

代码实现:

function selectionSort(arr) {
            var len = arr.length;
            var minIndex, temp;
            for (var i = 0; i < len - 1; i++) {
                minIndex = i;
                for (var 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;
        }
        //选择排序优化
function sort(arr) {
    let left = 0;
    let right = arr.length - 1;
    while (left <= right) {
        let minIndex = left;
        let maxIndex = right;
        for (let j = left; j <= right; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j
            }
            if (arr[j] > arr[maxIndex]) {
                maxIndex = j
            }
        }
        let preMin = arr[minIndex];
        arr[minIndex] = arr[left];
        arr[left] = preMin;
        
        if (left === maxIndex) {
            maxIndex = minIndex
        }
        let preMax = arr[maxIndex];
        arr[maxIndex] = arr[right];
        arr[right] = preMax;
        left++;
        right--;
    }
    console.log(arr)
}
5.归并排序

基本思想: 采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

具体算法描述如下:

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

33d105e7e7e9c60221c445f5684ccfb6.gif

代码实现:

function mergeSort(arr) {  //采用自上而下的递归方法
    var len = arr.length;
    if (len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
    var 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;
}
6.希尔排序

基本思想: 是简单插入排序的改进版;它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序,希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。

具体算法描述如下: (看图好理解)

如图(来自于网络 感谢

23140434_61c411727a27255154.png

代码实现:

function shellSort(arr){
    let tmp = 0;
    for(let gap = arr.length / 2;gap > 0;gap =Math.floor( gap/2)){
        for(let i=gap;i<arr.length;i++){
            for(let j = i - gap;j >= 0;j -= gap){
                if(arr[j] > arr[j+gap]){
                    tmp = arr[j];
                    arr[j] = arr[j+gap];
                    arr[j+gap] = tmp;
                }
            }
        }
    }
    return arr;
}
线性排序 (桶排序,计数排序,基数排序)
7.桶排序

基本思想: 桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分 到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数 据按照顺序依次取出,组成的序列就是有序的了。

如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个 元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就 是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。 当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的 时间复杂度接近 O(n)。

就是先分到桶里,每个桶里再分别排序,再依次取出

另外,数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数 据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端 情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。

eaa995f9dcd445fbbd2e2d129d4420d8.png

bc9138435f704df2b90815fe59595aef.png

8.计数排序

基本思想: 计数排序其实是桶排序的一种特殊情况, 当要排序的 n 个数据,所处的范围 并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是 相同的,省掉了桶内排序的时间。

例:

  1. 给一个 originArr [1,3,4,3,6], 设计数数组就是 countArr,
  2. countArr计算得出 为 【empty, 1, empty, 2, 1, empty, 6】,
  3. countArr 的下标(偏移量)对应 originArr的值,countArr 下标的值 对应 originArr 当前 value 的数量,比如 originArr里 3 出现两次, 那么sortArr[3] = 2;

20210425170147529.gif

function countSort (originArr) {
    //countArr 统计 originArr 的每个 value 的个数
    //例 originArr [1,3,4,3,6]
    //countArr 为 【empty, 1, empty, 2, 1, empty, 6】, countArr 的下标(偏移量)对应 originArr
    //的值,sortArr 下标的值 对应 originArr 当前 value 的数量,比如 originArr里 3 出现两次, 那么sortArr[3] = 2;
    let countArr = [];
    let returnSortArr = [];
    for (let i of originArr) {
        countArr[i] = countArr[i] ? countArr[i]++ : countArr[i] = 1
    }
    // 然后根据 countArr 重新生成一个新的排序好的数组
    for (let [key, value] of countArr.entries()) {
        if (!value) continue;
        while (value) {
            returnSortArr.push(key)
            value--
        }
    }
    console.log(returnSortArr)
    return returnSortArr;
}
var origin = [ 9, 3, 10, 6, 4, 100, 1, 9, 8, 7, 2, 2, 5, 10, 3, 32, 5 ]
countSort(origin);
9.基数排序

基本思想: 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

具体算法描述如下:

  • 按优先级低的先排序(用计数或者桶)
  • 依次递增优先级排序
  • 肯定是稳定算法,不稳定 低优先级先排序就没用了