js解析常见排序算法

750 阅读6分钟

冒泡排序

基本思想

相信大家也看过在水中的冒泡现象,从下至上,气泡会越来越大,而所谓冒泡排序,也是建立在这个原理上,即从下至上按照一定的顺序,进行排列。

相邻的元素,依次进行对比,如果第一个元素大于第二个元素,那么交换位置。而经过一轮比较之后,最大的元素就会“冒泡”到队尾,之后对已经排好序的元素不予理会,对未排序的元素继续这个步骤,在第n - 1(n为数组元素个数)轮之后,完成排序。

代码实现

function bubbleSort(nums) {

    for(let i = 0; i < nums.length - 1; i++) {

        for(let j = 0; j < nums.length - 1 - i; j++) {

            if(nums[j] > nums[j + 1] ) {

                let temp = nums[j]

                nums[j] =  nums[j + 1]

                nums[j + 1]= temp

            }

        }

    }

    return nums

}

复杂度

冒泡排序是一种稳定的排序方法,其平均时间复杂度为O(n^2),最好情况的时间复杂度为O(n),最坏情况的时间复杂度为O(n^2),在排序过程当中只需要一个元素的辅助空间,也就是temp,所以空间复杂度为O(1)

例题分析

let nums = [3, 6, 4, 2, 11, 10, 5]

第一轮排序:3 与 6 比较之后,不交换,6 和 4 交换位置,之后 6 再次与 2 比较,仍然需要进行位置交换,然后继续类似的操作,11 就到了末尾,排序之后数组为 【3, 4, 2, 6, 10, 5, 11】

第二轮排序:3 与 4 比较不进行位置交换,这个时候我们从上帝视角发现,10 为最大元素,那么这个时候 10 会一直冒泡到 11 的前一位元素上。结果为 【3, 2, 4, 6, 5, 10, 11】

后续操作类似,直到四轮排序结束后,数组变为有序数组

图例如下:

图片.png

选择排序

基本思想

选择排序算法,顾名思义,选择两个字至关重要,首先元素之间进行对比,使用 min 来记录当前最小元素的下标索引,与首元素交换,也就是排列在数组的起始位置,之后在剩下未排序的数组元素当中又通过这种方式选择出一个最小的元素,在经过 n - 1 轮排序后,数组变为有序数组。

代码实现

function selectionSort(nums) {

    for (let i = 0; i < nums.length - 1; i++) {

        let min = i

        for (let j = i + 1; j < nums.length; j++) {

            if (nums[j] < nums[min])

                min = j    //保存最小值的下标索引

        }

        let temp = nums[i]

        nums[i] = nums[min]

        nums[min] = temp

    }

    return nums

}

复杂度

选择排序是一种不稳定的排序方法,其平均时间复杂度为O(n^2),最好最坏的情况时间复杂度都为O(n^2),在排序过程当中只需要一个元素的辅助空间,所以空间复杂度为O(1)

例题分析

let nums = [7, 4, 5, 9, 8, 2, 1]

第一轮排序: 第一个元素与其余元素进行比较,找出最小值的下标索引,保存第一个元素的下标为 min ,之后分别与各个元素进行比较,此时 1 为最小值,保存min,然后与 元素 7 进行交换,排序完之后为 【1, 4, 5, 9, 8, 2, 7】

第二轮排序: 元素 4 与其余未排序元素进行比较,最后与 2 交换位置,排序之后,数组为【1, 2, 5, 9, 8, 4, 7】

之后依次按照这个方法寻找未排序的最小元素,4轮之后最终变为有序数组

图片.png

插入排序

基本思想

在插入第 i 个记录时候, R0 至 R1 是有序的,那么将 nums[i] 与这些元素进行比较,找到应该插入的位置,在插入的位置之后的元素都需要依次向后移动

默认第一个元素是存在顺序的,那么从第二个元素开始,需要依次进行比较,将该元素从后往前开始比较,如果比当前元素大,那么将该元素后移,直到找到一个比该元素更小的元素,然后将该元素放在当前元素后面。如此反复,即可排序成功

代码实现

    function insertionSort(nums){
    
        for (let i = 1; i < nums.length; i++) {
        
            for (let j = i; j > 0; j--) {
            
                if (nums[j] < nums[j - 1]){  
                
                    let temp = nums[j];
                    
                    nums[j] = nums[j - 1];
                    
                    nums[j - 1] = nums;
                }
            }
        }
        return nums;
    }

这么一瞅,是不是有点像冒泡排序

还有第二种,但是第二层循环我用了var关键字来声明,因为let块级作用域的原因,nums[j + 1] = temp这一行代码会访问不到变量j,所以我用var来声明,但是无伤大雅,这里也可以用while循环来做的,网友们可自行琢磨

function charu(nums) {

    for (let i = 1; i < nums.length ; i++) {

        if (nums[i] < nums[i - 1]) {

            let temp = nums[i] //保存nums[i]

            nums[i] = nums[i - 1]

            for (var j = i - 1; j >= 0 && nums[j] > temp; j--)

                nums[j + 1] = nums[j]        

            nums[j + 1] = temp

        }

    }

    return nums;

}

复杂度

插入排序是一种稳定的排序方法,其平均时间复杂度为O(n^2),最好情况的时间复杂度为O(n),最坏情况的时间复杂度为O(n^2),在排序过程当中只需要一个元素的辅助空间,所以空间复杂度为O(1)

例题分析

let nums = [5, 7, 4, 6, 3, 1, 2, 9, 8]

默认第一个元素 5 是有序的,那么从第二个元素开始,5 会先与 7 比较,此时按兵不动,而后比较 4 会插入 5 的前面,依次类推,就可以得到有序数组

82ccab2a330fd1e84e61557092100923(1).gif

希尔排序

基本思想

希尔排序又被称为“缩小增量排序”,这是对插入排序算法的改进

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

  • 先取一个小于 n 的整数 d1 作为第一个增量,将数组分为 d1 个组, 即所有距离为 d1 倍数序号的记录放在同一个组当中,在各组当中进行插入排序。

  • 然后取 d2 (d2 < d1) 重复上述分组和排序工作

  • 直到所取的增量为 di = 1,所有记录都在同一组然后进行插入序列为止 一般取的都是 d1 = n/2 d2 = 2/d1,依次类推

代码实现

function shellSort( nums) {

    let half = parseInt(nums.length / 2); //设置增量为原数组长度的一半

    for (let gap = half; gap >= 1; gap = parseInt(gap / 2)) {

        for (let i = gap; i < nums.length; i++) {

            for (let j = i - gap; j >= 0; j = j - gap) {

                if (nums[j] > nums[j + gap]) {

                    let temp = nums[j];

                    nums[j] = nums[j + gap];

                    nums[j + gap] = temp;

                }

            }

        }

    }

return nums

}

复杂度

希尔排序也是一种不稳定的排序方法,其平均时间复杂度约为O(n logn ^2),约为O(n ^ 1.3),最好情况的时间复杂度为O(n),最坏情况的时间复杂度为O(n^2),在排序过程当中只需要一个元素的辅助空间,所以空间复杂度为O(1)

例题分析

因为这里数量太少不利于讲解,我换个例子

let nums = [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]

我们就取 d1 = 5(n / 2) 、d2 = 3(d1 / 2) 、d3 = 2 (d2 / 2),d4 = 1 来作为排序的增量,依据情况而定上下取整

第一轮排序:按照 d1 = 5,进行分组,那么 9 和 4 为一组,1 和 28, 2 和 6, 5 和 3, 7 和 5 两两配对,那么此时就需要比较元素之间的大小,各组之间进行插入排序,那么第一轮排序后,数组为【4, 1, 2, 3, 5, 9, 8, 6, 5, 7】

第二轮排序:按照 d2 = 3,进行分组,那么4、3、8、7为一组, 1 、5 、6 以及 2、 9、 5各成一组,插入排序后, 数组为 【3, 1, 2, 4, 5, 5, 7, 6, 9, 8】

第三轮排序:按照d3 = 2, j进行分组,也就是相邻元素不为一组,3, 2, 5, 7, 9为一组,1, 4, 5, 6, 8 为一组,那么两组内部进行插入排序后,数组为【2, 1, 3, 4, 5, 5, 7, 6, 9, 8】

第四轮排序:直接取d4 = 1,全部序列进行插入排序,得到最后的结果 【1, 2, 3, 4, 5, 5, 6, 7, 8, 9】

其实实际当中不用四轮,三轮即可,只要向下取整就好。

图片.png

快速排序

基本思想

通过一趟排序将待排序的元素划分为独立的两个部分,称为前半区和后半区,其中前半区的元素都不大于后半区元素,然后再分别对这两部分进行快速排序,从而使整个序列有序

附设两个指针变量 ij ,分别指向序列第一个元素和最后一个元素,设第一个关键字为 pivot ,从j 的位置向前检索找到第一个小于 pivot 的元素,将该元素向前移动到 **i **指示的位置,从 i 所指的位置向后搜索,找到第一个大于 pivot 的元素将该元素移动到 j 所指的位置,重复过程直到 **i、 j **相遇

代码实现

function Partition(nums,low,high){

    var i = low,j = high,pivot = nums[i];

    while(i < j ){

        //从右往左找

        while(i < j && nums[j] > pivot )

            j--;                      

        if(i < j ){

            swap(i++,j,nums);

        }
        //从左往右找

        while(i < j && nums[i] <= pivot)

            i++;

        if(i < j ){

            swap(i,j--,nums);

        }

    }

    return i;  //返回基准元素位置

}

function swap(a,b,nums){

    console.log(a,b,nums[a],nums[b])

    var temp = nums[a];

    nums[a] = nums[b];

    nums[b] = temp;

}

function QuickSort(nums,low,high){

    var mid;

    if(low < high){

        mid = Partition(nums,low,high); //返回基准元素位置

        QuickSort(nums,low,mid-1);   //左边快速排序

        QuickSort(nums,mid+1,high);  //右边快速排序

    }

}

QuickSort(nums,0,nums.length-1);

复杂度

快速排序是一种不稳定的排序方法,其平均时间复杂度为O(nlog2 n),最好情况的时间复杂度为O(nlog2 n),最坏情况的时间复杂度为O(n^2)空间复杂度为O(nlog2 n)

例题分析

我从网上找了个例子,相信大家更容易看懂,哈哈哈

重点就是对双指针解法的理解程度,需要在 j 指针找到 比 i 指针更小的元素,进行替换

图片.png

归并排序

基本思想

归并其实利用的是一个递归的思想,将长度为 n 的整个数组看成是 n 个长度为 1 的多个数组,对这些数组进行两两归并,得到 n / 2 个长度为2 或者 1 的有序数组,之后再两两归并,重复这个步骤,直到所有的元素都形成长度为 n 的数组。

大致步骤如下:

  • 把 n 个记录看成 n 个长度为 l 的有序子表
  • 进行两两归并使记录关键字有序,得到 n/2 个长度为 2 的有序子表
  • 重复第 2 步直到所有记录归并成一个长度为 n 的有序表为止。

具体操作为不断切割数组,类似于对半切,不断的在中间元素切割,使得各个数组一分为二,最终变为 n 个长度为 1 的数组,之后再两两合并这些元素,形成一个有序链表。

代码实现

function merge(arr1, arr2) {

    var result = [];

    while (arr1.length && arr2.length) {

        if (arr1[0] <= arr2[0]) {

            result.push(arr1.shift());

        } else {

            result.push(arr2.shift());

        }

        //元素有剩余,合并新的数组

        return result.concat(arr1, arr2);

    }

}

function mergeSortTopDown(arr) {

    if (arr.length <= 1) {

        return arr;

    }

//将数组切割为两个部分,后续进行合并

    var middle = Math.floor(arr.length / 2);

    var left = arr.slice(0, middle);

    var right = arr.slice(middle);

    left = mergeSortTopDown(left);

    right = mergeSortTopDown(right);

    return merge(left, right);

}

复杂度

归并排序是一种稳定的排序方法,其平均时间复杂度为O(n^2),最好情况的时间复杂度为O(nlog2 n),最坏情况的时间复杂度为O(nlog2 n),在排序过程当中只需要 n 个元素的辅助空间,所以空间复杂度为O(nlogn)

例题分析

大致步骤如下:

图片.png

堆排序

基本思想

若将序列对应的一维数组看成是一个完全二叉数 ,完全二叉树中所有非终端节点的值均不小于(或者不大于)其左右孩子节点的值,因此在一个堆中,堆顶元素,即完全二叉树的根节点必然为序列中的最大元素或者是最小元素,并且堆中的任一一棵子树也都是堆,若堆顶为最小元素,则称为小根堆落;堆顶为最大元素,则称为大根堆。

对一组待排序记录的元素首先按堆的定义排成一个序列,即建立初始堆,从而可以输出堆顶的最大元素(对于大根堆而言), 然后将剩余的元素再调整成新堆,便得到次大的元素,如此反复,直到全部元素排成有序序列为止 

写代码前,需要知道这些基本知识

  • Parent(i) = floor((i-1)/2),i 的父节点下标

  • Left(i) = 2i + 1,i 的左子节点下标

  • Right(i) = 2(i + 1),i 的右子节点下标

代码实现

function HeapSort(arr){

    //初始化一个堆结构

    for(var i =parseInt(arr.length/2)-1;i>=0;i--){

        HeapAdjust(i,arr,arr.length);

    }

    for(var j=arr.length-1;j>0;j--){

        //只有第一个元素被替换,剩下的满足堆排序的特点,所以从0开始就可以

        swap(arr,0,j);

        HeapAdjust(0,arr,j)

    }

}

//实现堆结构

function HeapAdjust(i,arr,length){

    let temp = arr[i];

    for(var k=i*2+1;k<length;k=k*2+1){

        //找到大的那个节点,然后赋值给父节点

        if(k+1<length && arr[k]<arr[k+1]){

            k++;

        }

        if(arr[k]>temp){

            arr[i] = arr[k];

            i = k;

        }else{

            break;

        }

    }

    //再用父节点的元素给补充上

    arr[i] = temp;

}

function swap(arr,i,j){

    let empty = arr[i];

    arr[i] = arr[j];

    arr[j] = empty;
    
}

复杂度

堆排序是一种不稳定的排序方法,其平均时间复杂度为O(nlog2 n),最好情况的时间复杂度为O(nlog2 n),最坏情况的时间复杂度为O(nlog2 n),在排序过程当中只需要一个元素的辅助空间,所以空间复杂度为O(1)

例题分析

这里我也是偷个懒,我感觉说的可能不明白,让读者们看图说话吧!

7ab0409de46e4e39a87840a4785cd7c6(1).gif