JavaScript排序算法专题

63 阅读5分钟

金克丝.jpg

基础排序

冒泡排序

最好时间复杂度O(n),最坏时间复杂度O(n^2),平均复杂度O(n^2)。

// 常规解法
function bubbleSort (nums) {
    const len = nums.length
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - 1; j++) {
            if (nums[j] > nums[j + 1]) {
                [ nums[j], nums[ j + 1 ] ] = [ nums[ j + 1 ], nums[j] ]
            }
        }
    }
    return nums
}
// 优化:外层轮次到第 i 次时,代表已经有 i 个元素已经冒泡完成。内层比对可以进行。
function bubbleSort (nums) {
    const len = nums.length
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                [ nums[j], nums[ j + 1 ] ] = [ nums[ j + 1 ], nums[j] ]
            }
        }
    }
    return nums
}
// 优化:针对本身数组就是有序数组时,一个轮次就知道是否有序。
function betterBubbleSort (nums) {
    const len = nums.length
    let flag = false
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                // 发生交换则标志非有序
                flag = true
                [ nums[j], nums[ j + 1 ] ] = [ nums[ j + 1 ], nums[j] ]
            }
        }
        // 一轮后若标志未发生变化则代表数组本身有序
        if (!flag) {
            return nums
        }
    }
    return nums
}

选择排序

循环遍历数组,每次找出范围内的最小值,把它放在当前范围内的头部,然后缩小范围,继续上述描述,直至数组有序。内外层循环都不可变避免,三个时间复杂度都为O(n^2)。

function selectSort (nums) {
    const len = nums.length
    // 记录当前区间的最小值索引
    let minIndex
    for (let i = 0; i < len - 1; i++) {
        // 初始化最小值为当前区间第一个元素
        minIndex = i
        for (let j = i; j < len; j++) {
            if (nums[j] < nums[minIndex]) {
                minIndex = j
            }
        }
        // 如果最小值不是当前区间的第一个元素,则与第一个元素发生交换
        if(minIndex !== i) {
            [ nums[i], nums[minIndex] ] = [ nums[minIndex], nums[i] ]
        }
    }
    return nums
}

插入排序

基于当前元素前面的序列是有序的前提,从后往前去寻找当前元素在该序列中的正确位置。最好时间复杂度为O(n),最坏时间以及平均时间复杂度为O(n^2)。

function insertSort (nums) {
    const len = nums.length
    // 缓存当前需要去插入的元素
    let temp
    // 默认第一个元素有序,从第二个开始插入
    for (let i = 1; i < len; i++) {
        // 用 j 来标记该元素的正确位置
        let j = i
        temp = nums[i]
        // 判断前一个元素如果比当前插入元素大,则让位,再往前对比
        while (j > 0 && nums[j - 1] > temp) {
            nums[j] = nums[j - 1]
            j--
        }
        // 对比结束后 j 的位置就是该插入元素的正确位置
        nums[j] = temp
    }
    return nums
}

非基础排序

掌握分治的思想,将大问题分解为若干子问题分别求解,再子问题的解整合为大问题的解。

归并排序

归并排序的时间复杂度 O(nlog(n))。

  • 分解子问题:将需要被排序的数组从中间分割为两半,然后再将分割出来的每个子数组各分割为两半,重复以上操作,直到单个子数组只有一个元素为止。
  • 求解每个子问题:从粒度最小的子数组开始,两两合并、确保每次合并出来的数组都是有序的。(这里的“子问题”指的就是对每个子数组进行排序)。
  • 合并子问题的解,得出大问题的解:当数组被合并至原有的规模时,就得到了一个完全排序的数组。
function mergeSort (nums) {
    const len = nums.length
    // 处理边界情况
    if (len <= 1) {
        return nums
    }
    let mid = Math.floor(len / 2)
    // 递归分割左子数组,然后合并为有序数组
    const leftNums = mergeSort(nums.slice(0, mid))
    // 递归分割右子数组,然后合并为有序数组
    const rightNums = mergeSort(nums.slice(mid))
    // 返回合并后的有序数组
    return mergeNums(leftNums, rightNums)
}

function mergeNums (nums1, nums2) {
    let i = 0, j = 0
    let res = []
    const len1 = nums1.length
    const len2 = nums2.length
    while (i < len1 && j < len2) {
        if ( nums1[i] < nums2[j] ) {
            res.push(nums1[i])
            i++
        } else {
            res.push(nums2[j])
            j++
        }
    }
    if (i < len1) {
        return res.concat( nums1.slice(i) )
    } else {
        return res.concat( nums2.slice(j) )
    }
}

快速排序

快排仍然是分治的思想,区别于归并排序,快排是在原数组内部排序,不会真正的分割数组再合并数组。

function quickSort (nums, left = 0, right = nums.length - 1) {
    // 定义递归边界
    while (nums.length > 1) {
        const lineIndex = partition(nums, left, right)
        // 左子数组长度大于 1, 则递归排序
        if (left < lineIndex - 1 ) {
            quickSort(nums, left, lineIndex - 1)
        }
        // 右子数组长度大于1, 则递归排序
        if (right > lineIndex) {
            quickSort(nums, lineIndex, right)
        }
    }
    return nums
}

// 以基准值为轴心 划分左右子数组的过程
function partition (nums, left, right) {
    // 基准值默认选取中间元素
    const pivotValue = nums[ Math.floor(left + (rigth + left) / 2) ]
    // 初始化左右指针
    let i = left
    let j = right
    while (i <= j) {
        // 左指针所指元素小于基准值
        while (nums[i] < pivotValue) {
            i++
        }
        // 右指针所指元素大于基准值
        while (nums[j] > pivotValue) {
            j++
        }
        // 寻找到打破两个while循环的元素,进行交换
        if (i <= j) {
            swap(nums, i, j)
            i++
            j--
        }
    }
    return i
}
// 交换数组元素的方法
function swap(arr, i, j) { [arr[i], arr[j]] = [arr[j], arr[i]] }

快速排序的时间复杂度的好坏,是由基准值来决定的。

  • 最好时间复杂度:它对应的是这种情况——我们每次选择基准值,都刚好是当前子数组的中间数。这时,可以确保每一次分割都能将数组分为两半,进而只需要递归 log(n) 次。这时,快速排序的时间复杂度分析思路和归并排序相似,最后结果也是 O(nlog(n))。
  • 最坏时间复杂度:每次划分取到的都是当前数组中的最大值/最小值。大家可以尝试把这种情况代入快排的思路中,你会发现此时快排已经退化为了冒泡排序,对应的时间复杂度是 O(n^2)。
  • 平均时间复杂度: O(nlog(n))