100道前端面试题(六): 用 TS 实现快速排序(单元测试)

197 阅读3分钟

前言

6-1.jpg

今天我们来看看快速排序


请输入一个数组,采取快速排序的方法对数组进行升序排序

请看示例1:

示例 1 :

输入: 
arr1 = [1, 6, 2, 7, 3, 8, 4, 9, 5] 
输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]

示例 2 :

输入: 
arr2 = [-2, 2, -3, 1]
输出: [-3, -2, 1, 2]

示例 3 :

输入:
arr3 = [2, 2, 2, 2]
输出:[2, 2, 2, 2] 

解题思路

1.找到中间位置midValue(二分法思想)

2.遍历数组,小于midValue的值放在left,否则放在right(小于中间值的在左,大于中间值的在右)

3.继续递归,最后concat拼接,返回数组(递归结束条件是传入的数组为空)

方法一(使用slice)

export function quickSort2(arr : number[]): number[] {
    // 递归跳出递归循坏的条件(分别建立两个数组)
    // 一个存储在每次递归中小于这个中间值的数,一个存储在每次递归中大于这个中间值的数
    const length = arr.length
    if (length === 0) return arr
    // 向上取整,取得中间值索引号
    const midIndex = Math.floor(length / 2)
    // 根据索引号取得中间值,记住一定要加[0],因为只是数组后面无法参与大小于逻辑判断
    const midValue = arr.slice(midIndex, midIndex + 1)[0]
    
    const left: number[] = []
    const right: number[] = []
     
    for (let i = 0; i < length; i++) {
        const n = arr[i]
        // 为什么一定要设置if (n < midValue)?
        if (i !== midIndex) {
            if (n < midValue) {
                // 小于 midValue ,则放在 left
                left.push(n)
            } else {
                // 大于 midValue ,则放在 right
                right.push(n)
            }
        }
      
    }
    
    return  quickSort2(left).concat(
        [midValue],
        quickSort2(right)
    )
}

为什么上述代码中一定要设置if (n < midValue)? 如果改成

     if (n < midValue) {
     // 小于 midValue ,则放在 left
         left.push(n)
     } else if(n > midValue) {
     // 大于 midValue ,则放在 right
     right.push(n)
     }

那么当数组元素都为一样时,left和right便不会接收到任何元素,那么最后返回的数组中,只会出现一个中间值元素

return  quickSort2(left).concat(
            [midValue],
            quickSort2(right)
        )
        // [ 2 ]

方法二(使用splice)

export function quickSort1(arr: number[]): number[] {
    const length = arr.length
    if (length === 0) return arr

    const midIndex = Math.floor(length / 2)
    const midValue = arr.splice(midIndex, 1)[0]

    const left: number[] = []
    const right: number[] = []

    // 注意:这里不用直接用 length ,而是用 arr.length 。因为 arr 已经被 splice 给修改了
    for (let i = 0; i < arr.length; i++) {
        const n = arr[i]
        if (n < midValue) {
            // 小于 midValue ,则放在 left
            left.push(n)
        } else {
            // 大于 midValue ,则放在 right
            right.push(n)
        }
    }

    return quickSort1(left).concat(
        [midValue],
        quickSort1(right)
    )
}

算法时间度分析

先给出我们的结论:方法一和方法二的时间复杂度基本接近,都为O(n * log(n)),我们再来逐步分析原因

1.有二分数组的算法,时间复杂度为O(log(n))

if (n < midValue) {
         // 小于 midValue ,则放在 left
             left.push(n)
         } else if(n > midValue) {
         // 大于 midValue ,则放在 right
         right.push(n)
         }

2.其次算法中含有一个常规for循环嵌套时间复杂度O(n)

3.因此最后的复杂度两者都为O(n * log(n))

只有推论是站不住脚的,因此我们会给出相关的性能测试结果

为什么splice()没有很明显影响性能

一般地,在算法中,都要尽量使用像slice()这种不改变原数组的方法,而非splice()这种会修改原数组的方法,但是在我们这回的算法中

  • 首先,算法本身时间复杂度就高达O(n * log(n)),splice()与slice()之间的性能不同,就被过高的时间复杂度所掩盖
  • 其次,splice()方法是在二分法之后进行的,二分会快速削弱数量级
  • 最后,当单独比较splice()与slice()之间的性能时,会发现它们的性能差距十分明显

测试环节

性能测试

/ // 性能测试
//  const arr1 = \[]
//  for (let i = 0; i < 10 \* 10000; i++) {
//      arr1.push(Math.floor(Math.random() \* 1000))
//  }
//  console.time('quickSort1')
//  quickSort1(arr1)
//  console.timeEnd('quickSort1') // 74ms

//  const arr2 = \[]
//  for (let i = 0; i < 10 \* 10000; i++) {
//      arr2.push(Math.floor(Math.random() \* 1000))
//  }
//  console.time('quickSort2')
//  quickSort2(arr2)
//  console.timeEnd('quickSort2') // 82ms

通过性能测试我们可以看到,使用splice与slice方法的性能十分接近,符合我们对时间复杂度的推测

单元测试

TS本身就自带类型约束,因此我们不需要考虑数组中,有元素不为number类型的情况

import { quickSort1, quickSort2 } from './quick-sort.copy'

describe('快速排序', () => {
it('正常情况', () => {
const arr = \[1, 6, 2, 7, 3, 8, 4, 9, 5]
const res = quickSort2(arr)
expect(res).toEqual(\[1, 2, 3, 4, 5, 6, 7, 8, 9])
})
it("有负数", () => {
const arr = \[-2, 2, -3, 1]
const res= quickSort2(arr)
expect(res).toEqual(\[-3, -2, 1, 2])
})

    it("数组元素一致", () => {
        const arr = [2, 2, 2, 2]
        const res= quickSort2(arr)
        expect(res).toEqual([2, 2, 2, 2])
    })
    it('空数组', () => {
        const res = quickSort2([])
        expect(res).toEqual([])
    })

})

测试所需要的环境变量:

{
      "name": "interview-js-code",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "jest --detectOpenHandles",
        "dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.dev.js",
        "build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.js",
        "build:analyzer": "cross-env NODE_ENV=production_analyzer webpack --config build/webpack.prod.js"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@babel/core": "^7.13.14",
        "@babel/preset-env": "^7.13.12",
        "@types/jest": "^27.0.2",
        "autoprefixer": "^10.2.5",
        "babel-jest": "^27.3.0",
        "babel-loader": "^8.2.2",
        "clean-webpack-plugin": "^3.0.0",
        "cross-env": "^7.0.3",
        "css-loader": "^5.2.0",
        "html-webpack-plugin": "^5.3.1",
        "jest": "^27.3.0",
        "less": "^4.1.1",
        "less-loader": "^8.0.0",
        "postcss-loader": "^5.2.0",
        "style-loader": "^2.0.0",
        "ts-loader": "^8.1.0",
        "typescript": "^4.2.3",
        "url-loader": "^4.1.1",
        "webpack": "^5.30.0",
        "webpack-bundle-analyzer": "^4.4.0",
        "webpack-cli": "^4.6.0",
        "webpack-dev-server": "^3.11.2",
        "webpack-merge": "^5.7.3",
        "ts-jest": "^27.0.7"
      }
 }