时间复杂度
大O的渐进表示法
大O符号:适用于描述函数渐进行为的数学符号。
推导大O阶的方法:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶。
- 如果最高阶存在且不是1,则去除与这个项目相乘的常数(系数)。
得到结果就是大O阶。
常见类型时间复杂度
常数循环时间复杂度
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)。
双重(多重)循环时间复杂度
O(n + m + k······)
嵌套循环时间复杂度
如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 或O(n*n).
strchar的时间复杂度
const char * strchr ( const char * str, int character);
/*while(*str)
{
if(*str == character)
return str;
else
++str;
}*/
在时间复杂度计算中,通常假设数组(整型数组和字符数组)或者是字符串的长度是N,和通常使用大小N来表示某种未知(不定)的大小,例如:
有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界) N
平均情况:任意输入规模的期望运行次数 N/2
最好情况:任意输入规模的最小运行次数(下界) 1
当一个算法随着输入的不同,时间复杂度不同,时间复杂度做悲观预期,看最坏的情况。
冒泡排序时间复杂度
for(i = 0; i < n - 1; i++)
{
int j;
for(j = 0; j < n - 1 - i; j++)
{
......
}
}
冒泡排序的时间复杂度,如果通过冒泡排序来排n个数字的话,就需要n-1趟冒泡,外层for循环的次数已经确定是n-1次,
但是内层for循环的次数是不断变换的,当i=0时,内层for循环第一次循环的次数是n-1次,,当i=1时,内层for循环第二次循环的次数是n-2次,所以,内层for循环的次数是不断在改变的,不能两个for循环次数直接相乘。
当for循环嵌套的时候,如果内外for循环的循环次数都是定值的时候,才可以直接相乘得到执行次数,再通过大O方法推导出时间复杂度。
嵌套循环如果有一层循环的循环次数在不断改变的时候,一般都是外层循环次数是定值,内层循环次数会不断改变,这样的话就不可以直接相乘得到执行次数。
如果通过冒泡排序来排n个数的话,外层for循环需要循环n-1次,内层for循环每次循环n-1-i 次,总的执行次数
T(N)=n-1 + n-2 +n-3 + … 1 ,
一共是n-1项相加, 这n-1项代表的是外层for循环的次数,每一项代表的是每一次外层for循环对应的内层for循环的次数。通由等差数列求和公式可知,
T(N)=((n-1+1)( n-1 ) )/2
最高次数项就是(n^2)/2
最终的时间复杂度就是O(n^2)。
二分法查找时间复杂度
在二分查找中,最好的情况就是查找一次就找到了,执行次数是1,则时间复杂度就是O(1),默认数组的长度为N,遍历的时候,则最坏情况就是执行N次,但二分查找,不是暴力查找,时间复杂度不可能是O(N)
对于二分查找,最坏的情况就是两端中间只有一个数字了,,由于二分查找是对半查找,最坏就剩一个数字,反推则有:1 * 2 * 2…*2=N,,假设乘了x个2,则有
2^x=N,
x=log以2为底的N次方
时间复杂度就是O(log以2为底的N次方)简写O(logN)。
二分查找很牛逼,但需要先排序。
阶乘递归的时间复杂度
关于递归求时间复杂度需要掌握两个结论:
1、如果每次函数调用中总的执行次数和递归参数无关,则递归算法的时间复杂度= 递归的次数 x 每次递归函数中的时间复杂度。
2、如果每次函数调用中总的执行次数和递归参数有关,则递归算法的时间复杂度就等于所有的递归调用中执行次数的累加。
在递归算法中,只考虑能递归的部分,即考虑N>=2的时候,对于N<2的情况,当做递归结束的标志。
斐波那契递归的时间复杂度
计算斐波那契数列的时间复杂度的公式:递归算法的时间复杂度= 递归的次数 *每次递归函数中的时间复杂度.
//计算斐波那契递归Fib的时间复杂度
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
2^ Fib(N)
2^1 Fib(N-1) Fib(N-2)
2^2 Fib(N-2)Fib(N-3) Fib(N-3) Fib(N-4)
Fib(2)
2^n-1 Fib(1) Fib(2) 右边一些递归会提前结束
Fib(N) = 20+21+22+…+2(N-1)-X( 缺一些递归调用)
=2^N-1-X
O((2^N-1-X)*1) = O(2^N)