前言
上回我们讲到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(),对有序固定存储的数据结构比如数组当中元素进行操作时,一定要慎之又慎