此为原创文章, 分享请声明出处
题目: 数组旋转
要求:
定义一个函数,
传入一个数组 arr 和 需要旋转的步数 k,
把数组arr的后 k 位依次移动到数组前面
例子:
传入 arr = [1, 2, 3, 4, 5, 6, 7, 8], k = 3
得到的结果是: [6, 7, 8, 1, 2, 3, 4, 5]
相关的知识点引入
- 时间复杂度
- 空间复杂度
- 数组元素的存放方式
- Array.unshift() 方法的执行步骤
关于时间复杂度和空间复杂度的相关知识点解释起来比较麻烦, 本文就不在这里作过多的解释了, 请自行查阅资料或者参考以下文章
算法之空间复杂度、时间复杂度
十分钟弄懂:数据结构与算法之美 - 时间和空间复杂度
特别说明:
相比于空间复杂度, 前端开发的算法是极度重视时间复杂度
也就是大佬们常说的 重时间轻空间
为了方便本文理解, 在此放一张复杂度的图
横轴代表的是原始的数据量
纵轴代表的是复杂度的计算结果
数组元素的存放方式
JS中只有一种数组, 那就是索引数组(下标数组)
首先, 数组是一个有序结构, 数组在内存空间中开辟的是一个连续的内存空间
索引数组: 数组索引始终是数字 0 开始且添加到数组中的每个后续元素的索引以 1 为增量递增的数组。
索引数组的下标是有规律的: 从 0 开始, 依次递增 1
由此就可以看出来数组元素的存放方式:从第一个位置(下标)开始有序的往后排列
就好像我们排队的时候一样, 一个接着一个的排。
Array.unshift() 方法的执行步骤 - 描述
那么, 知道了数组元素是有序排列了之后, 不妨思考一下像 Array.unshift() 这类对数组最前面执行操作的方法会引发什么现象?
我举个例子帮助大家消化一下:
假设某个超市还没开门, 就有一队人在排队买东西了。排队的人, 是一个接着一个的排的, 然后突然在第一位的那里有个人插队进来。那么原本在排队的那些人, 是不是要一个一个依次往后移动一个位置呢?
这么一说就好理解了吧?
像 Array.unshift() 这类对数组最前面执行操作的方法, 就相当于刚刚例子中所说的插队行为。
在数组的最前面插入一个元素, 会导致从原本下标 0 位置开始一直到最末尾的元素,一个一个依次往后移动.
同理, 在数组的最前面删除一个元素, 会导致从原本下标 1 位置开始一直到最末尾的元素,一个一个依次往前移动.
这属于什么情况呢? 就是你看上去只操作了一个元素, 实际上整个数组都做了运算. 数组长度是多少, 就做了多少次运算。
Array.unshift() 方法的执行步骤 - 演示图
好了, 其他相关联的知识点都理解了之后, 我们该进入主题了。先上算法:
算法一 : 使用 pop 和 unshift
/**
* 时间复杂度: O(n²)
* 空间复杂度: O(1)
* 旋转数组 k 步 - 使用 pop 和 unshift
* @param arr array 需要旋转的数组
* @param k number 旋转的步数
* @returns array 处理后的数组
*/
export function rotate1(arr: number[], k: number):number[]{
const length = arr.length
if(!k || length === 0) return arr
const step = Math.abs(k % length)
for(let i = 0; i < step; i++){
const n = arr.pop()
if(n){
arr.unshift(n)
}
}
return arr
}
算法一 : 使用 pop 和 unshift - 分析
首先呢, 上述算法一的代码中的 18 行, arr.unshift(n), 每一次执行的时间复杂度就是 O(n) 了
数组长度是多少, 每次执行这句代码就运行了多少次
接着就是 15 行开始的 for 循环了, 这个循环的时间复杂度也是 O(n), 每次循环里面的 arr.unshift(n) 也是O(n)
所以算法一的时间复杂度是 O(n²)
由于算法一中, 并没有新生成或者定义和 arr 相关的变量, 所以算法一的空间复杂度是 O(1)
所以算法一中的时间复杂度是 O(n²), 空间复杂度是 O(1)
算法二 : 使用 concat 和 slice
/**
* 时间复杂度: O(1)
* 空间复杂度: O(n)
* 旋转数组 k 步 - 使用 concat 和 slice
* @param arr array 需要旋转的数组
* @param k number 旋转的步数
* @returns array 处理后的数组
*/
export function rotate2(arr: number[], k: number):number[]{
const length = arr.length
if(!k || length === 0) return arr
const step = Math.abs(k % length)
const part1 = arr.slice(-step)
const part2 = arr.slice(0, length - step)
const part3 = part1.concat(part2)
return part3
}
算法二 : 使用 concat 和 slice - 分析
首先呢, 算法二中没有循环, 也没有调用那些会造成执行次数较多的一些方法, 所以算法二中的时间复杂度是O(1)
我们再来看看算法二的空间复杂度
可以看到, 15,16,17 行代码,是基于原数组生成了三个新的数组, 我们就根据这个去进行分析。
假设我们现在arr的数据量是100,
那么 15, 16 行的数据量加起来就是 100,
然后 17 行的 part3 的数据量也是100,
那么 15,16,17 行加起来的数据量就是 200 ,
因为 100 和 200 实际上是同一个数量级的, 所以说算法二的空间复杂度是O(n)
所以算法一中的时间复杂度是 O(1), 空间复杂度是 O(n)
在前端开发过程中, 基于时间复杂度和空间复杂度我们要记住一点, 那就是重时间轻空间,
通过分析可以知道, 算法一的时间复杂度是根据原始数据量的大小,成一个很急的趋势在上涨的。
所以说,其实算法二要比算法一是要更加优秀的,
我们使用同样的原始数据量和相同的一个k,来看看这两个算法执行的时间差距有多大
测试示例:
/**
* 使用 20万 数据测试
* 两个算法的运行时间
*/
let dataArr:number[] = []
for(let i = 1; i <= 20 * 10000; i++){
dataArr.push(i)
}
console.time('rotate1')
rotate1(dataArr, 9 * 10000)
console.timeEnd('rotate1')
console.time('rotate2')
rotate2(dataArr, 9 * 10000)
console.timeEnd('rotate2')
执行结果:
执行了5次, 算法一每次的执行结果都需要 1190 毫秒左右
而算法二每次的执行时间都不超过5毫秒
同样的操作, 同样的数据量, 算法的不同, 程序运行时间的差距上可以说是天壤之别
我是前端开发者-罗公子
到此, 我的 这篇 基于 [时间复杂度] 和 [空间复杂度] 分析 练习题 [数组旋转] 的两个算法 已经完毕, 感谢您的耐心阅读.
如果您觉得我这篇文章写得不错, 可以关注一下和把文章分享出去哦.
再次声明, 此为原创文章, 分享请注明出处.