算法---数组排序

一、排序算法

1. 冒泡排序

冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,
最终达到完全有序。

冒泡排序动图
-----------------(来自网络,侵权删)
代码实现:

function mySort(arr) {
    // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
    if (!Array.isArray(arr) || arr.length <= 1) return;

    // 第一趟为 [0, arr.length - 1]
    let lastIndex = arr.length - 1;//记录排序区间
    
    while (lastIndex != 0) {
        let k = lastIndex;  //  for 循环,for排序区间 [0, k)
        /* 优化1:外层while循环 */
        lastIndex = 0;//默认为0 ,除非当前for循环中有值交换
        
        for (let j = 0; j < k; j++) {
            // 将最大的值冒泡
            if (arr[j] > arr[j + 1]) {
                /* 优化2:内层for循环 */
                lastIndex = j;//记录最后一次交换元素的位置
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];//  交换位置
            }
        }
    }
}

冒泡排序有两种优化方式:

一种是外层循环的优化,我们可以记录当前循环中是否发生了交换,如果没有发生交换,则说明该序列已经为有序序列了。
因此我们不需要再执行之后的外层循环,此时可以直接结束。

一种是内层循环的优化,我们可以记录当前循环中最后一次元素交换的位置,该位置以后的序列都是已排好的序列,因此下
一轮循环中无需再去比较。

优化后的冒泡排序,当排序序列为已排序序列时,为最好的时间复杂度为 O(n)。

冒泡排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。

详细资料可以参考:
《图解排序算法(一)》
《常见排序算法 - 鸡尾酒排序 》
《前端笔试&面试爬坑系列---算法》
《前端面试之道》

2. 选择排序

选择排序的基本思想为每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。

在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。其实
我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。我们可以通过设置一个变量 min,每一次比较
仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。

选择排序动图
-----------------(来自网络,侵权删)

代码实现:

function selectSort(arr) {
    // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
    if (!Array.isArray(arr) || arr.length <= 1) return;

    for (let i = 0; i < arr.length - 1; i++) {
        let minIndex = i;   //设置最小值下标索引
        for (let j = i + 1; j < arr.length; j++) {
            // 升序排序
            if (arr[minIndex] > arr[j]) {
                minIndex = j;   //记录最新的最小值下标
            }
        }
        if (minIndex !== i) {//判断最小位置是否改变
            // 交换位置
            [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
        }
    }
}

选择排序不管初始序列是否有序,时间复杂度都为 O(n²)。

选择排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,不是稳定排序。

详细资料可以参考:
《图解排序算法(一)》

1.3 插入排序

直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

插入排序核心--扑克牌思想: 就想着自己在打扑克牌,接起来一张,放哪里无所谓,再接起来一张,比第一张小,放左边,
继续接,可能是中间数,就插在中间....依次

插入排序动图
-----------------(来自网络,侵权删)

代码实现:

function insertSort(arr) {
    // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
    if (!Array.isArray(arr) || arr.length <= 1) return;
    for (let i = 1; i < arr.length; i++) {
        let temp = arr[i];  //1.记录当前未排序区域的第一个值
        let j = i - 1;  //2.记录已排序区域的最好一个值
        for (; j >= 0 && arr[j] > temp; j--) {
            // 3.该循环目的:遍历找出已排序区域里第一个 小于temp 的值的下标 j,所有大于 temp 的均向右移一位
            arr[j + 1] = arr[j];
        }
        // 4.在下标 j 后 插入 temp值
        arr[j + 1] = temp;
    }
}

当排序序列为已排序序列时,为最好的时间复杂度 O(n)。

插入排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。

详细资料可以参考:
《图解排序算法(一)》

1.4 希尔排序

希尔排序的基本思想是把数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元
素越来越多,当增量减至1时,整个数组恰被分成一组,算法便终止。

希尔排序动图
-----------------(来自网络,侵权删)

代码实现:

function hillSort(arr) {
    if (!Array.isArray(arr) || arr.length <= 1) return;
    // 分组,取一半,最少一组,每次折半
    for (let d = Math.floor(arr.length / 2); d >= 1; d = parseInt(d / 2)) {
        // 对每一组进行插入排序
        for (let i = d; i < arr.length; i++) {
            // 找到第一个未排序的值
            let temp = arr[i];
            let j = i - d;
            // 在已排序区域 进行插值
            for (; j >= 0 && temp < arr[j]; j = j - d) {
                arr[j + d] = arr[j];
            }
            arr[j + d] = temp;
        }
    }
}

希尔排序是利用了插入排序对于已排序序列排序效果最好的特点,在一开始序列为无序序列时,将序列分为多个小的分组进行
基数排序,由于排序基数小,每次基数排序的效果较好,然后在逐步增大增量,将分组的大小增大,由于每一次都是基于上一
次排序后的结果,所以每一次都可以看做是一个基本排序的序列,所以能够最大化插入排序的优点。

简单来说就是,由于开始时每组只有很少整数,所以排序很快。之后每组含有的整数越来越多,但是由于这些数也越来越有序,
所以排序速度也很快。

希尔排序的时间复杂度根据选择的增量序列不同而不同,但总的来说时间复杂度是小于 O(n^2) 的。

插入排序是一个稳定排序,但是在希尔排序中,由于相同的元素可能在不同的分组中,所以可能会造成相同元素位置的变化,
所以希尔排序是一个不稳定的排序。

希尔排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n^s) ,空间复杂度为 O(1) ,不是稳定排序。

详细资料可以参考:
《图解排序算法(二)之希尔排序》
《数据结构基础 希尔排序 之 算法复杂度浅析》

1.5 快速排序

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

希尔排序动图

快排是处理大数据最快的排序算法之一。它是一种分而治之的算法,通过递归的方式将数据依次分解为包含较小元素和较大元素的不同子序列。该算法不断重复这个步骤直至所有数据都是有序的。

简单说: 找到一个数作为参考,比这个数字大的放在数字左边,比它小的放在右边; 然后分别再对左边和右变的序列做相同的操作:

  1. 选择一个基准元素,将列表分割成两个子序列;
  2. 对列表重新排序,将所有小于基准值的元素放在基准值前面,所有大于基准值的元素放在基准值的后面;
  3. 分别对较小元素的子序列和较大元素的子序列重复步骤1和2

代码实现:

function quickSort(arr) {
    if (arr.length <= 1) return arr;
    let left = [];
    let right = [];
    let zhou = arr.splice(0, 1);//选取第一个为轴值,原数组改变
    for(let i = 0; i < arr.length ;i++){
        if(arr[i] < zhou){
            left.push(arr[i]);//小于轴值的放左半
        }else{
            right.push(arr[i]);//大于轴值的放右半
        }
    }
    // 递归
    return quickSort(left).concat(zhou,quickSort(right));
}
let arr1 = [2, 15, 7, 1, 9, 4];
console.log(arr1);
console.log(quickSort(arr1));

当每次换分的结果为含 ⌊n/2⌋和 ⌈n/2⌉−1 个元素时,最好情况发生,此时递归的次数为 logn,然后每次划分的时间复杂
度为 O(n),所以最优的时间复杂度为 O(nlogn)。一般来说只要每次换分都是常比例的划分,时间复杂度都为 O(nlogn)。

当每次换分的结果为 n-1 和 0 个元素时,最坏情况发生。划分操作的时间复杂度为 O(n),递归的次数为 n-1,所以最坏
的时间复杂度为 O(n²)。所以当排序序列有序的时候,快速排序有可能被转换为冒泡排序。

快速排序的空间复杂度取决于递归的深度,所以最好的时候为 O(logn),最坏的时候为 O(n)。

快速排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(logn) ,不是稳定排序。

详细资料可以参考:
《图解排序算法(五)之快速排序——三数取中法》
《关于快速排序的四种写法》
《快速排序的时间和空间复杂度》
《快速排序最好,最坏,平均复杂度分析》
《快速排序算法的递归深度》

二、时间复杂度,稳定性总结

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 是否稳定
冒泡排序 O(n²) O(n²) O(1)
选择排序 O(n²) O(n²) O(1) 不是
直接插入排序 O(n²) O(n²) O(1)
快速排序 O(nlogn) O(n²) O(logn) 不是
希尔排序 O(nlogn) O(n^s) O(1) 不是
posted @ 2021-09-15 23:21  青柠i  阅读(56)  评论(0编辑  收藏  举报