数据结构与算法 —— 算法概念与时间复杂度介绍


参考文章:
https://www.jianshu.com/p/f4cca5ce055a
https://blog.csdn.net/zolalad/article/details/11848739
https://juejin.im/post/5c11ac8c6fb9a049e412912d

1. 算法的特征

输入: 算法具有0个或多个输入
输出: 算法至少有1个或多个输出
有穷性: 算法在有限的步骤之后会自动结束而不会无限循环
确定性:算法中的每一步都有确定的含义,不会出现二义性
可行性:算法的每一步都是可行的,也就是说每一步都是有穷行不会死循环

算法是计算机处理信息的本质,算法是独立存在的一种解决问题的方法和思想。对于算法而言,实现的语言并不重要,重要的是思想。他就是告诉程序猿告诉计算机如何去执行。

2. 一个算法例子:

如果 a+b+c=1000,且 a^2 + b^2 = c^2 (a,b,c 为自然数),如何求出所有a、b、c可能的组合?

第一种思路:枚举

import time
start_time = time.time()
for a in range(0, 1001):
    for b in range(0, 1001):
        for c in range(0, 1001):
            if (a + b + c == 1000) and (a ** 2 + b ** 2 == c ** 2):
                print(a, b, c)

end_time = time.time()
time_elapsed = end_time-start_time

print("times: %d" % time_elapsed)  # 244秒

可以看到使用的时间比较长。

第二种方式

我们可以发现因为a+b+c=1000,c可以由a和b推导出来所以c就可以由a,b确定,我们就不用再循环c了

import time

start_time = time.time()
for a in range(0, 1001):
    for b in range(0, 1001):
        c = 1000 - a - b
        if a ** 2 + b ** 2 == c ** 2:
            print(a, b, c)

end_time = time.time()
time_elapsed = end_time - start_time

print("times: %d" % time_elapsed)  # 1秒

这个解决方式发现执行时间只有1秒

3. 算法的效率衡量

通过以上例子,我们可以看到:实现算法程序的执行时间可以反应出算法的效率,即算法的优劣吗。
但是不够客观,当你使用同一台机器的时候可以用执行时间来看算法的优劣,也就是相同的执行环境,但是如果在不同的机器上,执行相同代码时间就会不同。那么如果我们用算法的执行步骤来评判算法优劣就更加客观了。
因为一个算法,不管在哪里执行,他的执行步骤都是不变的,总步数相同。所以我们通过计算执行步数来评判算法的效率优劣,而不管他在哪个地方执行。

我们得出一个简单的结论:算法执行时间可以用执行次数表示。
所以我们就可以使用算法的执行代码语句的次数来评判算法的优劣性

这时我们引入时间复杂度,时间复杂度是由执行代码次数来推出来的,他和执行步骤次数直接相关,也就是说,可以通过时间复杂度来评判算法的效率。

4. 时间复杂度——大O表示法

通过第一个代码的例子

for a in range(0, 1001):
    for b in range(0, 1001):
        for c in range(0, 1001):
            if (a + b + c == 1000) and (a ** 2 + b ** 2 == c ** 2):
                print(a, b, c)

这里有三个循环,我们使用T(n)来表示代码执行次数。
a + b + c == 1000时:
执行次数:T = 1000 * 1000 * 1000 * 2
a + b + c == 2000时:
执行次数:T = 2000 * 2000 * 2000 * 2
a + b + c == n时:
执行次数:T = N * N * N * 2

这里的n称为算法的规模
此时
T(n) = n^3 * 2 (如果我们细分if里面的语句的话,可以分为执行10步也就是T(n) = n^3 * 10)
这时我们使用f(n)= n^3
可得T(n) = k * f(n)
但是这个是10或者是2都不太影响我们这个整体量级,因为他相比n^3 太小了,几乎不会影响f(n)的数量级。

此时为了 估算算法需要的运行时间简化算法分析,我们引入时间复杂度的概念。
算法的时间复杂度,用来度量算法的运行时间,
记作: T(n) = O(f(n))
所以T(n)执行步骤也就是时间复杂度,用大O表示法来表示。
它表示随着 输入大小n 的增大,算法执行需要的时间的增长速度可以用 f(n) 来描述。

计算时间复杂度

那么当我们拿到算法的执行次数函数 T(n) 之后怎么得到算法的时间复杂度呢?

  1. 基本操作,即只有常数项,认为其时间复杂度为O(1)
  2. 顺序结构,时间复杂度按加法进行计算
  3. 循环结构,时间复杂度按乘法进行计算
  4. 分支结构,时间复杂度取最大值,也就是取分支当中步数最大值

判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略
在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度

我们知道常数项对函数的增长速度影响并不大,所以当 T(n) = c,c 为一个常数的时候,我们说这个算法的时间复杂度为 O(1);如果 T(n) 不等于一个常数项时,可以忽略常数项。
比如:

int aFunc(void) {
    printf("Hello, World!\n");      //  需要执行 1 次
    return 0;       // 需要执行 1 次
}
  • T(n) = 2,所以我们说那个函数(算法)的时间复杂度为 O(1)。
  • T(n) = n + 29,此时时间复杂度为 O(n)。

我们知道高次项对于函数的增长速度的影响是最大的。n^3 的增长速度是远超 n^2 的,同时 n^2 的增长速度是远超 n 的。 同时因为要求的精度不高,所以我们直接忽略低此项。

  • T(n) = n^3 + n^2 + 29,此时时间复杂度为 O( n^3 )。

因为函数的阶数对函数的增长速度的影响是最显著的,所以我们忽略与最高阶相乘的常数。

  • T(n) = 3n^3,此时时间复杂度为 O( n^3 )。

举例计算

for a in range(0, 1001):
    for b in range(0, 1001):
        c = 1000 - a - b
        if a ** 2 + b ** 2 == c ** 2:
            print(a, b, c)

T(n) = n * n * (1 + max(1,0)) = n^2 * 2
T(n) = O(n^2)
所以时间复杂度为O(n^2)
可以看出这个方法的时间复杂度要比第一种算法好

总结:如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到简化(渐变)函数 f(n),此时算法的时间复杂度就是 O(f(n))。就是在f(n)前面加个大O表示时间复杂度。
其实由执行次数 T(n) 得到时间复杂度很简单,很多时候困难的是从算法通过分析和数学运算得到 T(n)

最坏时间复杂度

比如一个序列排序,如果是乱序的,我们的时间复杂度为n^2,也就是最坏时间复杂度。 而如果是顺序的则时间复杂度为n,也就是最优时间复杂度。

分析算法时,存在几种可能的考虑:
算法完成工作最少需要多少基本操作,即最优时间复杂度
算法完成工作最多需要多少基本操作,即最坏时间复杂度
算法完成工作平均需要多少基本操作,即平均时间复杂度

对于最优时间复杂度,其价值不大,因为它没有提供什么有用信息,其反映的只是最乐观最理想的情况,没有参考价值。
对于最坏时间复杂度,提供了一种保证,表明算法在此种程度的基本操作中一定能完成工作。
对于平均时间复杂度,是对算法的一个全面评价,因此它完整全面的反映了这个算法的性质。但另一方面,这种衡量并没有保证,不是每个计算都能在这个基本操作内完成。而且,对于平均情况的计算,也会因为应用算法的实例分布可能并不均匀而难以计算。

因此,我们主要关注算法的最坏情况,亦即最坏时间复杂度,通常我们说的时间复杂度,也就是最坏时间复杂度。

发布了103 篇原创文章 · 获赞 94 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/chongbin007/article/details/93565643