排序算法(上)

79 阅读4分钟

选择排序

核心思想:数组有 n 个元素,进行 n 次循环,每次循环会从数组中找到最小的那个元素,然后将其交换到数组前面来,经过 n 次循环,数组就是从小到大排序好了。

function selectionSort(arr, n) {
    for(let i = 0; i < n; i++) {
        // 最小元素所在区间:[i, n)
        let minIndex = i
        for(let j = i + 1; j < n; j++) {
            if(arr[j] < arr[minIndex]) {
                minIndex = j
            }
        }
        let temp = arr[i]
        arr[i] = arr[minIndex]
        arr[minIndex] = temp
    }
}

两轮 for 循环,外层 for 循环有一个 minIndex 用来记录当前最小元素的索引,内层循环找到比当前最小元素还要小的元素并更新最小索引,内层循环结束后,minIndex 就是数组最小元素的索引了,将最小元素交换到前边来。

插入排序

核心思想:默认数组第一个元素是有序的,外层循环从第二个元素开始向后遍历,内层循环从当前 i 位置逐个与前一个元素作比较,只要比它小就做一次交换,直到当前元素比前面位置元素要大,那当前元素就插入当前的这个位置。

function insertionSort(arr, n) {
    for(let i = 1; i < n; i++) {
        for(let j = i; j > i; j--) {
            if(arr[j] < arr[j - 1]) {
                let temp = arr[j]
                arr[j] = arr[j - 1]
                arr[j - 1] = temp
            }
        }
    }
}

这里有简写的地方,将判断内容作为 for 循环的条件,如下代码:

function insertionSort(arr, n) {
    for(let i = 1; i < n; i++) {
        for(let j = i; j > i && arr[j] < arr[j - 1]; j--) {
            let temp = arr[j]
            arr[j] = arr[j - 1]
            arr[j - 1] = temp
        }
    }
}

最优的情况,每次内层循环只要执行一次,那就是当前元素直接大于上一个元素了,说明前边的元素已经有序,不需要继续执行循环。

当然还有性能优化的版本,就是上边内层循环当前元素不断与前一个元素做比较,比它小就做交换,最坏情况数组最后一个元素是数组中最小的那个,那就要与前边所有元素交换一次了。所以我们优化点就是,找到当前元素合适的插入位置后再将元素插入进行,核心比较逻辑没变,代码如下:

// 插入排序优化版
export function insertionSort1(arr, n) {
    for(let i = 1;i < n; i++) {
        // 默认要插入的元素
        let e = arr[i]
        // j 保存 e 要插入的位置
        let j
        for(j = i;j > 0 && arr[i] < arr[j - 1]; j--) {
            arr[j] = arr[j - 1]
        }
        arr[j] = e
    }
}

冒泡排序

核心思想:每次循环 n - 1 轮,每轮都会将当前元素与上一个元素做比较,通过设置一个标志位来判断当前数组是否有序了,如果有序了循环就停止,而且还有一个优化点,那就是每次 for 循环结束都会将一个最大元素冒到最后,此时 n-- 后,下次循环就不用考虑最后一个元素了。

function buddleSort(arr, n) {
    let swapped
    do() {
        swapped = false
        for(let i = 1; i < n; i++) {
            if(arr[i - 1] > arr[i]) {
                let temp = arr[i]
                arr[i] = arr[i - 1]
                arr[i - 1] = temp
                // 将 swapped 设为 true 表示当次有进行交换
                swapped = true
            }
        }
        // 优化:每次循环结束后数组最后一个元素就是最大的了,下次循环不用考虑了
        n--
    } while(swapped)
}

通过 swapped 标志位来判断当次循环是否有交换元素,如果有交换的话说明当前数组可能还不是有序的,设置 swapped 为 true,这里有优化的点就是 n-- 是因为每次循环能将最大一个元素冒到数组的最后,下次循环不用考虑了。如果当次循环没有元素交换,说明当前数组已经有序了,那么 swapped 为 false 循环就终止了。

封装交换逻辑

上边选择/插入/冒泡排序都有交换元素这一逻辑,我们可以把这一部分抽成一个交换函数

export function swap(arr, i, j) {
    let temp = arr[i]
    arr[i] = arr[j]
    arr[j] = temp
}

总结

选择排序:不管数组有没有序,每次内层循环都要完整执行,无法提前终止,效率低下。

插入排序:最优情况下,内层循环只要执行一次。

冒泡排序:最优情况下,数组完全有序,只需在 do 循环体执行一遍即可。