数组排序的10种常用方法(js版)

425 阅读10分钟

JavaScript 数组排序的 10 种常用方法

在 JavaScript 中,数组排序是一个常见的操作。本文将介绍 10 种常用的排序算法,并提供相应的 JavaScript 实现代码。每种算法的实现都附带了详细的注释,帮助理解其工作原理。


1. 选择排序

选择排序的基本思想是每次找到最小的元素,并将其与起始位置的元素交换。

// 1. 选择排序(找到最小)
function sort1(nums) {
    // 找到最小的那个,然后和起始位置互换
    const len = nums.length;
    let start = 0;
    while (start < len) {
        let min = start;
        for (let a = min + 1; a < len; a++) {
            if (nums[a] < nums[min]) {
                min = a;
            }
        }
        [nums[start], nums[min]] = [nums[min], nums[start]];
        start++;
    }
    return nums;
}

// console.log(sort1([1,2,5,32,5,6,1,3]))

2. 冒泡排序

冒泡排序通过多次遍历数组,每次将最大的元素“冒泡”到数组的末尾。

// 2. 冒泡(第一次找到最大的,再找到第二大的,依次往后放)
// 一次循环之后,因为是从前往后,所以是最后一个符合要求,为最大值.后面还是从前往后循环,最后一个已经好了,所以是len-i,优化算法
function sort2(nums) {
    const len = nums.length;
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - i; j++) {
            if (nums[j] > nums[j + 1]) {
                [nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];
            }
        }
    }
    return nums;
}

// console.log(sort2([1,2,5,1,0,6,1,3]))

3. 插入排序

插入排序通过将未排序的元素插入到已排序部分的适当位置来排序。

// 3. 插入排序
// 假设从0到start都是已经排过序的(类似斗地主起牌后插入对应位置)
function sort3(nums) {
    const len = nums.length;
    let start = 0;
    while (start < len) {
        // 这个从前往后的循环,一定会循环前面的全部有序数列,比如 12453, 第一次if走完是12354,必须要走完所有.会判断start次
        // 我们可以从后往前,这样第一次走完就是12435,第二次就是12345.第三次判断没问题,1就更不要判断,可以只判断4次
        for (let i = start; i >= 0; i--) {
            if (nums[i] < nums[i - 1]) {
                [nums[i - 1], nums[i]] = [nums[i], nums[i - 1]];
            } else {
                break;
            }
        }
        start++;
    }
    return nums;
}

// console.log(sort3([2.3,6,1,2,5,98,4,33,1]))

4. 希尔排序

希尔排序是插入排序的改进版,通过引入间隔(gap)来减少比较和交换的次数。

// 4. 插入排序的复杂度,和他的有序度有关,我们可以整理几次他的有序度.从而让复杂度小于n的平方
// 这个方法的复杂度与取得间隔h(gap)有关,根据经验,最好取值len/3+1  . 这个结论目前没有数学依据
function sort4(nums) {
    const len = nums.length;
    let gap = 1;
    while (gap * 3 + 1 < len) {
        gap = gap * 3 + 1;
    }
    while (gap >= 1) {
        // 对每个组的元素进行插入排序
        let start = gap;
        while (start < len) {
            for (let i = start; i >= gap; i -= gap) {
                if (nums[i - gap] > nums[i]) {
                    [nums[i - gap], nums[i]] = [nums[i], nums[i - gap]];
                }
            }
            start++;
        }
        gap = Math.floor(gap / 3);
    }
    return nums;
}

// console.log(sort4([2.3,6,1,2,5,98,4,33,1]))

5. 快速排序

快速排序通过选择一个基准元素,将数组分为两部分,一部分比基准小,另一部分比基准大,然后递归排序。

// 5. 快速排序
// 这个方法的思路是 我们把数组分为3部分.abc.,b是我取的一个元素(可能是第一个,或者是最后一个).然后数组里面比b大的,放到c里面,比b小的,放到a里面.直到每一个a c的长度都是1.再把这些全部合起来.就是排好的数组.
// 注意这个方法看起来更像是二分.不过面试中一般是指前面提到的二分插入排序.这个复杂度是一样的,这个也是更容易理解和更常用的.
function sort5(nums) {
    const len = nums.length;
    if (len <= 1) {
        return nums;
    } else {
        let left = [];
        let right = [];
        for (let i = 1; i < len; i++) {
            if (nums[i] <= nums[0]) {
                left.push(nums[i]);
            } else {
                right.push(nums[i]);
            }
        }
        return [...sort5(left), nums[0], ...sort5(right)];
    }
}

// console.log(sort5([2.3, 6, 1, 2, 5, 98, 4, 33, 1]))

6. 归并排序

归并排序通过将数组拆分为多个子数组,分别排序后再合并。

// 6. 归并排序
// 这个算法的思路大致是 我们先想一想,加入我们有2个有序数组[1,5,9,10],[2,4,6].我们如何把他合并成一个有序数组呢.我们可以用双指针,二边都从0开始,比较a[start]和b[start].然后小的放入新的数组c.对应的start加一,另一个不变
// 直到有一个start等于他本身的长度,我们再把另一个剩下的元素直接塞到后面就行了
// 那么我们这么等到这2个子序列呢?我们就要把原数组拆成2个.但他不一定有序,所以我们就一直拆.直到每个子序列的元素都只有一个元素.那么这个子序列一定是有序的. 这样我们得到2个子序列,每个子序列又有2个子序列....最后每一个子序列都是length==1为之.我们依次再组装回去就行了
function sort6(nums) {
    const len = nums.length;
    if (len <= 1) {
        return nums;
    }
    let mid = Math.floor(len / 2);
    let left = nums.slice(0, mid);
    let right = nums.slice(mid);

    const merge = (a, b) => {
        let res = [];
        let astart = 0;
        let bstart = 0;
        while (astart < a.length && bstart < b.length) {
            if (a[astart] >= b[bstart]) {
                res.push(b[bstart]);
                bstart++;
            } else {
                res.push(a[astart]);
                astart++;
            }
        }
        return res.concat(a.slice(astart).concat(b.slice(bstart)));
    }

    return merge(sort6(left), sort6(right));
}

// console.log(sort6([2.3, 6, 1, 2, 5, 98, 4, 33, 1]))

7. 堆排序

堆排序通过构建大顶堆来实现排序。

// 7. 堆排序
// 这个方法可能是对于前端开发最难受的一个算法了,他需要完全二叉树构成的大顶堆来解决的,这个不展开解释了,感兴趣的查看链接https://blog.csdn.net/wenwenaier/article/details/121314974
function sort7(nums) {
    // 构建堆的方法
    const heapify = (nums, n, i) => {
        let largest = i; // 初始化最大元素索引
        let left = 2 * i + 1; // 左子节点索引,画图就能发现了,这个是数学问题
        let right = 2 * i + 2; // 右子节点索引
        // 如果左子节点大于根节点
        if (left < n && nums[left] > nums[largest]) {
            largest = left;
        }

        // 如果右子节点大于当前最大元素
        if (right < n && nums[right] > nums[largest]) {
            largest = right;
        }

        // 当前树判断完了,我们的最大节点可能变了.我们要把他们的值也互换一下
        if (largest !== i) {
            [nums[i], nums[largest]] = [nums[largest], nums[i]];
            // 当前的最大节点变了,那么二叉树2个子节点肯定也变了.那么下层的最大堆可能也会受影响,所以我们要递归下方所有的子树
            heapify(nums, n, largest);
        }
    }

    let n = nums.length;
    // 开始构建大顶堆
    // 我们需要从最后一个非子节点开始遍历.因为只有他可能不是非完全二叉树.从这个开始,才能保障正确
    // 自己画一下,就能发现最后一个非子节点的索引,必是Math.floor(n / 2) - 1
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        heapify(nums, n, i);
    }

    // 构建完成,我们是从小到大排序,所以我们把最大值和最小值互换位置,让nums[0]这个当前的最大值,去最后.
    // 然后我们需要把剩下的元素(减少了一个最大值),重新再构建最大堆
    // 但是,此时我们的堆,除了第一个不对,下层的肯定是全对的.所以我们只需要一次就可以了.heapify(nums, n-1, 0).他自己会递归全部的子树的
    // 但是一次只能将一个元素移到最后,所以我们需要循环n-1次
    for (let i = n - 1; i > 0; i--) {
        // 将当前最大元素移到数组末尾
        [nums[0], nums[i]] = [nums[i], nums[0]];
        // 调用 maxHeapify 函数重新调整堆
        heapify(nums, i, 0);
    }

    return nums;
}

// console.log(sort7([2.3, 6, 1, 2, 5, 98, 4, 33, 1]))

8. 计数排序

计数排序通过统计每个元素的出现次数来进行排序。

// 8. 计数排序
// 思路大致:比如我们有一个数组[1,2,3,2,3,3]. 那么我们发现1个1,2个2,3个3. [1,2,2,3,3,3] 他可以先有一个计数器,知道各个元素的个数[1,2,3] .把这个处理一下,得到[1,3,6].这样1只能出现在1-1位或之前.2只能出现在3-1位或之前..依次同理,然后来填充新数组.时间复杂度是n
// 这个方法的空间复杂度不好,就算一个nums只有2个元素1和1000000000.那也是非常恐怖的.所以这个可以用map来优化
function sort8(nums) {
    let n = nums.length;
    let max = Math.max(...nums);
    let min = Math.min(...nums);
    let count = new Array(max - min + 1).fill(0);
    for (let i = 0; i < n; i++) {
        count[nums[i] - min]++;
    }
    for (let i = 1; i < count.length; i++) {
        count[i] += count[i - 1];
    }
    let res = new Array(n).fill(0);
    for (let i = n - 1; i > 0; i--) {
        // 注意我们是从后往前放入res的,所以我们也得从后往前循环,这样2个相同的元素,不会改变前后顺序
        res[count[nums[i] - min] - 1] = nums[i];
        count[nums[i] - min]--;
    }
    return res;
}

// console.log(sort8([6, 1, 2, 5, 98, 4, 33, 1,77,-6]))

9. 桶排序

桶排序通过将数据分到不同的桶中,分别排序后再合并。

// 9. 桶排序
// 这个方法有点像是计数排序和归并排序的二合一.就是把这样数据,根据一个范围,分到不同的桶里面,然后把每个桶里面的数据进行排序,然后再合并数组
// 同样只能处理整数
// 复杂度和桶的个数有关.理想状态是n,桶只有一个时,复杂度最大是n方
// 一个桶,全部数据进行排序.我们假设这个排序方法是n方.那么我们分开排,就算n1方+n2方+n3方...+nk方.它是小于n方的.如果k无穷大,那么他们的和是n.这个分而治之的方法是一个重要的算法思想
function sort9(nums, gap) {
    // 第二个参数表示桶之间的间隔,计数排序,默认是1
    if (nums.length === 0) {
        return nums;
    }
    let max = Math.max(...nums);
    let min = Math.min(...nums);

    // 桶的初始化
    let bucketCount = Math.floor((max - min) / gap) + 1;
    // 这里注意,不能之间fill([]).因为这样,所以的元素,都是同一个数组,我们必须用函数的方式,创建不同的空数组
    let buckets = new Array(bucketCount).fill(null).map(() => []);

    // 将数组中的值分配到各个桶中
    for (let i = 0; i < nums.length; i++) {
        buckets[Math.floor((nums[i] - min) / gap)].push(nums[i]);
    }
    // 对每个桶进行排序,这里使用插入排序
    nums = [];
    for (i = 0; i < buckets.length; i++) {
        sort3(buckets[i]);
        for (let j = 0; j < buckets[i].length; j++) {
            nums.push(buckets[i][j]);
        }
    }
    return nums;
}

// console.log(sort9([6, 1, 2, 5, 98, 4, 33, 1, 77, -6], 10))

10. 基数排序

基数排序通过按位排序来实现整体排序。

// 10. 基数排序
// 这个方法很有意思,它是将整数的个位数先进行排列,得到一个新的序列.再将这个新的序列,根据十位数进行排序.一直排到最大那个数的最高位
// 所以这个又是不能处理小数和无理数.但是它可以排序字符串
// 对于有负数的情况,我们要分开考虑.然后再把2个情况合起来.记得负数先转正数,再reverse排序后的数组,再把负号加回去.
function sort10(nums) {
    // 分离正数和负数
    let positives = [];
    let negatives = [];
    for (let num of nums) {
        if (num >= 0) {
            positives.push(num);
        } else {
            negatives.push(-num); // 取负数的绝对值
        }
    }
    // 定义基数排序的算法
    function radixSortForPositive(nums) {
        let max = Math.max(...nums);
        // 看看最大值一共有多少位
        let digits = max ? Math.floor(Math.log10(max)) + 1 : 0;

        for (let i = 0; i < digits; i++) {
            // 定义一个二维数组,一共10个,每个是空数组
            let buckets = Array.from({ length: 10 }, () => []);
            for (let j = 0; j < nums.length; j++) {
                // 开始从头开始遍历数组,从低到高位(i++)
                let digit = Math.floor(nums[j] / Math.pow(10, i)) % 10;
                buckets[digit].push(nums[j]);
            }
            // 每一次都会改变nums的顺序
            nums = [].concat(...buckets);
        }
        return nums;
    }
    // 对正数和负数分别进行基数排序
    let sortedPositives = radixSortForPositive(positives);
    let sortedNegatives = radixSortForPositive(negatives);

    // 负数部分需要反转,因为我们是按绝对值排序的
    sortedNegatives.reverse();

    // 将负数转换回原来的负值
    sortedNegatives = sortedNegatives.map(num => -num);

    // 合并结果
    return sortedNegatives.concat(sortedPositives);
}

// console.log(sort10([-1,0,-20,-3,3,12,5]))

以上就是 10 种常用的 JavaScript 数组排序方法,每种方法都附带了详细的注释和代码实现。希望这篇文章能帮助你更好地理解这些排序算法的工作原理和应用场景。