时间复杂度基本计算规则:
- 基本操作即只有常数项,认为其时间复杂度为O(1)
- 顺序结构,时间复杂度按加法进行计算
- 循环结构,时间复杂度按乘法进行计算
- 分支结构,时间复杂度取最大值
- 判断一个算法效率时,往往只需要关注操作数量的最高次项,其他次要项和常数项可以忽略
- 在没有特殊说明时,我们所分析的时间复杂度都是指最坏时间复杂度
单层循环时间复杂度计算
例题分析
例一:
i = n*n; whlie(i != 1) i = i/2;
我们发现,循环执行的条件是i!=1,然后循环体中i=i/2;改变了i的值。
我们列出循环体执行次数t 和i的最终值(即执行完t次循环后的值)的关系
循环体执行次数t 0 1 2 3 i的改变量i
第二步:找到t的最终值与i的关系:
第三步:确定循环停止条件:
第四步:联立第二步第三步两式解方程:
两边对2取对数得
所以得到时间复杂度为:
例二:
x = 0; while (n>=(x+1)*(x+1)) x = x+1;
第一步:列出循环趟数t及x的最终值(即执行完t次循环后的值):
循环体执行次数t 0 1 2 3 x的最终值x 0 1 2 3
第二步:找到t与x的最终值关系:
第三步:确定循环停止条件:
第四步:联立第二步第三步两式解方程:
所以得到时间复杂度为:
例三:
int i = 1; while (i<=n) i = i *2
第一步:列出循环趟数t及i的最终值(即执行完t次循环后的值):
循环趟数t 0 1 2 3 i的最终值 1 2 4 8
第二步:找到t与i的关系:
第三步:确定循环停止条件:
第四步:联立第二步第三步两式解方程:
所以得到时间复杂度为:
例四:
int i = 0; while (i*i*i<=n) i ++;
第一步:列出循环趟数t及i的最终值:
循环趟数t 0 1 2 3 i的最终值i 0 1 2 3
第二步:找到t与i的关系:
第三步:确定循环停止条件:
第四步:联立第二步第三步两式解方程:
所以得到时间复杂度为:
例五:
y = 0; while (y+1)*(y+1) <= n) y = y+1;
第一步:列出循环趟数t及y的最终值:
循环趟数t 0 1 2 3 y的最终值 0 1 2 3
第二步:找到t与y的关系:第三步:确定循环停止条件:
第四步:联立第二步第三步两式解方程:
所以得到时间复杂度为:
两层循环时间复杂度计算
对于两层循环时间复杂度的计算,我们完全可以将这个循环视作一个单层循环,然后它的循环体是另一个循环。这样子计算起时间复杂度就很简单了
例题分析
例一:
int m=0,i,j; for (i=1;i<=n;i++) for(j=1;j<=2*i;j++) m++;
第一步列出是第n次大循环:第二步列出在第n次大循环中内循环层语句能执行的次数:
第几次大循环 1 2 3 …… n 第n次大循环中内循环语句的执行次数 2 4 6 …… 2n 注意上面这个都是每次大循环对应的内部循环语句执行次数,整个语句的时间复杂度还需要把每次大循环中内循环语句执行的次数相加起来
注意是将内循环语句次数相加
第三步 求和,写结果
例二:
for (i=0;i<n;i++) for(j=0;j<m;j++) a[i][j] = 0;
第一步列出第n次大循环:
第二步列出第n次大循环中内层语句的执行次数:
第n次大循环 1 2 3 ... n 第n次大循环中内层循环语句的执行次数 m m m m m 注意上面这个都是每次大循环对应的内部循环语句执行次数,整个语句的时间复杂度还需要把每次大循环中内循环语句执行的次数相加起来
第三步 求和,写结果注意是将内循环语句次数相加
例三:
count = 0; for (k=1;k<=n;k*=2) for(j=1;j<=n;j++) count ++;
第一步列出第a次大循环:(注意这里k*=2)
第二步列出第a次大循环中内层语句的执行次数:
由于这里大循环能执行的次数不是一眼就能看出来的,所以还得先计算能执行多少次大循环
假设能执行t次大循环,则
第a次大循环 1 2 3 …… k的值 1 2 4 …… n 第a次大循环中内层语句的执行次数 n n n n n 内层每个都是n,求和则可以得到:
注意是将内循环语句次数相加
内循环语句总的执行次数是
例四:
for (i=n-1;i>=1;i--) for(j=1;j<=i;j++) if (A[j] > A [j+1]) {//时间复杂度为O(1)的语句 }
第一步列出第t次大循环:
第二步列出第t次大循环中内层语句的执行次数:
第t次大循环 1 2 3 ... n-1 第t次大循环中内层循环语句的执行次数 n-1 n-2 n-3 ... 1 第三步 求和,写结果
注意是将内循环语句次数相加
在本例中它就是个等差数列求和
所以时间复杂度就是
多层循环时间复杂度计算
实际上无论多少层循环,我们只需一层一层分开来计算就行了,最后只需将最内层的语句的执行次数全加起来就好了
例一:
for(i=0;i<=n;i++)
for(j=0;j<=i;j++)
for(k=0;k<j;k++)
要计算这个嵌套循环的时间复杂度,我们首先要分析每个循环的执行次数。
外层循环 for(i=0;i<=n;i++)
从 0
到 n
,因此会执行 n+1
次。
对于每一个 i
的值,内层循环 for(j=0;j<=i;j++)
的执行次数从 0
到 i
。具体来说:
- 当
i = 0
时,内层循环执行1
次(j
从0
到0
)。 - 当
i = 1
时,内层循环执行2
次(j
从0
到1
)。 - 当
i = 2
时,内层循环执行3
次(j
从0
到2
)。 - ...
- 当
i = n
时,内层循环执行n+1
次(j
从0
到n
)。
因此,内层循环的总执行次数是 1 + 2 + 3 + ... + (n+1)
,这是一个等差数列的和,其和为 (\frac{(n+1)(n+2)}{2})。
对于每一个 j
的值,最内层的循环 for(k=0;k<j;k++)
的执行次数是从 0
到 j-1
。这实际上是一个等差数列的前 j
项和,但我们要注意,这个内层循环对于每一个 j
都会执行,所以我们需要将它与 j
的所有可能值相乘。
具体来说,当 j
分别取 0, 1, 2, ..., n
时,最内层循环的执行次数分别是 0, 1, 3, ..., n(n-1)/2
。因此,最内层循环的总执行次数是这些值的和。
为了计算这个总和,我们可以观察到一个事实:每一个 k
值在 j
从 k+1
到 n
的过程中都会被计算一次。因此,k=0
会被计算 n
次,k=1
会被计算 n-1
次,以此类推,直到 k=n-1
只被计算 1
次。
这个总和是另一个等差数列的和,其和为
因此,整个嵌套循环的时间复杂度是外层循环次数 n+1
乘以最内层循环的总执行次数 (\frac{n(n-1)}{2}),即
简化后得到时间复杂度为 (O(n^3))。这是因为尽管有系数和较低阶的项,但在大 n
的情况下,(n^3) 项将主导整个表达式,因此时间复杂度是立方级的。
常见时间复杂度计算举例
实例1:
// 计算Func2的时间复杂度?
void Func2(int N)
{ int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{ ++count; }
int M = 10;
while (M--)
{ ++count; }
printf("%d\n", count);
}
实例1基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)
实例2
// 计算Func3的时间复杂度?
void Func3(int N, int M)
{ int count = 0;
for (int k = 0; k < M; ++ k)
{ ++count; }
for (int k = 0; k < N ; ++ k)
{ ++count; }
printf("%d\n", count);
}
实例2基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)
实例3
// 计算Func4的时间复杂度?
void Func4(int N)
{ int count = 0;
for (int k = 0; k < 100; ++ k)
{ ++count; }
printf("%d\n", count);
}
实例3基本操作执行了10次,通过推导大O阶方法,时间复杂度为 O(1)
实例4
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
实例4基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)
实例5
// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{ assert(a);
for (size_t end = n; end > 0; --end)
{ int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{ Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
实例5基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最坏,时间复杂度为 O(N^2)
实例6
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
while (begin < end)
{ int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid;
else return mid;
}
return -1;
}
实例6基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN).。ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。(建议通过折纸查找的方式讲解logN是怎么计算出来的)
实例7
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{ if(0 == N)
return 1;
return Fac(N-1)*N; }
实例7通过计算分析发现基本操作递归了N次,时间复杂度为O(N)
实例8
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2); }
实例8通过计算分析发现基本操作递归了2^N次,时间复杂度为O(2^N)。(建议画图递归栈帧的二叉树 讲解)