在学习数据结构阶段,认识的第一个概念应该就是时间复杂度了。时间复杂度是一个数学上的一个函数式,该函数式计算的基本执行次数,就是时间复杂度。通俗的说,时间复杂度就是衡量这个算法的运行时间长短,效率高低的一个手段,它是通过粗略的计算算法的执行次数来衡量的。那时间复杂度该怎么计算呢?

一、计算算法的时间复杂度:

下面先看这段代码:

//判断算法的时间复杂度
#include<stdio.h>

void Func1(int N)
{
    int count = 0;
    for (int i = 0; i < N; ++i)
    {
        for (int j = 0; j < N; ++j)
        {
            ++count;
        }
    }

    for (int k = 0; k < 2 * N; ++k)
    {
        ++count;
    }

    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d\n", count);
}

int main()
{
    int N = 0;
    scanf("%d", &N);
    Func1(N);
    return 0;
}

上面的代码中,Func1函数效率的高低,就能衡量这个函数的好坏。观察出该函数有四个循环,前两个循环是嵌套的形式,每个循环的循环次数是N次,所以一共是N×N次。第三个循环的循环次数是2×N次,第四个循环的循环次数是10次,所以该函数一共执行了N×N+2×N+10次,写成数学表达式就是Func1(N) = N² + 2×N + 10。

带入具体值是:

N值

准确值(Func1(N) = N² + 2 × N + 10)

估算值(Func1(N) = N²)

N = 1

Func1(N) = 13

Func1(N) = 1

N = 10

Func1(N) = 130

Func1(N) = 100

N = 100

Func1(N) = 10210

Func1(N) = 10000

N = 1000

Func1(N) = 1002010

Func1(N) = 1000000

N = 10000

Func1(N) = 100020010

Func1(N) = 100000000

N = 100000

Func1(N) = 10000200010

Func1(N) = 10000000000

N值越大,估算值与准确值之间相差的就越少,也就是说估算值就越接近准确值,那么就可以用估算值代替准确值,得到的执行次数就是算法Func1的大致执行次数。

实际上这样做有些过于细节了,时间复杂度求的是一个算法的大致运行次数,所以并不是精确到到底是多少次,那么时间复杂度就会存在着等级,比如O(1)、O(N)、O(2×N)、O(N²)、O(log₂N)、O(N!)、O(Nlog₂N)等,N就代表该算法大致执行了多少次,1代表该算法执行的是常数次,也就是确定的次数,这种表示的方法是大O渐进表示法。

上面的Func1函数的时间复杂度是O(N²),也就是大致执行了N²次,比如当N = 10时,Func1(N) = 100,这显然是一个大致的数字,之所以舍弃后面的次数,是因为当N无限大时,能决定具体的执行次数的是N²,不是2×N+10,所以会舍弃后面的执行次数,这点在当N无限大时就很明显。

如果算法执行次数是确定的呢,那时间复杂度又是多少?其实,不管这个确定的执行次数有多大,时间复杂度都是O(1),“1”也代表常数次,比如下面的代码:

//判断算法的时间复杂度
#include<stdio.h>

void Func3(int N)
{
    int count = 0;
    for (int k = 0; k < 100; ++k)
    {
        ++count;
    }
    printf("%d\n", count);
}

int main()
{
    int N = 0;
    scanf("%d", &N);
    Func3(N);
    return 0;
}

代码就是循环100次,所以时间复杂度是O(1)。

只要是O(1)就说明该算法执行次数是常数次,不管确定的常数值多大或多小都是O(1)。这是因为CPU的处理速度已经非常快了,所以即使几十亿次的执行,也不过是几秒钟而已,当然如果一个O(1)的算法需要执行几百万亿甚至几千万亿次,这也需要很长时间的,即使这种算法的时间复杂度是O(1),实际上也是不太可能存在的。

时间复杂度大小关系为:O(log₂N) < O(N) < O(Nlog₂N) < O(N²) < O(2^N) < O(N!)。

二、时间复杂度的量级:

从上面的分析就可以看出,时间复杂度是分等级的,最快的也是最优的是O(1)、O(log₂N),其次是O(N)、O(Nlog₂N),这些时间复杂度都是比较不错的。然后是O(N²),这种时间复杂度的算法效率就很低了,在实际生活中一般是不用的,最后是O(N!),这种时间复杂度的算法也就是看看而已,很少很少存在,因为它的效率实在是太低了。

只是这么说可能不太直观的感受,下面使用具体数值举例,时间复杂度每个量级之间的差距:

O(log₂N)

O(N)

O(Nlog₂N)

O(N²)

O(2^N)

O(N!)

1

1

1

1

1

1

7

100

700

10,000

2^100

100!

14

10,000

140,000

100,000,000

没有计算必要了

没有计算必要了

17

100,000

1,700,000

10,000,000,000

没有计算必要了

没有计算必要了

20

1,000,000

20,000,000

没有计算必要了

没有计算必要了

没有计算必要了

24

10,000,000

240,000,000

没有计算必要了

没有计算必要了

没有计算必要了

27

100,000,000

2,700,000,000

没有计算必要了

没有计算必要了

没有计算必要了

30

1,000,000,000

30,000,000,000

没有计算必要了

没有计算必要了

没有计算必要了

这下就可以看出效率了,时间复杂度为O(N)的算法还算可以,之后的算法就比较拉胯了,到了O(N²)的算法基本没人使用了。

三、求时间复杂度时的预期管理:

有如下程序:

//判断算法的时间复杂度
#include<stdio.h>

const char* strchr(const char* str, int character);

int main() {}

上面的函数没有具体的实现,也就不知道是执行常数次还是N次,也就是说,最好的情况是执行1次(常数次),最坏的情况是执行N次,平均的情况(最常见的情况)是执行次数在区间[2,N-1]之之间,那就不知道这个算法到底执行多少次,这时可以采用预期管理,按照最坏的情况考虑。

遇到不确定的执行次数时,均按照最坏情况考虑。

所以上面算法的时间复杂度是O(N)。

四、求算法的时间复杂度的方法:

1、最简单的情况,直接数循环次数即可,根据循环次数判断是在哪个量级。

2、大部分的算法都需要特殊考虑,到底执行次数是在哪个量级,比如下面的代码:

//判断算法的时间复杂度
#include<stdio.h>

long long Fib(size_t N)
{
    if (N < 3)
        return 1;
    return Fib(N - 1) + Fib(N - 2);
}

int main()
{
    size_t N = 0;
    scanf("%d", &N);
    Fib(N);
    return 0;
}

这个算法看起来很简单,功能是求第N个斐波那契数,但是想要计算出时间复杂度还是比较复杂的。首先算法中含有函数递归,每一次函数递归都会开辟一个函数栈帧,这也算执行了一次,那问题就转换成这个算法到底递归了多少次即可。

函数递归过程如下:

Fib(N)函数递归过程:
                                                             Fib(N)                                                                  2º
                            Fib(N-1)                                              Fib(N-2)                                     2¹
           Fib(N-2)                Fib(N-3)                   Fib(N-3)                  Fib(N-4)                     2²
Fib(N-3)  Fib(N-4)    Fib(N-4)  Fib(N-5)    Fib(N-4)  Fib(N-5)    Fib(N-5)  Fib(N-6)            2³
                                                                 ...                                                                      ...
                                                               Fib(5)
                               Fib(4)                                                       Fib(3)
                 Fib(3)                return = 1                     return = 1           return = 1 
return = 1       return = 1

从上面的递归过程发现,前期的调用大概是按照2ⁿ的形式调用的,后面当N<3时,就会返回1。所以如果是按照量级看来,大致计算执行次数应该是2º + 2¹ + 2² + 2³ + ... + 2^(n - 1) = 2ⁿ - 1。所以时间复杂度就是O(2ⁿ)。

错位相减计算法:
S(n) = 2º + 2¹ + 2² + 2³ + ... + 2^(n - 1)
2×S(n) = 2¹ + 2² + 2³ + ... + 2ⁿ
可知S(n) = 2ⁿ - 2º = 2ⁿ - 1。

还有一种求法,就是画出前几个递归图后,就大概判断出执行次数差不多是2ⁿ,少算的那部分根本不用管,这样就推测出时间复杂度是O(2ⁿ)了,这个也是求算法的时间复杂度中比较难的一个了。

以上就是我这次分享的内容啦!写的不好,请多担待!