面试必会面试题之手写冒泡、选择、插入排序

166 阅读8分钟

作者:前端实习生鲸落

博客:鲸落 (xiaojunnan.cn)

Github:鲸落(github.com)

冒泡排序

基本思路

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

实现步骤

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

代码实现

时间复杂度:O(n2)

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
}
// 性能优化版本:当我们在循环的时候,某一次循环,发现没有发生交换的时候,这个排序就已经结束了,不需要再继续下去消耗性能了
​
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)

// 先找到最小的排到第一个
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
}
// 循环上述步骤
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)。

插入排序

image-20230626133845534.png

基本思路

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

实现步骤

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

代码实现

时间复杂度:O(n2)

// 内循环
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
}
// 循环上述步骤
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)。

补充

后续会补充归并排序、快排序、堆排序和希尔排序

其他文章推荐:

浏览器输入url会发生什么 - 巨详细完整版 - 掘金 (juejin.cn)

浏览器输入url会发生什么 - 巨详细完整版续集 - 掘金 (juejin.cn)

你真的了解宏任务与微任务的执行顺序吗?你确定是微任务先执行吗? - 掘金 (juejin.cn)

强缓存、协商缓存,居然还有启发式缓存?? - 掘金 (juejin.cn)

原来V8引擎和js的执行原理这么简单呀! - 掘金 (juejin.cn)