一步一步的带你梭哈常见的排序算法,每一个步骤都巨详细!

排序算法

冒泡排序

基本思路

  • 基本思路是通过两两比较相邻的元素并交换它们的位置,从而使整个序列按照顺序排列。
  • 该算法一趟排序后,最大值总是会移到数组最后面,那么接下来就不用再考虑这个最大值。
  • 一直重复这样的操作,最终就可以得到排序完成的数组。

实现步骤

  • 从第一个元素开始,逐一比较相邻元素的大小。
  • 如果前一个元素比后一个元素大,则交换位置。
  • 在第一轮比较结束后,最大的元素被移动到了最后一个位置。
  • 在下一轮比较中,不再考虑最后一个位置的元素,重复上述操作。
  • 每轮比较结束后,需要排序的元素数量减一,直到没有需要排序的元素。
  • 排序结束。
  • 这个流程会一直循环,直到所有元素都有序排列为止。

代码实现

时间复杂度:O(n2)

ini 复制代码
function foo(arr){
    const n = arr.length
    // 循环未排序的部分
    for (let i = 0; i < n - 1; i++) {
        // 这里为什么是arr.length - 1:在下面我们有 j+1 的情况,不减1会越界
        // arr.length - 1 - i:每一次排序过后,后面的数据就不用管他了
        // 内层循环找到最大值,不断交换
        for (let j = 0; j < n - 1 - i; j++) {
            if(arr[j] > arr[j+1]){
                // let temp = arr[j]
                // arr[j] = arr[j+1]
                // arr[j+1] = temp
    
                // 下面是es6的语法,简写
                [ arr[j], arr[j+1] ] = [ arr[j+1], arr[j] ]
            }
        }
        
    }
    return arr
}
ini 复制代码
// 性能优化版本:当我们在循环的时候,某一次循环,发现没有发生交换的时候,这个排序就已经结束了,不需要再继续下去消耗性能了
​
function foo(arr){
    const n = arr.length
    
    for (let i = 0; i < n-1; i++) {
        let isSwapped = false
        for (let j = 0; j < n-1-i; j++) {
            if( arr[j] > arr[j+1] ){
                [ arr[j], arr[j+1] ] = [ arr[j+1], arr[j] ]
                isSwapped = true
            }
        }
​
        if(!isSwapped) break
        
    }
    return arr
}
​

时间复杂度

  • 在冒泡排序中,每次比较两个相邻的元素,并交换他们的位置,如果左边的元素比右边的元素大,则交换它们的位置。这样的比较和交换的过程可以用一个循环实现。

  • 最好情况:O(n)

    • 即待排序的序列已经是有序的。
    • 此时仅需遍历一遍序列,不需要进行交换操作。
  • 最坏情况:O(n^2)

    • 即待排序的序列是逆序的。
    • 需要进行n-1轮排序,每一轮中需要进行n-i-1次比较和交换操作。
  • 平均情况:O(n^2)

    • 即待排序的序列是随机排列的。
    • 每一对元素的比较和交换都有1/2的概率发生,因此需要进行n-1轮排序,每一轮中需要进行n-i-1次比较和交换操作。
  • 由此可见,冒泡排序的时间复杂度主要取决于数据的初始顺序。最坏情况下时间复杂度是O(n^2),不适用于大规模数据的排序。

选择排序

基本思路

  • 首先在未排序的数列中找到最小元素 ,然后将其存放到数列的起始位置
  • 接着,再从剩余未排序的元素中继续寻找最小元素,然后放到已排序序列的末尾
  • 以此类推,直到所有元素均排序完毕。

实现流程

  • 遍历数组,找到未排序部分的最小值

    • 首先,将未排序部分的第一个元素标记为最小值
    • 然后,从未排序部分的第二个元素开始遍历,依次和已知的最小值进行比较
    • 如果找到了比最小值更小的元素,就更新最小值的位置
  • 将未排序部分的最小值放置到已排序部分的后面

    • 首先,用解构赋值的方式交换最小值和已排序部分的末尾元素的位置
    • 然后,已排序部分的长度加一,未排序部分的长度减一
  • 重复执行步骤1和2,直到所有元素都有序

实现步骤

时间复杂度:O(n2)

ini 复制代码
// 先找到最小的排到第一个
function foo(arr){
    const n = arr.length
    
    let minIndex = 0

    // 从第一个开始,第0个是自己不需要找
    for (let j = 1; j < n; j++) {
        if(arr[minIndex] > arr[j]){
            minIndex = j
        }
    }

    //这里我们就可以找到最小值了
    console.log(arr[minIndex]);
    //交换,这个时候就最小值在第一个了
    [ arr[0], arr[minIndex] ] = [ arr[minIndex], arr[0] ]


    return arr
}
ini 复制代码
// 循环上述步骤
function foo(arr){
    const n = arr.length
    
    // 为什么是n-1,因为排到最后一个,一定是最大的
    for (let i = 0; i < n - 1; i++) {
        
        let minIndex = i

        // 从第i+1个开始,前面已经排序过了
        // 作用:找到最小值
        for (let j = 1 + i; j < n; j++) {
            if(arr[minIndex] > arr[j]){
                minIndex = j
            }
        }

        //小优化:不相等的时候交换
        if(i !== minIndex){
            [ arr[i], arr[minIndex] ] = [ arr[minIndex], arr[i] ]
        }
        
    }


    return arr
}

时间复杂度

  • 最好情况时间复杂度:O(n^2)

    • 最好情况是指待排序的数组本身就是有序的。
    • 在这种情况下,内层循环每次都需要比较n-1次,因此比较次数为n(n-1)/2,交换次数为0。
    • 所以,选择排序的时间复杂度为O(n^2)。
  • 最坏情况时间复杂度:O(n^2)

    • 最坏情况是指待排序的数组是倒序排列的。
    • 在这种情况下,每次内层循环都需要比较n-i-1次,因此比较次数为n(n-1)/2,交换次数也为n(n-1)/2。
    • 所以,选择排序的时间复杂度为O(n^2)。
  • 平均情况时间复杂度:O(n-2)

    • 平均情况是指待排序的数组是随机排列的。
    • 在这种情况下,每个元素在内层循环中的位置是等概率的,因此比较次数和交换次数的期望值都是n(n-1)/4。
    • 所以,选择排序的时间复杂度为O(n^2)。

插入排序

基本思路

  • 首先假设第一个数据是已经排好序的,接着取出下一个数据,在已经排好序的数据中从后往前扫描,找到比它小的数的位置 ,将该位置之后的数整体后移一个单位,然后再将该数据插入到该位置
  • 不断重复上述操作,直到所有的数据都插入到已经排好序的数据中,排序完成。

实现步骤

  • 1.首先,假设数组的第一个元素已经排好序了,因为它只有一个元素,所以可以认为是有序的。
  • 2.然后,从第二个元素开始,不断与前面的有序数组元素进行比较。
  • 3.如果当前元素小于前面的有序数组元素,则把当前元素插入到前面的合适位置。
  • 4.否则,继续与前面的有序数组元素进行比较。
  • 5.以此类推,直到整个数组都有序。
  • 6.循环步骤2~5,直到最后一个元素。

代码实现

时间复杂度:O(n2)

scss 复制代码
// 内循环
function foo(arr){
    const n = arr.length

    // 假设数组的那么我们前面有三个牌已经排好了,我们应该去第4个牌来与前面进行比较
    let newNum = arr[4]
    // 不确定循环次数,还不知道在哪里插入。假设拿到第四个牌,那么我们前面有三个牌已经排好了
    let j = 4 - 1
    // 我们的找法是从后往前找的
    while(arr[j] > newNum && j >=0 ){
        //后移操作
        arr[j+1] = arr[j]
        j--
    }

    //循环结束,要么 j=-1,要么 arr[j] == newNum
    // 把后面空的位置给newNum
    arr[j+1] = newNum

    return arr
}
ini 复制代码
// 循环上述步骤
function foo(arr){
    const n = arr.length
    // 从下标为1的开始,因为下标为0的第一个我们默认是排好序的
    for (let i = 1; i < n; i++) {
        let newNum = arr[i]
        let j = i - 1
        while(arr[j] > newNum && j >=0 ){
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = newNum
    }

    return arr
}

时间复杂度

  • 最好情况:O(n)

    • 如果待排序数组已经排好序
    • 那么每个元素只需要比较一次就可以确定它的位置,因此比较的次数为n-1,移动的次数为0。
    • 所以最好情况下,插入排序的时间复杂度为线性级别,即o(n)。
  • 最坏情况:O(n^2)

    • 如果待排序数组是倒序排列的
    • 那么每个元素都需要比较和移动i次,其中i是元素在数组中的位置。
    • 因此比较的次数为n(n-1)/2,移动的次数也为n(n-1)/2。
    • 所以最坏情况下,插入排序的时间复杂度为平方级别,即O(n^2)。
  • 平均情况:O(n^2)

    • 对于一个随机排列的数组,插入排序的时间复杂度也为平方级别,即O(n^2)。

归并排序

基本思路

  • 它的基本思想是将待排序数组分成若干个子数组。
  • 然后将相邻的子数组归并成一个有序数组。
  • 最后再将这些有序数组归并(merge)成一个整体有序的数组。

实现步骤

  • 归并排序是一种基于分治思想的排序算法,其基本思路可以分为三个步骤:

  • 步骤一:分解(Divide)。归并排序使用递归算法来实现分解过程,具体实现中可以分为以下几个步骤

    • 如果待排序数组长度为1,认为这个数组已经有序,直接返回;
    • 将待排序数组分成两个长度相等的子数组,分别对这两个子数组进行递归排序
    • 将两个排好序的子数组合并成一个有序数组,返回这个有序数组。
  • 步骤二:合并(Merge)。合并过程中,需要比较每个子数组的元素并将它们有序地合并成一个新的数组:

    • 可以使用两个指针i和j分别指向两个子数组的开头,比较它们的元素大小,并将小的元素插入到新的有序数组中。
    • 如果其中一个子数组已经遍历完,就将另一个子数组的剩余部分直接插入到新的有序数组中。
    • 最后返回这个有序数组。
  • 步骤三:归并排序的递归终止条件

    • 归并排序使用递归算法来实现分解过程,当子数组的长度为1时,认为这个子数组已经有序,递归结束。

代码实现

时间复杂度:O(n logn)

scss 复制代码
function mergeSort(arr){

    // 1.3 递归结束条件
    if(arr.length <= 1) return arr

    const n = arr.length

    // 1 分解:对数组进行分解(递归)
    // 1.1 切割操作
    const mid = Math.floor(n/2)
    const leftArr = arr.slice(0, mid)
    // 不写slice(mid, n)就默认从mid切割到末尾
    const rightArr = arr.slice(mid)

    // 1.2 递归切割
    const newLeftArr = mergeSort(leftArr)
    const newRightArr = mergeSort(rightArr)

    // 当我们可以执行下述代码的时候,上述的newArr数组只有一个数了

    // 2.合并:将两个子数组进行合并(双指针)
    // 2.1 定义双指针
    let newArr = []
    let i = j = 0
    while( i < newLeftArr.length && j < newRightArr.length ){
        // 2.2 比较大小
        if(newLeftArr[i] <= newRightArr[j]){
            newArr.push(newLeftArr[i])
            i++
        }else{
            newArr.push(newRightArr[j])
            j++
        }
    } 
    // 结束条件是一边相等,为length结束,因为取值最多取到length-1
    // 2.3 其中一个子数组已经遍历完,另一个子数组的可能还有剩余部分,直接插入到新的有序数组中

    // 循环完左边有剩余
     if( i < newLeftArr.length ){
        newArr.push(...newLeftArr.slice(i))
     }
      // 循环完右边有剩余
     if( j < newLeftArr.length ){
        newArr.push(...newRightArr.slice(j))
     }

     // 3.返回新数组
    return newArr
}

时间复杂度

  • 复杂度的分析过程:

    • 假设数组长度为n,需要进行logn次归并操作
    • 每次归并操作需要o(n)的时间复杂度
    • 因此,归并排序的时间复杂度为O(nlogn)
  • 最好情况:o(log n)

    • 最好情况下,待排序数组已经是有序的了,那么每个子数组都只需要合并一次,即只需要进行一次归并操作。
    • 因此,此时的时间复杂度是O(log n)。
  • 最坏情况:O(nlogn)

    • 最坏情况下,待排序数组是逆序的,那么每个子数组都需要进行多次合并。
    • 因此,此时的时间复杂度为o(nlogn)。
  • 平均情况:O(nlogn)

    • 在平均情况下,我们假设待排序数组中任意两个元素都是等概率出现的。
    • 此时,可以证明归并排序的时间复杂度为O(nlogn)。

快速排序

基本思路

快速排序(Quick Sort)是一种基于分治思想的排序算法:

  • 基本思路是将一个大数组分成两个小数组 ,然后递归地对两个小数组进行排序
  • 具体实现方式是通过选择一个基准元素 (pivot),将数组分成左右两部分,左部分的元素都小于或等于基准元素,右部分 的元素都大于基准元素
  • 然后,对左右两部分分别进行递归调用快速排序,最终将整个数组排序。

实现步骤

  • 1.首先,我们需要选择一个基准元素 ,通常选择第一个或最后一个元素作为基准元素。
  • 2.然后,我们定义两个指针i和j,分别指向数组的左右两端。
  • 3.接下来,我们从右侧开始,向左移动j指针,直到找到一个小于或等于基准元素的值
  • 4.然后,我们从左侧开始,向右移动i指针,直到找到一个大于或等于基准元素的值
  • 5.如果i指针小于或等于j指针,交换i和j指针所指向的元素
  • 6.重复步骤3-5,直到i指针大于j指针 ,这时,我们将基准元素与i指针所指向的元素交换位置,将基准元素放到中间位置
  • 7.接着,我们将数组分为两部分,左侧部分包含小于或等于基准元素的元素,右侧部分包含大于基准元素的元素
  • 8.然后,对左右两部分分别进行递归调用快速排序,直到左右两部分只剩下一个元素
  • 9.最终,整个数组就变得有序

代码实现

时间复杂度:O(n logn)

scss 复制代码
// 第一次交换
function quickSort(arr){

    const n = arr.length

    //初始划分区域
    position(0, n-1)
    function position(left, right){
        // 1.找到基准元素
        const pivot = arr[right]
        // 2.双指针:目的是左边都是比pivot小的数字,右边都是比pviot大的数字
        let i = left
        let j = right - 1   // right是基准,前一个

        // 3.开始寻找对比
        while(arr[i] < pivot){
            // 找比pivot大的数,没有就继续++
            i++
        }
        while(arr[j] > pivot){
            // 找比pivot小的数,没有就继续--
            j--
        }

        // 来到这,说明我们已经找到了,然后交换,并继续++--
        if( i <= j){
            [ arr[i], arr[j] ] = [ arr[j], arr[i] ]
            i++
            j--
        }
    }
    
     
    return arr
}
scss 复制代码
//循环上述代码
function quickSort(arr){

    const n = arr.length

    //初始划分区域
    position(0, n-1)
    function position(left, right){

        // 结束条件
        if(left >= right) return

        // 1.找到基准元素
        const pivot = arr[right]
        // 2.双指针:目的是左边都是比pivot小的数字,右边都是比pviot大的数字
        let i = left
        let j = right - 1   // right是基准,前一个

        // 3.开始寻找对比:i>j的时候停止
        while(i <= j){
            while(arr[i] < pivot){
                // 找比pivot大的数,没有就继续++
                i++
            }
            while(arr[j] > pivot){
                // 找比pivot小的数,没有就继续--
                j--
            }
    
            // 来到这,说明我们已经找到了,然后交换,并继续++--
            if( i <= j){
                [ arr[i], arr[j] ] = [ arr[j], arr[i] ]
                i++
                j--
            }
        }

        // 4. 交换基准元素与i位置的元素
        [ arr[i], arr[right] ] = [ arr[right], arr[i] ]

        //5.继续划分(递归调用)
        position(left, i-1)
        // position(left, j)
        position(i+1, right)  // i位置是之前的基准元素换到这里,已经固定了

    }
    
     
    return arr
}

时间复杂度

  • 快速排序的时间复杂度主要取决于基准元素的选择、数组的划分、递归深度等因素。

  • 下面是快速排序的复杂度算法分析过程:

  • 最好情况:O(nlogn)

    • 当每次划分后,两部分的大小都相等,即基准元素恰好位于数组的中间位置,此时递归的深度为O(log n)
    • 每一层需要进行n次比较,因此最好情况下的时间复杂度为O(nlogn)。
  • 最坏情况:O(n^2)

    • 当每次划分后,其中一部分为空,即基准元素是数组中的最大或最小值,此时递归的深度为o(n)。
    • 每一层需要进行n次比较,因此最坏情况下的时间复杂度为0(n^2)。
    • 需要注意的是,采用三数取中法或随机选择基准元素可以有效避免最坏情况的发生。
  • 平均情况:O(nlogn)

    • 在平均情况下,每次划分后,两部分的大小大致相等,此时递归的深度为o(log n)
    • 每一层需要进行大约n次比较,因此平均情况下的时间复杂度为O(nlogn)。
  • 需要注意的是,快速排序是一个原地排序算法,不需要额外的数组空间。

堆排序

基本思路

  • 堆排序(Heap Sort)是堆排序是一种基于比较的排序算法,它的核心思想是使用二叉堆来维护一个有序序列。

    • 二叉堆是一种完全二叉树,其中每个节点都满足父节点比子节点大(或小)的条件。
    • 在堆排序中,我们使用最大堆来进行排序,也就是保证每个节点都比它的子节点大。
  • 堆排序和选择排序有一定的关系,因为它们都利用了"选择"这个基本操作。

    • 选择排序的基本思想是在待排序的序列中选出最小(或最大)的元素,然后将其放置到序列的起始位置。
    • 堆排序也是一种选择排序算法,它使用最大堆来维护一个有序序列,然后不断选择出最大的值。

实现步骤

在堆排序中,我们首先构建一个最大堆。

  • 然后,我们将堆的根节点(也就是最大值)与堆的最后一个元素交换 ,这样最大值就被放在了正确的位置上。
  • 接着,我们将堆的大小减小一,并将剩余的元素重新构建成一个最大堆
  • 我们不断重复这个过程,直到堆的大小为1。
  • 这样,我们就得到了一个有序的序列。

代码实现

堆排序的两大步骤:构建最大堆和排序

ini 复制代码
function foo(arr){
​
    const n = arr.length
    
    // 原地建堆
    const start = Math.floor( n / 2  - 1 )
    for(let i = start; i >= 0;i--){
        heapDown(arr, n, i)
    }
    
    // 思路
    // 1. 对最大堆进行排序操作
    // [ arr[0], arr[n-1] ] = [ arr[n-1], arr[0] ]
    // heapDown(arr, n-1, 0)
    // 2. 我们只需要n-1个了下一次,然后对n-1个的数组进行下滤操作
​
    for (let i = n-1; i >= 0; i--) {
        [ arr[0], arr[i] ] = [ arr[i], arr[0] ]
        heapDown(arr, i, 0)
    }
​
    return arr
}
​
/**
 * 
 * @param {*} arr 数组
 * @param {*} n 范围/长度
 * @param {*} index 对哪个位置间下滤
 */
function heapDown(arr, n, index){
    while( 2 * index + 1 < n ){
        let leftChildIndex = index * 2 + 1
        let rightChildIndex = leftChildIndex + 1
        let largerIndex = leftChildIndex
        if(rightChildIndex < n && arr[rightChildIndex] > arr[leftChildIndex]){
            largerIndex = rightChildIndex
        }
​
        if(arr[index] >= arr[largerIndex]){
            break;
        }
​
        [ arr[index], arr[largerIndex] ] = [ arr[largerIndex], arr[index] ]
​
        index = largerIndex
    }
}

时间复杂度

  • 堆排序的时间复杂度分析较为复杂,因为它既涉及到堆的建立过程 也涉及到排序过程。

  • 下面我们分别对这两个步骤的时间复杂度进行分析。

  • 步骤一:堆的建立过程

    • 堆的建立过程包括n/2次堆的向下调整操作,每次调整的时间复杂度logn,因此它的时间复杂度为o(n log n)。
  • 步骤二:排序过程

    • 排序过程需要执行n次堆的删除最大值操作,每次操作都需要将堆的最后一个元素与堆顶元素交换,然后向下调整堆。
    • 每次向下调整操作的时间复杂度为o(log n),因此整个排序过程的时间复杂度为o(n log n)。
  • 综合起来,堆排序的时间复杂度为o(n log n)。

  • 需要注意的是,堆排序的空间复杂度为o(1),因为它只使用了常数个辅助变量来存储堆的信息。

希尔排序

基本思路

实现步骤

代码实现

scss 复制代码
function shellSort(arr){
    const n = arr.length
    // 选择不同的增量/步长
    let gap = Math.floor(n/2)
​
    // 第一层循环:不断改变步长
    while( gap > 0){
        // 获取不同的gap,使用gap进行插入排序
​
        // 第二次循环:找到不同的数列集合进行插入排序
        for(let i = gap; i<n;i++){
            let j = i
            const num = arr[i]
            // 使用num向前去找一个比num小的值
            
            // 第三次循环:排序操作
            while(j > gap - 1 && num < arr[j-gap]){
                arr[j] = arr[j-gap]
                j = j-gap
            }
            arr[j] = num
        }
        gap = Math.floor(gap/2)
    }
}

时间复杂度

希尔排序的效率和增量是有关系的。,希尔排序使用原始增量,最坏的情况下时间复杂度为o(n^2), 通常情况下都要好于o(n^2)

相关推荐
a栋栋栋3 小时前
apifox
java·前端·javascript
请叫我飞哥@4 小时前
HTML 标签页(Tabs)详细讲解
前端·html
Anlici4 小时前
React18与Vue3组件通信对比学习(详细!建议收藏!!🚀🚀)
前端·vue.js·react.js
m0_748251525 小时前
PDF在线预览实现:如何使用vue-pdf-embed实现前端PDF在线阅读
前端·vue.js·pdf
中生代技术5 小时前
3.从制定标准到持续监控:7个关键阶段提升App用户体验
大数据·运维·服务器·前端·ux
m0_748239335 小时前
从零开始:如何在.NET Core Web API中完美配置Swagger文档
前端·.netcore
m0_748232925 小时前
【前端】Node.js使用教程
前端·node.js·vim
hawleyHuo5 小时前
umi 能适配 taro组件?
前端·前端框架
web130933203986 小时前
[JAVA Web] 02_第二章 HTML&CSS
java·前端·html
黑客呀6 小时前
Go Web开发之Revel - 网页请求处理流程
开发语言·前端·web安全·golang·系统安全