排序算法剖析总结(附js完成)

本文对一些排序算法举行了简朴剖析,并给出了 javascript 的代码完成。由于本文包含了大批的排序算法,所以剖析不会异常细致,合适有对排序算法有肯定相识的同砚。

本文内容实在不是许多,就是代码占了许多行。

总览

默许须要排序的数据结构为数组,时刻复杂度为均匀时刻复杂度。

排序算法时刻复杂度空间复杂度是不是稳固
冒泡排序O(n^2)O(1)稳固
插进去排序O(n^2)O(1)稳固
挑选排序O(n^2)O(1)不稳固
兼并排序O(nlogn)O(n)稳固
疾速排序O(nlogn)O(1)不稳固

下面代码完成,排序默许都是 从小到大 排序。

一切代码

我的 js 代码完成都放在 github:https://github.com/F-star/js-…

代码仅供参考。

冒泡排序(Bubble Sort)

假定要举行冒泡排序的数据长度为 n。

冒泡排序会举行屡次的冒泡操纵,每次都邑相邻数据比较,假如前一个数据比后一个数据大,就交流它们的位置(即让大的数据放在背面)。如许每次交流,至少有一个元素会挪动到排序后应该在的位置。反复冒泡 n(或者说 n-1) 次,就完成了排序。

细致来讲,第 i(i 从 0 最先) 趟冒泡会对数组的前 n - i 个元素举行比较和交流操纵,要对照的次数是 size - i - 1

冒泡排序总共要举行 n-1 次冒泡(固然你能够说是 n 次冒泡,不过末了一次冒泡只需一个元素,不必举行比较)。

优化

有时刻,能够只举行了 n 次冒泡,数组就已是有序的了,以至数组原本就是有序的。这时刻我们愿望:当发明一次冒泡后,数组有序,就住手下一次的冒泡,返回当前的数组。

这时刻我们能够在每一趟的冒泡前,声明一个变量 exchangeFlag,将其设置为 true。冒泡历程当中,假如发生了数据交流,就将 exchangeFlag 设置为 false。完毕一趟冒泡后,我们就能够经由历程 exchangeFlag 晓得 数据是不是发生过交流。假如没有发生交流,就申明数组有序,直接返回该数组即可;不然申明还没有排好序,继承下一趟冒泡。

代码完成

const bubbleSort = (a) => {
    // 每次遍历找到最大(小)的数放到最背面的位置。
    // 优化:假如某次冒泡操纵没有数据交流,申明已有序了。

    // 两重轮回。
    if (a.length <= 1) return a;
    // 这里的 i < len 改成 i < len - 1 也是准确的,由于末了第 len - 1次并不会实行。
    for (let i = 0, len = a.length; i < len; i++) {
        let exchangeFlag = false;   // 是不是发生过换
        for (let j = 0; j < len - i - 1; j++) {
            if (a[j] > a[j + 1]) {
                [a[j], a[j + 1]] = [a[j + 1], a[j]];
                exchangeFlag = true;
            }
            
        }
        console.log(a)
        if (exchangeFlag == false) return a;
    }
}

// 测试
let array = [199, 3, 1, 2, 8, 21,4, 100, 8];
console.log (bubbleSort(array));

剖析

1. 冒泡排序的时刻复杂度是 O(n^2)

最好时刻复杂度是 O(n),即第一趟举行 n-1 次比较后,发明原数组是有序的,完毕冒泡。

最坏时刻复杂度是 O(n^2),当原数组刚好是倒序分列时,即须要举行 n 次冒泡,要举行 (n-1) + (n-2) … + 1 次比较后,用等比数列乞降公式乞降后并化简,即可求出最坏时刻复杂度。

均匀时刻复杂度不好剖析,它是 O(n^2)

2. 冒泡排序是 稳固 的排序算法。

这里的“稳固”指的是:排序后,值相称的数据的前后递次坚持稳定。

相邻数据假如相称,不交流位置即可。

3. 冒泡排序是原地排序算法

原地排序指的是空间复杂度是 O(1) 的排序算法。

冒泡排序只做了相邻数据交流,别的有两个暂时变量(交流时的暂时变量、flag),只须要常量级的暂时空间,空间复杂度为 O(1)

插进去排序(Insertion Sort)

插进去排序。实质是从 未排序的地区 内掏出数据,放到 已排序地区 内,这个掏出的数据会和已排序的区间内数据逐一对照,找到准确的位置插进去。

我们直接将数组分为 已排序地区未排序地区。刚最先最先,已排序地区只需一个元素,即数组的第一个元素。插进去的体式格局有两种:夙昔今后查找插进去 和 从后往前查找插进去。这里我挑选 从后往前查找插进去。

代码完成

const insertionSort = a => {
    for (let i = 0, len = a.length; i < len; i++) {
        let curr = a[i];     // 存储当前值,排序的时刻,它对应的索引指向的值能够会在排序时被掩盖
        for (let j = i - 1; j >= 0;j--) {
            if (curr < a[j]) {
                a[j + 1] = a[j];
            } else {
                break;
            }
            // 找到位置(0 或 curr >= a[j]时)
            a[j] = curr;
        }
    } 
    return a;
}

剖析

1. 插进去排序的时刻复杂度是:O(n^2)

当要排序的数据是有序的,我们每次插进去已排序的地区,只须要比较一次,一共比较 n-1 次就完毕了(注重这里是从后往前遍历已排序地区)。所以最好时刻复杂度为 O(n)。

最坏时刻复杂度是 O(n^2),是数据刚好是倒序的状况,每次都要遍历完 已排序地区的一切数据。

2. 插进去排序是稳固排序

遍历已排序地区时,值雷同的时刻,放到末了的位置即可。

3. 插进去排序是原地排序算法

不须要分外空间,是在数组上举行数据交流,所以插进去排序是原地排序算法。

挑选排序(Selection Sort)

挑选排序也有一个 已排序地区 和一个 未排序地区。它和插进去排序差别的处所在于:挑选排序是从 未排序地区 中找出最小的值,放到 已排序地区的末端。

为了削减内存斲丧,我们也是直接在数组上举行数据的交流。

插进去排序比冒泡排序优异的缘由

插进去排序和冒泡排序的时刻复杂度都是 O(n^2),元素交流次数也雷同,但插进去排序更优异。缘由是冒泡排序的交流,须要一个 tmp 的中心变量,来举行两个元素交流,这就变成了 3 个赋值操纵。而插进去排序(从后往前遍历已排序地区),不须要中心遍历,它是直接一些元素后移掩盖,只需1个赋值操纵。

冒泡排序中数据的交流操纵:
if (a[j] > a[j+1]) { // 交流
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}
 
插进去排序中数据的挪动操纵:
if (a[j] > value) {
  a[j+1] = a[j];  // 数据挪动
} else {
  break;
}

另外,插进去排序还能够举行优化,变成 希尔排序。这里不详细说。

代码完成

const selectionSort = a => {
    let tmp;
    for (let i = 0, len = a.length; i < len; i++) {

        let min = a[i],     // 保留最小值,用于比较大小。
            minIndex = i;   // 保留未排序区间中,最小值对应的索引(轻易举行元素交流)
        for (let j = i; j < len; j++) {
            if (a[j] < min) {
                minIndex = j;
                min =a[j]
            }
        }
        tmp = a[minIndex];
        a[minIndex] = a[i];
        a[i] = tmp;
    }
    return a;
}

剖析

1. 挑选排序的时刻复杂度是 O(n^2)

最好时刻复杂度是 O(n^2)。由于每次从未排序地区内找出最小值,都要遍历未排序地区内的一切元素,一共要查找 n-1 次,所以时刻复杂度是 O(n^2)。

最坏时刻复杂度也是 O(n^2),来由同上。

2. 挑选排序是原地排序算法

我们找到为排序地区的最小元素,会交流该元素和 排序地区的下一个位置的元素(即排序地区的第一个元素),然后 i 后移。只做了元素的交流,且只用到了常数级的内存空间(交流两个数据须要的一个暂时遍历),因而挑选排序是原地排序算法。

3. 挑选排序是不稳固的排序算法

不稳固,是由于每次都要找最小值和前面的元素举行交流,如许会损坏稳固性。举个反例来证实:3 3 2, 第一次交流后,为 2 3 3,此时两个 3 的相对递次就改变了。

固然你能够分外的建立一个大小为数组长度的空数组,来作为 已排序地区。如许做就不须要交流元素,能够做到排序稳固,但如许做耗费了分外的内存,变成了非原地排序算法。

兼并排序

兼并排序用到了 分治头脑。分治头脑的中心是:将一个大题目分解成多个小的题目,处置惩罚后兼并为原题目。分治通经常运用递返来完成。分治和递归的区分是,分治是一种处置惩罚题目的处置惩罚头脑,递归是一种编程技能。

兼并排序,会将数组从中心分红摆布两部份。然后对这两个部份各自继承从中心分红两部份,直到没法再分。然后将离开的两部份举行排序兼并(兼并后数组有序),不停地往上排序兼并,终究兼并成一个有序数组。

申明下 merge 函数。它是将两个有序数组兼并为一个有序数组,做法是建立一个空数组,长度为两个有序数组的大的一个。设置指针 i 和 j 分指向两个数组的第一个元素,取其中小的到场数组,对应的数组的指针后移。反复上面这个历程,直到一个数组为空,就将另一个数组的盈余元素都推入新数组。

别的,merge() 函数能够借助 尖兵 举行优化处置惩罚。详细我没研讨,有空再斟酌完成。

代码完成

兼并的代码完成用到了递归,所以代码不是很好看懂。

const mergeSort = a => {
    mergeSortC(a, 0, a.length - 1)
    return a;
}

const mergeSortC = (a, p, r) => {
    if (p >= r) return
    let q = Math.floor( (p + r) / 2 ); // 如许取中心值,right.length >= left.length
    mergeSortC(a, p, q);
    mergeSortC(a, q+1, r);
    merge(a, p, q, r)  // p->q (q+1)->r 地区的两个数组兼并。
}

/**
 * merge要领(将两个有序数组兼并成一个有序数组)
 */
function merge(a, p, q, r) {
    let i = p,
        j = q+1,
        m = new Array(r - q);    // 保留兼并数据的数组
    
    let k = 0;
    while (i <= q && j <= r) {
        if (a[i] <= a[j]) {
            m[k] = a[i];
            i++;
        } else {
            m[k] = a[j]
            j++;
        }
        k++;
    }

    // 起首找出两个数组中,有盈余的元素的数组。
    // 然后将盈余元素顺次放入数组 m。
    let start = i,
        end = q;
    if (j <= r) {
        start = j;
        end = r;
    }

    while (start <= end) {
        m[k] = a[start];
        start++;
        k++;
    }
    // m的数据拷贝到 a。
    for(let i = p; i <= r; i++) {
        a[i] = m[i-p];
    }
}

机能剖析

兼并排序的时刻复杂度是 O(nlogn)

以下为简朴推导历程,摘自 专栏-「数据结构与算法之美」

题目a分解为子题目 b 和 c,设求解 a、b、c 的时刻为 T(a)、T(b)、Y(c),则有

T(a) = T(b) + T(c) + K

而兼并两个有序子数组的时刻复杂度是 O(n),因而有

T(1) = C;   n=1 时,只须要常量级的实行时刻,所以示意为 C。
T(n) = 2*T(n/2) + n; n>1

化简后,获得 T(n)=Cn+nlog2n。所以兼并排序的时刻复杂度是 O(nlogn)。

兼并排序是稳固的排序

兼并交流元素的状况发生在 兼并 历程,只需让比较摆布两个子数组时发明相称时,取左侧数组的元素,就能够保证有序了。

兼并排序 不是 原地排序

依旧兼并排序异常优异(指时刻复杂度),但,它的空间复杂度是 O(n)。由于举行兼并操纵时,须要请求一个暂时数组,该数组的长度最大不会凌驾 n。

疾速排序

疾速排序,简称 “快排”。快排运用的是分区头脑。

快排会取数组中的一个元素作为 pivot(分区点),将数组分为三部份:

  1. 小于 pivot 的部份
  2. pivot
  3. 大于或即是 pivot 的部份。

我们取摆布双方的子数组,实行和上面所说的操纵,直到区间减少为0,此时全部数组就变成有序的了。

在兼并排序中,我们用到一个 merge() 兼并函数,而在快排中,我们也有一个 partition() 分区要领。该要领的作用是依据供应的区间局限,随机取一个 pivot,将该区间的数组的数据举行交流,终究将小于 pivot 的放左侧,大于 pivot 的放右侧,然后返回此时 pivot 的下标,作为下一次 递归 的参考点。

partition() 分区函数有一种奇妙的完成体式格局,能够完成原地排序。处置惩罚体式格局有点类似 挑选排序。起首我们选一个 pivot,pivot 后的元素全都往前挪动一个单元,然后pivot 放到末端。接着我们将从左往右遍历数组,假如元素小于 pivot,就放入 “已处置惩罚地区”,详细操纵就是类似插进去操纵那种,举行直接地交流;假如没有就不做操纵,继承下一个元素,直到完毕。末了将 pivot 也放 “已处置惩罚区间”。如许就完成了原地排序了。

别的,对 partition 举行恰当的革新,就能够完成 “查找无序数组内第k大元素” 的算法。

代码完成

const quickSort = a => {
    quickSortC(a, 0, a.length - 1)
    return a;
}

/**
 * 递归函数
 * 参数意义同 partition 要领。
 */
function quickSortC(a, q, r) {
    if (q >= r) {
        // 供应的数组长度为1时,完毕迭代。
        return a;
    }
    let p = partition(a, q, r);
    quickSortC(a, q, p - 1);
    quickSortC(a, p + 1, r);
}

/**
 * 随机挑选一个元素作为 pivot,举行原地分区,末了返回其下标
 * 
 * @param {Array} a 要排序的数组
 * @param {number} p 肇端索引
 * @param {number} r 完毕索引
 * @return 基准的索引值,用于后续的递归。
 */
export function partition(a, p, r) {
    // pivot 默许取末了一个,假如获得不是末了一个,就和末了一个交流位置。
    let pivot = a[r],
        tmp,
        i = p;     // 已排序区间的末端索引。
    // 类似挑选排序,把小于 pivot 的元素,放到 已处置惩罚区间
    for (; p < r; p++) {
        if (a[p] < pivot) {
            // 将 a[i] 放到 已处置惩罚区间。
            tmp = a[p];
            a[p] = a[i];
            a[i] = tmp;    // 这里能够简写为 [x, y] = [y, x]
            i++;
        }
    }

    // 将 pivot(即a[r])也放进 已处置惩罚区间
    tmp = a[i];
    a[i] = a[r];
    a[r] = tmp;   
    return i;   
}

疾速排序和兼并排序都用到了分治头脑,递推公式和递归代码很很类似。它们的区分在于:兼并排序是 由下而上 的,排序的历程发生在子数组兼并历程。而疾速排序是 由上而下 的,分区的时刻,数组就最先趋向于有序,直到末了区间长度为1,数组就变得有序。

机能剖析

1. 疾速排序的时刻复杂度是 O(nlogn)

快排的时刻复杂度递推求解公式跟兼并是雷同的。所以,快排的时刻复杂度也是 O(nlogn)。但这个公式建立的条件是每次分区都能正好将区间中分(即最好时刻复杂度)。

固然均匀复杂度也是 O(nlongn),不过不好推导,就不剖析。

极度状况下,数组的数据已有序,且取末了一个元素为 pivot,如许的分区是及其不均等的,共须要做约莫 n 次的分区操纵,才完成快排。每次分区均匀要扫描约 n/2 个元素。所以,快排的最坏时刻复杂度是 O(n^2)

2. 疾速排序是不稳固的排序

疾速排序的分区历程,触及到了交流操纵,该交流操纵类似 挑选排序,是不稳固的排序。

3. 疾速排序是原地排序

为了完成原地排序,我们前面临 parition 分区函数举行了奇妙的处置惩罚。

末端

也许就是如许,做了简朴的总结。假如文章有毛病的处所,请给我留言。

另有一些排序盘算下次再更新,能够会新开一篇文章,也能够直接修正这篇文章。

参考

数据结构与算法之美

    原文作者:Fstar
    原文地址: https://segmentfault.com/a/1190000019012209
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞