彻底搞懂插入排序、选择排序、冒泡排序及优化

915 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

插入排序、选择排序、冒泡排序都是两层遍历,时间复杂度都为O(n²);且都是操作原数组,无需新建额外数组内存空间,空间复杂度为O(1)

插入排序

  • 插入排序的思路是把同一数组分为有序和无序两部分,每次遍历,把无序部分的插入到有序部分中。如有下面数组,绿色部分代表有序部分,紫色代表无序部分

    image.png
    此时把元素12依次与45、42、23一一比较,一一互换位置,最后12比11大停止,所以插入到11与23之间。

    image.png

  • 代码

        let arr = [3, 1, 2, 4, 5, 6]
        const insertionSorter = function (arr) {
        // 默认数组第一个元素为有序的,所以从i=1开始为无序的
            for(let i = 1; i < arr.length; i++) { // 外层循环为从i=1开始的每个无序元素
                for(let j = i; j > 0; j--) { // 内层循环为单个无序元素与每个有序元素逐个比较,互换位置
                    if (arr[j] < arr[j - 1]) {
                        let temp = arr[j - 1]
                        arr[j - 1] = arr[j]
                        arr[j] = temp
                    } else {
                        break
                    }
                }
            }
        }
        insertionSorter(arr) // [1, 2, 3, 4, 5, 6]
    
  • 优化:元素互换位置的时候,每个元素都被访问了2次(arr[j-1], arr[j]),比较消耗性能。从这个角度优化的方式是,使用赋值移动的方式,有如下数组:绿色部分为有序,紫色为无序,当前待插入的元素为12,使用变量temp临时存储,

    image.png
    12首先与42比较,42向右移动(赋值) image.png
    12与23比较,23向右移动(赋值),再与16比较,向右移动(赋值)

    image.png
    循环结束后,插入所在索引位置 j

    image.png
    所以使用赋值(元素只访问一次)取代元素替换的方式减少访问次数,提升性能

        const insertionSorter = function (arr) {
            for(let i = 1; i < arr.length; i++) {
                // 记录待插入的元素
                let temp = arr[i]
                let j
                for(j = i; j > 0; j--) {
                    if (temp < arr[j - 1]) { // 待插入元素temp依次与前一个元素比较
                        arr[j] = arr[j - 1] // 较大的元素向右移动,元素只被访问一次
                    } else {
                        break
                    }
                }
                // 内层循环结束后,找到元素插入的位置
                arr[j] = temp
            }
        }
    

选择排序

  • 选择排序思路是在数组中找到当前数组最小值元素位置,然后与数组第一位交换,接着在除第一个元素外剩余数组中同样找到当前数组最小值,然后与数组第二位交换,以此类推,最后完成排序
        let arr = [3, 1, 2, 4, 5, 6]
        const selectionSorter = function (arr) {
            for(let i = 0; i < arr.length; i++) { // 外层循环控制每次交换位置
                let minIndex = i // 设置初始最小值索引
                for(let j = i + 1; j < arr.length; j++) { // 内层循环找到当前剩余元素最小值,所以剩余元素索引从 i+1 开始遍历
                    if (arr[j] < arr[minIndex]) {
                        minIndex = j
                    }
                }
                // 最小值与数组元素按i的顺序互换位置
                let temp = arr[i]
                arr[i] = arr[minIndex]
                arr[minIndex] = temp
            }
        }
        selectionSorter(arr) // [1, 2, 3, 4, 5, 6]
    
  • 优化(待更新)

冒泡排序

  • 冒泡排序的思路是数组中的元素要逐一的与其它元素比较,如果值比较大,则与其互换位置,然后再与下一个元素比较,如果还是更大,继续互换位置,以此类推,如果这个元素是最大的,那么此元素将“冒泡”到数组末尾,第一轮比较结束。按照这个逻辑,再把第二个元素依次与剩下的元素依次进行第二轮比较,然后第三轮,第四轮,直至数组元素遍历完,排序完成。
        let arr = [3, 1, 2, 4, 5, 6]
        const bubleSorter = function (arr) {
            for(let round = 1; round < arr.length; round++) { // round 为轮次
            let compareTimes = arr.length - round // 每轮比较的次数
                for(let i = 0; i < compareTimes; i++) { 
                    if (arr[i] > arr[i+1]) { // 如果值更大,那么与后一元素互换位置
                        let temp = arr[i]
                        arr[i] = arr[i+1]
                        arr[i+1] = temp
                    }
                }
            }
        }
        bubleSorter(arr) // [1, 2, 3, 4, 5, 6]
    
    
  • 优化:在一轮round遍历当中,如果没有执行一次元素位置,说明数组已经排序好了,则可提前终止遍历
        const bubleSorter = function (arr) {
            for(let round = 1; round < arr.length; round++) {
                let hasSwap = false // 标记此轮冒泡未发生交换
                let compareTimes = arr.length - round
                for(let i = 0; i < compareTimes; i++) {
                    if (arr[i] > arr[i+1]) {
                        let temp = arr[i]
                        arr[i] = arr[i+1]
                        arr[i+1] = temp
                        hasSwap = true // 如果发生交换,则为 true
                    }
                }
                if (!hasWeap) break // 退出遍历
            }
        }
    

  • 以数组长度为1000的乱序数组,从消耗时间角度,三种排序的性能高低排序分别是 插排 > 选排 > 冒泡排序
  • 如果存在多个值相等的元素,使用选择排序后,原位置可能会发生变化,如[2,1,1,3],排序后[1,1,2,3],两个1的先后位置可能发生变化,而冒泡排序和插入排序不变