100道前端面试题(二):把一个数组旋转 k 步

167 阅读3分钟

前言

上回我们讲到js的内存泄漏的场景,今天我们来看一道算法题:把一个数组旋转 k 步


请输入一个数组和一个K值(int),若k为正值,则将数组arr从第arr[k]位开始颠倒过来;如果k为负值,则将数组从第arr[arr.length + k]为颠倒过来

请看示例1:

示例 1 :

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

示例 2 :

输入: 
arr2 = [1, 2, 3, 4, 5, 6, 7]
k = -3
输出: [5, 6, 7, 1, 2, 3, 4]

示例 3 :

输入:
arr3 = [1, 2, 3, 4, 5, 6, 7]
k = 6
输出:[7, 6, 5, 4, 3, 2, 1] 
解释:若K值为0,则返回原数组

当数组为空或者k值为非int类型时,返回原数组

当k值的绝对值大于等于数组长度时,返回原数组

当k值的为正无穷、负无穷或者NaN时,返回原数组

解题思路


我们首先排除掉特殊情况(k为特殊值或者数组为特殊值情况)


        if (length == 0 || k == 0) {
                    return arr
                }   else if (k == Infinity || k == -Infinity || isNaN(k)) {
                    return arr
                } else if (typeof k != "number") {
                    return arr
                } 

排除之后,我们使用slice()函数将传入的数组,从第arr[k]为拆分,成为两个新的数组,然后分别创建两个变量,接受这些数据,并且使用concat函数将他们呢拼接起来

let n = Math.abs(k)
        n = Math.trunc(n)
        const arr1 = arr.slice(0, n + 1)
        const arr2 = arr.slice(n + 1, length)
        const arr3 = arr2.concat(arr1)

下面是全部代码:

    export function rotate1(arr: number[], k: number): number[] {
            let length = arr.length
            if (length == 0 || k == 0) {
                return arr
            }   else if (k == Infinity || k == -Infinity || isNaN(k)) {
                return arr
            } else if (typeof k != "number") {
                return arr
            } 
            let n = Math.abs(k)
            n = Math.trunc(n)
            const arr1 = arr.slice(0, n + 1)
            const arr2 = arr.slice(n + 1, length)
            const arr3 = arr2.concat(arr1)
            return arr3
    }

算法分析

其实这道题相当简单,我们真正要研究的是,为什么我们不使用以下这种方案来解决该问题

    export function rotate2(arr: number[], k: number): number[] {
        const length = arr.length
        if (!k || length === 0) return arr
        const step = Math.abs(k % length) // abs 取绝对值

        // O(n^2)
        for (let i = 0; i < step; i++) {
            const n = arr.pop()
            if (n != null) {
                arr.unshift(n) // 数组是一个有序结构,unshift 操作非常慢!!! O(n)
            }
        }
        return arr
    }

我们先给出一个结论:

在方法一中,时间复杂度为(1),空间复杂度为O(n)

在方法二中,时间复杂度为O(n^2),空间复杂度为O(1)

性能测试

        // const arr1 = []
        // for (let i = 0; i < 10 * 10000; i++) {
        //     arr1.push(i)
        // }
        // console.time('rotate1')
        // rotate1(arr1, 9 * 10000)
        // console.timeEnd('rotate1') // 885ms O(n^2)

        // const arr2 = []
        // for (let i = 0; i < 10 * 10000; i++) {
        //     arr2.push(i)
        // }
        // console.time('rotate2')
        // rotate2(arr2, 9 * 10000)
        // console.timeEnd('rotate2') // 1ms O(1)

为什么会出现这样的情况?

这是因为我们的操作的数据是数组,数组作为一个有序数据结构,通过索引号查询本身元素时,速度很快,然而对其中的元素进行移动时,就慢到了极点,比如我们数组自带的方法unshift()和shift()。假设数组中有n个元素,则使用unshift()和shift()进行元素位置的改动时,要对每个元素都进行移动,因此时间复杂度为O(n),在我们的方法二中,unshift()方法本身还嵌套在一个for循环中,因此,方法二最后的时间复杂度为O(n^2),这在看重时间复杂度,看轻空间复杂度的前端程序里,简直是无法容忍的错误。

方法一的空间复杂度是O(n),因为我们在方法中创建了太多数组,用于存储数据。

至于方法一的时间复杂度为什么是O(1),原因在于,slice()方法根本不会对原数组进行任何修改,他做的只是根据数组索引号,对数组里的内容进行快速拷贝(前面我们说过,得益于有序的存储结构,数组的查询非常快速),进而生成新数组。

单元测试

为了更好的提升我们的代码规范性,我们最好养成写单元测试的习惯,以下是本题的单元测试和测试环境 ts单元测试:

    import { rotate1, rotate2 } from './array-rotate'
    describe('数组旋转', () => {
    it('正常情况', () => {
    const arr = \[1, 2, 3, 4, 5, 6, 7]
    const k = 3
    const res = rotate1(arr, k)
    expect(res).toEqual(\[5, 6, 7, 1, 2, 3, 4]) // 断言
    })

        it('数组为空', () => {
            const res = rotate1([], 3)
            expect(res).toEqual([]) // 断言
        })

        it('k 是负值', () => {
            const arr = [1, 2, 3, 4, 5, 6, 7]
            const k = -3

            const res = rotate1(arr, k)
            expect(res).toEqual([5, 6, 7, 1, 2, 3, 4]) // 断言
        })

        it('k 是 0',  () => {
            const arr = [1, 2, 3, 4, 5, 6, 7]
            const k = 0

            const res = rotate1(arr, k)
            expect(res).toEqual(arr) // 断言
        })

        it('k 不是数字', () => {
            const arr = [1, 2, 3, 4, 5, 6, 7]
            const k = 'abc'

            // @ts-ignore
            const res = rotate1(arr, k)
            expect(res).toEqual(arr) // 断言
        })

        it('k 为正无穷', () => {
            const arr = [1, 2, 3, 4, 5, 6, 7]
            const k = Infinity

            // @ts-ignore
            const res = rotate1(arr, k)
            expect(res).toEqual(arr) // 断言
        })

        it('k 为负无穷', () => {
            const arr = [1, 2, 3, 4, 5, 6, 7]
            const k = -Infinity

            // @ts-ignore
            const res = rotate1(arr, k)
            expect(res).toEqual(arr) // 断言
        })

        it('k 为NaN', () => {
            const arr = [1, 2, 3, 4, 5, 6, 7]
            const k = NaN

            // @ts-ignore
            const res = rotate1(arr, k)
            expect(res).toEqual(arr) // 断言
        })

    })

测试所需要的环境变量:

{
      "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"
      }
 }

小结

  • 在前端开发中,更多的倾向于时间复杂度较低的算法
  • 在我们使用一些方法比如unshift()和shift(),对有序固定存储的数据结构比如数组当中元素进行操作时,一定要慎之又慎