数据规模
时间复杂度
并不是所有的双层循环都是O(n^2)的
复杂度实验来确定复杂度
// O(N) 两倍增加
int findMax( int arr[], int n ){assert( n > 0 );int res = arr[0];for( int i = 1 ; i < n ; i ++ )if( arr[i] > res )res = arr[i];return res;}
随后,O(n^2),数据规模乘二,时间复杂度乘4……
随着数据的增加,可以看到O(logN)
递归算法时间复杂度分析
不是有递归的函数就一定是O(nlogn)
深入:主定理
resize的复杂度分析——均摊复杂度 amortized time complexity
均摊分析和平均情况时间复杂度,前者是一个序列的操作取平均值,后者是针对不同输入来计算平均值
动态数组(Vector)每一个操作增加一个元素,删除一个元素相应的复杂度,就需要Amortized Time
动态栈,动态队列类似(数组)
对于添加操作来说,最坏的情况是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减半