排序算法分析及ts版本代码实现

50 阅读3分钟
文章开头第一句加入:本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

一、排序算法分类及简介:

image.png 排序有十大算法,分为两大类-比较类七种和非比较类三种。 比较类又分为4类,分别为:

  1. 交换类:冒泡排序,快速排序
  2. 插入类:插入排序,希尔排序
  3. 选择类:选择排序,堆排序
  4. 归并排序
  • 其中稳定的排序算法有:(插帽龟)
  • 使用分治思想可以用递归实现的排序算法有:快速排序和归并排序,其中快速排序用到divede-conqure, 而归并排序用到divide-conqure-merge。这两者的时间复杂度都为O(nlogn)。
  • 其中用到额外存储空间的有:归并排序,空间复杂度为O(n)

概览如下:

image.png

二、代码实现及解释

function swap(arr:number[],i:number,j:number){
    const temp=arr[i];
    arr[i]=arr[j];
    arr[j]=temp;
}

/**第一类:交换类排序**/
/**
 * 1. 冒泡排序(简单,省略)
 */

/**
 * 2. 快速排序: 递归,每轮将比pivot小的放其左边,大的放其右边,再分别对其左右子问题递归 
 */
const quickSort=(nums:number[]):number[]=>{
    quick(nums,0,nums.length-1);
    return nums;
}
const quick=(nums:number[],left:number,right:number)=>{
    if(left>right) return [];
    let index=findIndex(nums,left,right);
    index-1>left&&quick(nums,left,index-1);
    index+1<right&&quick(nums,index+1,right);
}
// 获取pivot的最终index,同时将比其小的放其左边,大的放其右边
const findIndex=(nums:number[],left:number,right:number)=>{
    
    if(left>right) return -1;

    let len=right+1-left;
    // 1.随机获取pivot,放在left位置
    let randomIndex=Math.floor(Math.random()*len+left);
    swap(nums,randomIndex,left);
    const pivot=nums[left];
    
    // 2. 从left+1遍历,将比pivot小的从left位置依次放入左边,然后pivot,其右边自然为比pivot大的元素
    let lt=left; // 记录比pivot小的子集的末尾index,找到比其小的之后,放入该位置,然后递增
    for(let i=left+1;i<=right;i++){
        if(nums[i]<pivot){
            lt++;
            swap(nums,lt,i); // 易错点
            // nums[lt]=nums[i]; // 错误
        }
    }
    swap(nums,lt,left) // 易错点:lt为比其小的子集的尾index,直接和left位置的pivot交换即可
    return lt;
}

/**第二类-插入类排序:遍历维护有序子集,每轮找pivot在有序子集中的位置**/
/** 
 * 3. 插入排序:遍历过的元素按大小顺序放在左侧,左边维护有序子集,从后往前便利有序子集寻找pivot在有序子集中的位置
 * 每个迭代内目标:从后往前找pivot在有序序列里的位置(遍历过的元素排为有序放在左侧)
 */ 
const insertSort=(arr:number[]):number[]=>{
    for(let i=1;i<arr.length;i++){
        let pivot=arr[i];
        let j=i-1;
        while(arr[j]>pivot&&j>=0){
            arr[j+1]=arr[j];
            j--;
        }
        arr[j+1]=pivot;
    }
    return arr;
}

/**
 * 4. 希尔排序:按步长插入排序(改进版插入排序)
 * 在插入排序的基础上,加入了步长增量
 * 步长依次递减:gap=length/2 -> length/4 -> length/8 ->... ,终止条件:gap<1
 * 每个步长迭代内:插入排序
 */
const shellSort=(nums:number[]):number[]=>{
    let length=nums.length;
    if(length<2) return nums;

    for(let gap=Math.floor(length/2);gap>=1;gap=Math.floor(gap/2)){
        for(let i=gap;i<length;i++){
            let pivot=nums[i];
            let j=i-gap;
            while(pivot<nums[j]&&j>=0){
                nums[j+gap]=nums[j];
                j-=gap;
            }
            nums[j+gap]=pivot;
        }
    }
    return nums;
}

/**第三类 - 选择类排序:每轮找到最大/小,放到数组尾/头**/
// 5. 选择排序: 每轮找到最大,放到队尾
const selectSort=(arr:number[]):number[]=>{
    let len=arr.length;
    for(let i=0;i<len;i++){
        let minIndex=i;
        for(let j=i+1;j<len;j++){
            if(arr[j]<arr[minIndex]){
                minIndex=j;
            }
        }
        swap(arr,minIndex,i);
    }
    return arr;
}

/**
 * 6. 堆排序:每轮选择最大,改进版选择排序
 * 利用堆的特性,大顶堆顶元素最大,获取最大的元素。再对剩下的元素建堆,获取次最大元素。以此类推递归。
 * 前置知识:堆父子节点的关系:子节点i的父节点index=Math.floor(i-1/2), 节点j的左右子节点index分别为2*j+12*j+2
 */
const heapSort=(nums:number[]):number[]=>{
    const len=nums.length;
    // 1. 建堆:从数组最后一个元素开始建堆,利用堆的特性,其父亲节点index即为Math.floor(len/2-1),递归建堆
    for(let i=Math.floor(len/2-1);i>=0;i--){
        adjustHeap(nums,len,i);
    }
    // 2. 排序:建堆后,堆顶元素即为最大值,和数组最后一个元素交换,再堆剩下的元素建堆。
    // 由于剩下的元素堆顶元素变化,所以需要重新建堆,从变化的堆顶元素开始。
    // 以此类推递归。
    for(let i=len-1;i>=0;i--){
        swap(nums,i,0);
        adjustHeap(nums,i,0);
    }
    
    return nums;
}
const adjustHeap=(nums:number[],len:number,i:number)=>{
    
    // 1. 比较父和左右子节点,获取最大节点index
    let largest=i,lt=2*i+1,rt=2*i+2;
    if(lt<len&&nums[lt]>nums[largest]){
        largest=lt;
    }
    if(rt<len&&nums[rt]>nums[largest]){
        largest=rt;
    }

    // 2. 若三者中最大非父节点,则交换。然后针对交换改变的元素重新调整建堆。
    if(largest!==i){
        swap(nums,largest,i);
        adjustHeap(nums,len,largest);
    }
}

/**第四类 - 归并类排序: 二路或多路归并**/
/**
 * 7. 归并排序:分治思想,divide-conqure-merge
 * 分半,递归,合并
 */
const mergeSort=(nums:number[]):number[]=>{
    // 1. 终止条件
    if(nums.length<2) return nums;

    // 2. 分半递归排序,然后在合并
    let mid=Math.floor(nums.length/2)
    return merge(mergeSort(nums.slice(0,mid)),mergeSort(nums.slice(mid)));
}
// 针对两个有序数组的合并
const merge=(left:number[],right:number[]):number[]=>{
    if(left.length===0&&right.length===0) return [];

    const res=[];
    while(left.length>0&&right.length>0){
        if(left[0]<right[0]){
            res.push(left.shift() as  number)
        }else{
            res.push(right.shift() as number)
        }
    }
    return res.concat(...left,...right);
}