四种排序算法的JS实现

194 阅读3分钟

本文讨论四种常见的排序算法:选择排序、快速排序、归并排序、计数排序。代码用 JS 实现。

目标:考虑一个由数字组成的一维数组,对其中的数字由小到大排列。

一、选择排序 Selection Sort

1.1 递归写法

选择排序递归写法思路:
每次找到数组中余下数字中最小的,放到前面来,然后对余下数字做同样操作,以此类推。

let min = numbers => {
    if (numbers.length > 2) {
        return min([numbers[0], min(numbers.slice(1))])
    } else {
        return Math.min.apply(null, numbers)
    }
}

let minIndex = numbers => {
    return numbers.indexOf(min(numbers))
}

let sort = numbers => {
    let index = minIndex(numbers)
    let min = numbers[index]
    if (numbers.length > 1) {
        numbers.splice(index, 1) // 从 numbers 里删掉 min
        return [min].concat(sort(numbers))
    } else { // 递归出口设置为数组长度等于1,多递归了一次而已
        return numbers
    }
}

let result = sort([53, 22, 1])
console.log(result)

1.2 循环写法

选择排序循环写法思路:
从左到右遍历数组,每次找到余下数字中最小的,和当前正在查看的数字的索引对比,只要索引不同,就交换两者位置。然后对余下数字做相同操作。

let minIndex = numbers => {
    let index = 0
    for (let i = 1; i < numbers.length; i++) {
        if (numbers[i] < numbers[index]) {
            index = i
        }
    }
    return index
}

let swap = (array, i, j) => {
    let temp = array[i]
    array[i] = array[j]
    array[j] = temp
}

let sort = numbers => {
    for (let i = 0; i < numbers.length - 1; i++) {
        console.log('-----')
        console.log(`i:${i}`)
        let index = minIndex(numbers.slice(i)) + i
        console.log(`index:${index}`)
        console.log(`min:${numbers[index]}`)
        if (index !== i) { swap(numbers, index, i) }
    }
    return numbers
}

let result = sort([1, 199, 27, 93, 124, 4903, 4])
console.log(result)

1.3 时间复杂度

O(n²)

推导:(n-1) + (n-2) + (n-3) + 2 + 1 = (n²-n)/2

二、快速排序 Quick Sort

2.1 递归写法

快速排序递归写法思路:
指定一个基准(一般指定中间的数字),比它小的移到前面,比它大的移到后面。对基准两侧的数字继续执行快速排序,以此类推。
特点:基准指在哪,谁的位置就确定了。

let quickSort = arr => {
    if (arr.length <= 1) { return arr }
    let pivotIndex = Math.floor(arr.length / 2)
    let pivot = arr.splice(pivotIndex, 1)[0]
    let left = [], right = []
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    return quickSort(left).concat([pivot], quickSort(right))
}

let result = quickSort([4, 99, 2, 0, 88, 67, 35, 28, 19, 9])
console.log(result)

2.2 时间复杂度

O(nlog₂n)

推导:假设 1000 个数,第一次确定基准后,遍历 999 次并移动位置(基准的位置不用动),此时基准前面的数字都比它小,基准后面的数字都比它大。这一次遍历,共执行1000次,只是基准的位置没有动。后面还需要遍历 log₂1000 次,每次都是执行1000次,共计 1000 * log₂1000 次,即 nlog₂n 次

image.png

三、归并排序 Merge Sort

归并排序思路:左边一半排好序,右边一半排好序,然后把左右两边合并。

3.1 递归写法

merge 采用递归写法:
两个指针只比每组的第一个。

/**
 * @description 两个指针只比每组的第一个
 * @param arr1 已经排好序的数组
 * @param arr2 已经排好序的数组
 */
let merge = (arr1, arr2) => {
    if (arr1.length === 0) { return arr2 }
    if (arr2.length === 0) { return arr1 }
    if (arr1[0] < arr2[0]) {
        return [arr1[0]].concat(merge(arr1.slice(1), arr2))
    } else {
        return [arr2[0]].concat(merge(arr1, arr2.slice(1)))
    }
}

let mergeSort = numbers => {
    if (numbers.length === 1) {
        return numbers
    }

    let midIndex = Math.floor(numbers.length / 2)
    let halfLeft = numbers.slice(0, midIndex)
    let halfRight = numbers.slice(midIndex)
    return merge(mergeSort(halfLeft), mergeSort(halfRight))
}


// 测试用例 1
let numbers = [12, 3, 7, 21, 5, 9, 4, 6]
// 测试用例 2
// let numbers = [12, 3, 7, 21, 5, 9, 4, 6,1]

let result = mergeSort(numbers)
console.log(result)

3.2 循环写法

merge 采用循环写法思路:
两组指针从左到右依次移动,哪个小就 push 到新数组。直到其中一个指针到达终点,循环结束。

/**
 * @description 两个指针从0开始,按从小到大进行合并
 * @param arr1 已经排好序的数组
 * @param arr2 已经排好序的数组
 */
let merge = (arr1, arr2) => {
    let result = []
    for (let i = 0, j = 0; (i < arr1.length) || (j < arr2.length);) {
        if (arr1[i] === undefined && arr2[j]) {
            result.push(arr2[j])
            j++
            continue
        } else if (arr2[j] === undefined && arr1[i]) {
            result.push(arr1[i])
            i++
            continue
        }
        if (arr1[i] < arr2[j]) {
            result.push(arr1[i])
            i++
        } else {
            result.push(arr2[j])
            j++
        }
    }
    return result
}


let mergeSort = numbers => {
    if (numbers.length === 1) {
        return numbers
    }

    let midIndex = Math.floor(numbers.length / 2)
    let halfLeft = numbers.slice(0, midIndex)
    let halfRight = numbers.slice(midIndex)
    return merge(mergeSort(halfLeft), mergeSort(halfRight))
}

// 测试用例 1
let numbers = [12, 3, 7, 21, 5, 9, 4, 6]
// 测试用例 2
// let numbers = [12, 3, 7, 21, 5, 9, 4, 6,1]

let result = mergeSort(numbers)
console.log(result)

3.3 时间复杂度

O(nlog₂n)

image.png

四、计数排序 Counting Sort

计数排序思路:
用一个哈希表做记录。发现数字 N 就记 N:1,如果再次发现 N 就加 1。记录的同时挑选并记下数字的最大值。 最后把哈希表的 key 全部装进新数组中,假设 N:m,那么 N 要装载 m 次。

let countingSort = arr => {
    let hashTable = {}, min = 0, max = 0, result = []
    for (let i = 0; i < arr.length; i++) {
        if (!(arr[i] in hashTable)) {
            hashTable[arr[i]] = 1
        } else {
            hashTable[arr[i]] += 1
        }
        min = arr[i] < min ? arr[i] : min
        max = arr[i] > max ? arr[i] : max
    }
    // 遍历哈希表
    for (let j = min; j <= max; j++) {
        if (j in hashTable) {
            for (let i = 0; i < hashTable[j]; i++) { // 解决数字出现多次的情况
                result.push(j)
            }
        }
    }
    return result
}

// 测试用例 1
// let numbers = [12, 12, 12, 3, 4, 89, 23, 1, 1, 2, 2, 2]
// 测试用例 2
let numbers = [-12, -12, -12, 3, -4, -89, -23, -1, 1, 2, 2, 2]
let result = countingSort(numbers)
console.log(result)

特点:使用了哈希表的数据结构,「用空间换时间」;只遍历一次数组,之前算法都要遍历好几遍。

注意事项:写代码时,到底写的是 key 还是 value 是易错点。

4.1 时间复杂度

O(n+max-min)

相比于以上三种,计数排序时间最快,「用空间换时间」。

五、时间复杂度对比

算法时间复杂度
选择排序O(n²)
快速排序O(nlog₂n)
归并排序O(nlog₂n)
计数排序O(n+max-min)

六、其他排序算法

  • 冒泡排序 Bubble Sort
  • 插入排序 Insertion Sort
    天生就会。扑克牌起牌过程中,每个人给扑克牌排序,就是用的插入排序。
  • 希尔排序 Shell Sort
  • 基数排序 Radix Sort
    按个位、十位、百位...依次排序

七、学习资源与工具

  1. visualgo.net/zh 通过可视化的方式观看排序过程。