关于动态数组的Java实现,参考Github代码:Interview/Data Structures/Array/
在这篇文章中,将针对数组中各方法进行时间复杂度的分析,以及对均摊复杂度进行介绍,同时会对代码中可能出现的复杂度震荡进行剖析,并提供思路进行解决(主要是针对resize操作)
resize的复杂度分析——均摊复杂度 amortized time complexity
对于添加操作来说,最坏的情况是addLast(e)的时候,也需要进行resize,那么复杂度就是O(n)级别的了。但是我们忽略了个问题:我们根本不可能每次操作的时候都会触发resize,因此我们使用最坏的情况分析添加操作的时间复杂度是不合理的
17次基本操作包含了9次添加操作 + 8次元素转移操作
平均,每次addLast操作,进行2次基本操作( 17/9 约等于2 )
假设capacity=n,n+1次addLast,触发resize,总共进行2n+1次基本操作
平均,每次addLast操作,进行2次基本操作( 2n+1/n+1 约等于 2 )
将1次resize的时间平摊给了n+1次addLast的时间,于是得到了平均每次addLast操作进行2次基本操作的结论
这样均摊计算,时间复杂度是O(1)级别的,这和我们数组中有多少个元素是没有关系的
在这个例子里,这样均摊计算,比计算最坏情况是有意义的,这是因为最坏的情况是不会每次都出现的。
关于均摊复杂度,其实在很多算法书中都不会进行介绍,但是在实际工程中,这样的一个思想是蛮有意义的:就是一个相对比较耗时的操作,如果我们能保证他不会每次都被触发的话,那么这个相对比较耗时的操作它相应的时间是可以分摊到其它的操作中来的。
同理,我们看removeLast操作,均摊复杂度也为O(1)
resize的复杂度分析——复杂度震荡
但是,当我们同时看addLast和removeLast操作的时候:
假设我们现在有一个数组,这个数组的容量为n,并且现在也装满了元素,那么现在我们再调用一下addLast操作,显然在添加一个新的元素的时候会需要扩容(扩容会耗费O(N)的时间),之后我们马上进行removeLast操作(根据我们之前的逻辑,在上一个操作里通过扩容,容量变为了2n,在我们删除1个元素之后,元素又变为了n = 2n/2,根据我们代码中的逻辑,会触发缩容的操作,同样耗费了O(n)的时间);那么我们如果再addLast、removeLast…等相继依次操作
对于addLast和removeLast来说,都是每隔n次操作都会触发resize,而不会每次都触发
但是现在我们制造了一种情景:同时看addLast和removeLast的时候,每一次都会耗费O(n)的复杂度,那么这就是复杂度的震荡。
resize的复杂度分析——出现复杂度震荡的原因及解决方案
removeLast时resize过于着急(采用了Eager的策略: 一旦我们的元素变为当前容积的1/2的时候,我们马上就把当前的容积也缩容为1/2)
解决方案: Lazy (在线段树中,也会用到类似的思路)
当元素变为当前容积的1/2时,不着急把当前容积缩容,而是等等;如果后面一直有删除操作的话,当删除元素到整个数组容积的1/4时,那么这样看来我们的数组确实用不了这么大的容积,此时我们再来进行缩容,缩容整个数组的1/2(这样,即便我们要添加元素,也不需要马上触发扩容操作)
当 size == capacity / 4时,才将capacity减半
————————————————
版权声明:本文为CSDN博主「疯狂呼呼呼」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lemonzhaotao/article/details/80379525
均摊复杂度和防止复杂度的震荡