JavaScript 排序算法

本文详细介绍了JavaScript中常见的排序算法,包括直接插入排序、折半插入排序、希尔排序、冒泡排序、快速排序、选择排序、堆排序、归并排序、基排序、计数排序和桶排序。讨论了每种算法的时间复杂度、空间复杂度和稳定性,并指出它们的适用场景。例如,快速排序在平均情况下的时间复杂度为O(nlogn),而计数排序是一种线性时间复杂度的稳定排序算法,但不适用于范围跨度大或浮点数排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
在这里插入图片描述
排序算法的稳定性:排序后,多个相同记录的相对次序保持不变,则排序算法稳定;否则不稳定。

总结

内部排序:排序期间元素全部放在内存中

  • 插入排序:直接插入排序折半插入排序希尔排序
  • 交换排序:冒泡排序快速排序
  • 选择排序:简单选择排序堆排序
  • 归并排序
  • 基数排序

外部排序:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存

  • 多路归并排序

1.直接插入排序

1.把数组分为[已排序]和[未排序]两部分,第一个数为[已排序],其余为[未排序];
2.从[未排序]抽出下一个数,在[已排序]部分从后向前扫描,插入到合适的位置,重复此步骤;

function insertSort(nums) {
    for(var i=1;i<nums.length;i++){
        var j = i -1;
        var tmp = nums[i];
        while (j>=0 && nums[j] > tmp){
            nums[j+1] = nums[j];
            j--;
        }
        nums[j+1] = tmp;
    }
    return nums;
}

空间复杂度

只用到O(1)的额外空间

时间复杂度

最好情况O(n):数组有序,比较次数为n-1次,移动次数为0次;
最坏情况O(n2):数组逆序,比较次数为n*(n-1)/2,移动次数同比较次数;
平均情况O(n2):取最好和最坏的平均值,比较次数与移动次数约为n2/4;

稳定性

每次插入都是从后向前先比较再插入,不会改变相同元素的相对位置,是一个稳定的排序算法;

算法说明

适用于待排序数据大部分已排序时,适用于顺序存储和链式存储的线性表


2.折半插入排序

直接插入排序是边比较边移动元素,而折半插入排序将比较和移动分开进行

function insertSort(nums) {
    var len = nums.length;
    if(len < 2){
        return nums;
    }
    for(var i=1;i<len;i++){
        var tmp = nums[i],
            low = 0,
            high = i - 1;
        //找到插入元素的位置
        while(low<=high){
            var mid = Math.floor((low+high)/2);
            if(nums[mid]>tmp){
                high = mid - 1;
            }
            else{
                low = mid+1;
            }
        }
        //移动,将元素插入到high+1的位置
        for(var j = i-1;j>=high+1;j--){
            nums[j+1] = nums[j];
        }
        nums[high+1] = tmp;
    }
    return nums;
}
console.log('insertSort2',insertSort([2,1,3,3,5,4,0]));

首先,折半插入排序的比较次数与待排序表的初始状态无关,仅取决于元素个数n,约为O(nlogn);
第二,折半插入排序相对直接插入排序仅仅减少了比较元素的次数,而元素的移动次数没有改变;
最好情况下,比较次数为O(nlogn),移动次数为0,时间复杂度为O(nlogn);
最坏情况下,比较次数为O(nlogn),移动次数为n*(n-1)/2,时间复杂度为O(n2);
平均复杂度为O(n2);
同直接插入排序一样,是稳定的排序方法

适用于顺序存储的线性表


3.希尔排序

希尔排序是把记录按下标的一定增量d分组,对每组使用直接插入排序算法排序;随着增量(d/2)逐渐减少,每组包含的元素越来越多,当增量减至1时(d=1),整个序列恰被分成一组,算法终止。

//希尔排序
function xierSort(nums) {
    var len = nums.length;
    if(len < 2){
        return nums;
    }

    for(var i=Math.floor(len/2);i>0;i = Math.floor(i/2)){
        //内部类似于直接插入排序,只是步长不一样
        for(var j = i;j<len;j++){
            var k = j - i,
                tmp = nums[j];
            while(k>=0 && nums[k]> tmp){
                nums[k+i] = nums[k];
                k -= i;
            }
            nums[k+i] = tmp;
        }
    }
    return nums;
}
console.log(xierSort([4,5,2,3,1,2]));

希尔排序在最坏情况下为O(n2),当n在某个特定范围时,约为O(n1.3);
当相同的元素被分到不同的子表中时,可能会改变他们之间的相对次序,所以是不稳定的排序;
适用于顺序存储的线性表


4.冒泡排序

比较相邻两个数,若前者大于后者,交换位置,第一轮选出一个最大的数放在最后面;经过n-1轮,就完成了所有数的排序

function bubbleSort(nums) {
    var len = nums.length;
    for(var i=0;i<len-1;i++){
        var flag = false;
        for(var j = 0;j<len-1-i;j++){
            if(nums[j] > nums[j+1]){
                [nums[j],nums[j+1]] = [nums[j+1],nums[j]];
                flag = true;
            }
        }
        if(flag == false){
            break;
        }
    }
    return nums;
}

最好情况:已经有序,一趟即可,比较n-1次,交换0次,时间复杂度为O(n);
最坏情况:数组逆序,交换1+2+…+n-1 = n*(n-1)/2次,时间复杂度为O(n2);


5.快速排序

1.以一个数为基准,比基准小的放到左边,比基准大的放到右边。
2.再按此方法对这两部分数据分别进行快速排序(递归)。

function quickSort(nums) {
    if(nums.length<2){
        return nums;
    }
    var left = 0,
        right = nums.length-1;

    while (left < right){
        while (left < right && nums[right] > nums[0]) {
            right--;
        }
        while (left < right && nums[left] <= nums[0]){
            left++;
        }
        if(left == right){
            [nums[left],nums[0]] = [nums[0],nums[left]];
            break;
        }
        [nums[left],nums[right]] = [nums[right],nums[left]];
    }
    return quickSort(nums.slice(0,left)).concat(nums[left]).concat(quickSort(nums.slice(left+1)));
}
console.log(quickSort([2,2,1,4,1]));

时间复杂度

快速排序的运行时间与划分是否对称有关
最好情况O(n*logn),每次递归恰好能均分序列O(n),其递归深度为O(logn);
最坏情况O(n2),每次划分两个区域(O(n))分别包含n-1个元素和0个元素,递归深度为O(n);

空间复杂度

由于快速排序是递归的,需要一个递归工作栈保存每层递归调用的必要信息;
最好情况O(logn)
最坏情况O(n)
平均情况O(logn)

稳定性

在划分时,若右端存在两个相同的元素且小于基准值,则在交换到左边区间时,其相对位置会发生变化,因此不稳定;如[‘b’,‘a1’,‘a2’];

注意

快速排序中,每一趟排序后将基准元素放到其最终的位置上


6.选择排序

选出最小的数和第一个数交换,在剩余部分又选择最小的数和第二个数交换,依次类推

function selectSort(nums) {
    var len = nums.length;
    for(var i=0;i<len;i++){
        var min = i;
        for(var j = i+1;j<len;j++){
            if(nums[j]<nums[min]){
                min = j;
            }
        }
        if(min != i){
            [nums[i],nums[min]] = [nums[min],nums[i]];
        }
    }
    return nums;
}

比较次数为1+2+…+n-1 = n*(n-1)/2次,时间复杂度为O(n2);
最好情况:已经有序,交换0次;
最坏情况:交换n-1次,逆序交换n/2次;

算法不稳定:在一趟选择,如果元素A比当前元素B小,而A又出现在一个和B相等的元素后面,那么交换后两个B间的稳定性就被破坏了,如5 8 5 2 9;


7.堆排序

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

堆排序的基本思想

将待排序序列构造成一个大顶堆,序列的最大值就是堆顶的根节点。将其与末尾元素交换,然后将剩余n-1个元素重新构造成一个大顶堆,如此反复执行,得到有序序列

步骤

  • 构造初始堆。将给定无序序列构造成一个大顶堆
  • 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆(从顶点开始往下调整),再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
//建堆
var len;
function buildMaxHeap(nums) {
    len = nums.length;
    for(var i=Math.floor(len/2)-1;i>=0;i--){
        AdjustDown(nums,i);
    }
}

function AdjustDown(nums,k) {
    var tmp = nums[k];
    for(var i=2*k+1;i<len;i=2*i+1){
        if(i<len-1 && nums[i]<nums[i+1]){
            i++;
        }
        if(tmp>=nums[i]) break;
        else{
            nums[k] = nums[i];
            k = i;
        }
    }
    nums[k] = tmp;
}

function heapSort(nums) {
    buildMaxHeap(nums);
    for(var i=nums.length-1;i>0;i--){
        [nums[0],nums[i]] = [nums[i],nums[0]];
        len--;
        AdjustDown(nums,0);
    }
    return nums;
}

console.log(heapSort([5,4,2,3,2,1]));

注意

特别特别注意: 初始化大顶堆时 是从最后一个有子节点开始往上调整最大堆。而堆顶元素(最大数)与堆最后一个数交换后,需再次调整成大顶堆,此时是从上往下调整的。

时间复杂度

建堆时,每个非终端结点最多进行两次比较和一次互换操作,时间复杂度为O(n)。大概n/2 * 2 = n次比较和n/2次交换。

排序时,n个结点的完全二叉树的深度为⌊log2n⌋+1,每次调整成大顶堆的时间复杂度为O(log2n),并且有n-1次向下调整操作。因此,重建堆的时间复杂度为 O(nlogn)。

稳定性

筛选时,有可能把后面相同的关键字的元素调整到前面,因此堆排序是不稳定的排序算法,如[‘a’,‘b2’,‘b1’],可能将‘b2’换到堆顶;


8.归并排序

1.不断将数组对半分,直到每个数组只有一个。
2.将分出来的部分重新合并。
3.合并的时候按顺序排列。

// 将两个数组合并, 合并的时候按从小到大的顺序
function merge(left,right) {
    var res = [],
        l = 0,
        r = 0;
    while(l<left.length && r < right.length){
        if(left[l]<right[r]){
            res.push(left[l++]);
         }
        else{
            res.push(right[r++]);
        }
    }
    return res.concat(left.slice(l)).concat(right.slice(r));
}


function mergeSort(nums) {
    var len = nums.length;
    if(len < 2){
        return nums;
    }

    var mid = Math.floor(len/2),
        left  = nums.slice(0,mid),
        right = nums.slice(mid);

    // 递归
    // 不断拆分只到一个数组只有一个数
    return merge(mergeSort(left),mergeSort(right));
}

在这里插入图片描述

将递归式完全扩展后,形成了完整的递归树,一共是lgn+1层,每层的执行时间是cn,那么总代价cnlgn+cn,忽略低阶项和常量c,即有T(n) = Θ(nlgn)。


9.基排序

基数排序是一种 非比较型 整数 排序算法,不能对float和double类型的实数进行排序,是根据键值的每位数字来分配桶;

//基排序,maxDigit为最大位数
var counter = [];
function radixSort(nums,maxDigit) {
    var len = nums.length;
    if(len < 2){
        return nums;
    }
    var mod = 10,
        dev = 1;
    for(var i=0;i<maxDigit;i++,dev*=1,mod*=10){
        for(var j = 0;j<len;j++){
            var bucket = parseInt((nums[j]%mod)/dev);
            if(counter[bucket]==null){
                counter[bucket] = [];
            }
            counter[bucket].push(nums[j]);
        }
        nums.length = 0;
        for(var j=0;j<counter.length;j++){
            var val = null;
            if(counter[j]!=null){
                while((val=counter[j].shift())!=null){
                    nums.push(val);
                }
            }
        }
    }
    return nums;
}
console.log(radixSort([4,5,3,2,1,2],1));

空间复杂度 :O®,需要r(r个队列)存储空间
时间复杂度 :基数排序需要d趟分配与收集,一趟分配O(n),一趟收集O®,所以时间复杂度为O(d*(n+r)),与序列的初始状态无关
稳定性:按位排序是稳定的


10.计数排序

计数排序是一种非基于比较的排序算法,排序的速度快于任何比较排序算法;

计数排序核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。计数排序要求数据必须是有确定范围的整数

计数排序是一种稳定的线性时间排序算法,其时间复杂度为O(n+k),k = max-min+1(时间复杂度理解为遍历计数范围时间+将n个元素添加入数组的时间),空间复杂度可以为O(k),总的来说,计数排序的时间和空间复杂度均为O(n);

弊端

不擅长处理范围跨度很大的数字排序
浮点型数字不好处理

1.花O(n)的时间扫描一下整个序列 A,获取最大值 max【获取最大值max和最小值min】

2.开辟一块新的空间创建新的数组 B,长度为 ( max + 1)【长度为max-min+1】

3.数组 B 中 B[A[i]]记录的是 A[i] 元素出现的次数 【B[A[i]-min]】

4.最后遍历数组 B,输出相应元素

function countingSort(nums) {
    const max = Math.max(...nums);

    //将类数组对象转换为数组
    let B = Array.from({length:max+1}).fill(0);  
    nums.forEach(item=>B[item]++);

    var index =0;
    for(let i=0;i<B.length;i++){
        while(B[i]>0){
            nums[index++] = i;
            B[i]--;
        }
    }
    return nums;
}

console.log(countingSort([5, 2, 4, 6, 1, 3]));
function countingSort2(nums) {
    const max = Math.max(...nums);
    const min = Math.min(...nums);
    let B = Array.from({length:max-min+1}).fill(0);
    nums.forEach(item=>B[item-min]++);

    var index = 0;
    for(let i=0;i<B.length;i++){
        while (B[i]>0){
            nums[index++] = i+min;
            B[i]--;
        }
    }
    return nums;
}
console.log(countingSort2([5, 2, 4, 6, 2, 3]));

11.桶排序

桶排序是将数组分到有限数量的桶里,再对每个桶中的数据进行排序

桶排序是对技术排序的改进,对计数排序,若待排序集合中元素不是依次递增的,则必然有空间浪费情况,而桶排序最小值到最大值之间的每一个位置申请空间更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况。

对于相同数量的数据,桶的数量越多,数据分散得越平均,桶排序的效率越高,可以说,桶排序的效率是空间的牺牲换来的。

桶排序的两个关键环节

元素到桶的映射规则,根据待排序集合的元素分布特性进行选择,时间复杂度为O(n);
排序算法的选择,在对各个桶中元素进行排序时,可以自主选择合适的排序算法,桶排序算法的复杂度和稳定性,都根据选择的排序算法不同而不同

//桶排序,bucketSize为间隔大小
function bucketSort(nums,bucketSize = 5) {
    var len = nums.length;
    if(len<2){
        return nums;
    }

    //寻找数组中的最大值和最小值
    var max = Math.max(...nums),
        min = Math.min(...nums);

    //bucketCount为桶个数
    var bucketCount = Math.floor((max - min) / bucketSize) + 1;
    var buckets = new Array(bucketCount);
    for (var i = 0; i < buckets.length; i++) {
        buckets[i] = [];
    }
    //利用映射函数将数据分配到各个桶中
    for (var i = 0; i < len; i++) {
        buckets[Math.floor((nums[i] - min) / bucketSize)].push(nums[i]);
    }

    // 对每个桶进行排序,这里使用了插入排序
    nums.length = 0;

    for(var i=0;i<buckets.length;i++){
        insertSort(buckets[i]);
        for(var j=0;j<buckets[i].length;j++){
            nums.push(buckets[i][j]);
        }
    }
    console.log(nums);
}

function insertSort(nums){
    var len = nums.length;
    if(len < 2)
        return nums;
    var j,tmp;
    for(var i=1;i<len;i++){
        if(nums[i]<nums[i-1]){
            j = i-1;
            tmp = nums[i];
            while(j>=0 && nums[j]>tmp){
                nums[j+1] = nums[j];
                j--;
            }
            nums[j+1] = tmp;
        }
    }
}

var nums = [5,6,4,2,3,1,2,7,9,8];
bucketSort(nums);

空间复杂度:O(n * k),共k个桶,每个桶的大小为n

时间复杂度

桶排序的,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)

稳定性:稳定


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值