1 复杂度分析
1.1 时间复杂度
时间复杂度用来表示算法运行时间的长短,用来定性的描述程序的运行时间。要了解时间复杂度,我们需要先了解程序执行的次数。
1.1.1 循环执行次数
(1)
for (int i=0; i<n; i++)
{
printf("%d\n", i);
}
//该代码段内,for() 语句将被执行 n+1 次,而不是 n 次,当 i=n-1 时,循环正常运行,当 i=n 时,for() 仍需要判断 i<n 条件是否成立,不成立,跳出 for() 循环,最后一次判断导致 for() 语句的执行次数是 n+1 次,而不是 n 次。
//printf()只有在符合 i<n 条件时,才会执行printf语句,所以执行次数为n,而不是n+1。
(2)
for (int i=0; i<n; i++)
{
for (int j=0; j<n; j++)
{
printf("i:%d,j:%d\n", i ,j);
}
}
//该代码段内,for(i=0, i<n, i++)外部循环语句将执行 n+1 次。for(j=0 , j<n, j++)将被执行 n(n+1) 次。
//printf()将执行 n*n 次。
(3)
for (i=1; i<=n; i++)
{
for (j=1; j<=i; j++)
{
for (k=1; k<=j; k++)
{
printf("i:%d, j:%d, k:%d \n", i, j , k);
}
}
}
//该代码段内,从内到外思考 printf() 的循环次数,内部第一层循环执行 j 次,第二层循环,执行次数是 j 分别取值 0~i 的求和。可得 printf() 的执行次数为:
∑ i = 1 i = n ∑ j = 1 j = i j = 1 / 6 n ( n + 1 ) ( 2 n + 1 ) \sum_{i=1}^{i=n}\sum_{j=1}^{j=i} j = 1/6n(n+1)(2n+1) i=1∑i=nj=1∑j=ij=1/6n(n+1)(2n+1)
这里有些小伙伴会疑惑,为什么就成累加了呢?我们只考虑内部的 j 和 k 两层循环,带入具体的值想一下:
假设 j 的最大值是5,j 会从 1 一直累加到 5 ,那么 j = 1 时,k <= 1,此时printf会被执行一次,之后内部循环就不满足循环条件退出了,而外部循环中的 j 就被累加到了 2 ,此时 k <= 2,此时printf会被执行两次,然后跳出循环,以此类推,执行的次数就是 1+ 2 + 3+ 4 + 5。
(4)
for (i=1; i<=n; i=i*2)
{
printf("Hello World\n");
}
//该代码段内,i=i*2 我们设执行次数为k,所以跳出循环的判断条件可以简化表示为:
2 k − 1 > n 即 k = l o g 2 ( n + 1 ) 2^k-1 > n 即 k = log_2 (n+1) 2k−1>n即k=log2(n+1)
1.1.2 大O(n)表示法
(1)对于以上四个循环例子,当 n 趋于无限大时,执行次数的系数和常数项可以忽略不计,因此,以上四个例子的 printf() 语句的时间复杂度用 O(n) 可以表示为:
-
O ( n ) O(n) O(n)
-
O ( n 2 ) O(n^2) O(n2)
-
O ( n 3 ) O(n^3) O(n3)
-
O ( l o g 2 ( n ) ) O(log_2 (n)) O(log2(n))
(2)常见的时间复杂度,以及当 n 区域无穷大时的效率:
(3)在用 O(n) 描述算法的时间复杂度时,我们往往一般说的是平均时间复杂度,除此之外,我们还需要考虑,最差时间复杂度,和最好时间复杂度。
1.2 空间复杂度
即算法占用内存空间的大小。与时间复杂度不同,我们通常只关注最差空间复杂度。因为内存空间是硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。
空间复杂度的推算方法与时间复杂度大致相同,只需将统计对象从 “操作数量” 转为 “使用空间大小” 我们举几个例子。
-
O ( 1 ) :与循环无关的变量 O(1):与循环无关的变量 O(1):与循环无关的变量
-
O ( l o g 2 ( n ) ) :例如递归树,分治算法 O(log_2 (n)):例如递归树,分治算法 O(log2(n)):例如递归树,分治算法
-
O ( n ) :例如数组,链表,栈,队列 O(n):例如数组,链表,栈,队列 O(n):例如数组,链表,栈,队列
-
O ( n 2 ) :例如二维数组 O(n^2):例如二维数组 O(n2):例如二维数组
-
O ( l o g 2 ( n ) ) :例如二叉树 O(log_2 (n)):例如二叉树 O(log2(n)):例如二叉树