算法的引入
我们举一个可能不太恰当的例子: 如果将最终写好运行的程序比作战场,我们码农便是指挥作战的将军,而我们所写的代码便是士兵和武器。 那么数据结构和算法是什么?答曰:兵法! 我们可以不看兵法在战场上肉搏,如此,可能会胜利,可能会失败。即使胜利,可能也会付出巨大的代价。我们写程序亦然:没有看过数据结构和算法,有时面对问题可能会没有任何思路,不知如何下手去解决;大部分时间可能解决了问题,可是对程序运行的效率和开销没有意识,性能低下;有时会借助别人开发的利器暂时解决了问题,可是遇到性能瓶颈的时候,又不知该如何进行针对性的优化。 如果我们常看兵法,便可做到胸有成竹,有时会事半功倍!同样,如果我们常看数据结构与算法,我们写程序时也能游刃有余、明察秋毫,遇到问题时亦能入木三分、迎刃而解。 故,数据结构和算法是一名程序开发人员的必备基本功,不是一朝一夕就能练成绝世高手的。冰冻三尺非一日之寒,需要我们平时不断的主动去学习积累。
算法的概念:
算法是计算机处理信息的本质,因为计算机程序本质上是一个算法来告诉计算机确切的步骤来执行一个指定的任务。一般地,当算法在处理信息时,会从输入设备或数据的存储地址读取数据,把结果写入输出设备或某个存储地址供以后再调用。 算法是独立存在的一种解决问题的方法和思想。 对于算法而言,实现的语言并不重要,重要的是思想。 算法可以有不同的语言描述实现版本(如C描述、C++描述、Python描述等),我们现在是在用Python语言进行描述实现。
算法的五大特性:
- 输入: 算法具有0个或多个输入
- 输出: 算法至少有1个或多个输出
- 有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
- 确定性:算法中的每一步都有确定的含义,不会出现二义性
- 可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成
我们先来看一道题吧:
如果 a+b+c=1000,且 a^2+b^2=c^2(a,b,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):
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()
print('run: %.6f' %(end_time-start_time))
emmm…… 然后经过漫长的等待,程序最终运行停止,可以看到该算法的耗时:
那么,我们根据a、b和c它们三个的关系,是否可以对上面的算法进行优化呢?首先我们还是需要穷举a的值,b的值的范围就可以缩减到0~1000-a,则c的值的范围就可以缩减到了0~1000-a-b,这样就可以大大减少程序运行时间。代码如下:
"""
a: 0~1000
b: 0~1000-a
c: 1000-a-b
"""
import time
start_time = time.time()
for a in range(0, 1001):
for b in range(0, 1001 - a):
c = 1000 - a - b
if a ** 2 + b ** 2 == c ** 2:
print(a, b, c)
end_time = time.time()
print('run: %.6f' %(end_time-start_time))
运行之后,可以看到运行结果和上面的算法结果是一样的,并且也大大缩减了运行的时间:
从上面的例子可以看出研究算法的必要性,针对某一个问题可以有一个效率很高的算法时非常重要的。
算法效率的衡量
从上面的例子可以看出实现算法程序的执行时间可以反映出算法的效率,即算法的优劣。
但是单靠时间值绝对可信吗?
程序的运行离不开计算机环境(包括硬件和操作系统), 单纯依靠运行的时间来比较算法的优劣并不一定是客观准确的!
时间复杂度与“大O记法”
时间复杂度:假设存在函数g,使得算法A处理规模为n的问题示例所用时间为T(n)=O(g(n)),则称O(g(n))为算法A的渐近时间复杂度,简称时间复杂度,记为T(n)
也就是说假定计算机执行算法每一个基本操作的时间是固定的一个时间单位,那么有多少个基本操作就代表会花费多少时间单位。
“大O记法”: 计量算法基本操作数量的规模函数中那些常量因子可以忽略不计
时间复杂度的几条基本计算规则:
- 基本操作:只有常数项,认为时间复杂度为O(1)
- 顺序结构:时间复杂度按加法进行计算
- 循环结构:时间复杂度按乘法进行计算
- 分支结构:时间复杂度取最大值
注意:没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度。
那么根据这个规则,我们对上面的那个例子的两个算法分别进行分析:
第一个算法:T(n) = O(n * n * n) = O(n³)
第二个算法:T(n) = O(n * n * (1+1)) = O(n * n) = O(n²)
常见的时间复杂度如下:
用折线图表示如下:
性能分析
有一个 timeit 模块可以用来测试一小段Python代码的执行速度。
下面看一下 timeit的格式:
class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>)
- Timer是测量小段代码执行速度的类。
- stmt参数是要测试的代码语句(statment)
- setup参数是运行代码时需要的设置
- timer参数是一个定时器函数,与平台有关。
timeit.Timer.timeit(number=1000000)
这个是测试语句执行速度的对象方法
number参数是测试代码时的测试次数, 默认为1000000次。
下面我们还是通过例子,并且使用timeit模块去分析算法的性能。
我们前面已经知道生成列表有多种方式,但是每一种方式它们的性能却各不相同,接下来我们看一下每一种方式的效率。
生成列表有以下方式:
- 通过连接操作符创建
- 通过append方法
- 通过列表生成式创建
- range创建
代码如下:
def list_create1():
"""通过连接操作符创建"""
li = []
for i in range(0, 1000):
li = li + [i]
def list_create2():
"""通过append方法"""
li = []
for i in range(0, 1000):
li.append(i)
def list_create3():
"""通过列表生成式创建"""
li = [i for i in range(1000)]
def list_create4():
"""range创建"""
list(range(1000))
if __name__ == '__main__':
import timeit
t1 = timeit.Timer('list_create1()',"from __main__ import list_create1 ")
print(t1.timeit(number=1000))
t2 = timeit.Timer('list_create2()',"from __main__ import list_create2 ")
print(t2.timeit(number=1000))
t3 = timeit.Timer('list_create3()',"from __main__ import list_create3 ")
print(t3.timeit(number=1000))
t4 = timeit.Timer('list_create4()',"from __main__ import list_create4 ")
print(t4.timeit(number=1000))
运行程序后可以得到4种生成列表的耗时,它们分别为:
下面总结一下列表和字典内置操作的常见时间复杂度如下:
列表内置操作的时间复杂度:
字典内置操作的时间复杂度: