### 10.5.1 算法复杂度
为了回答上述问题,首先要明确如何衡量算法的好坏。以搜索问题为例,线性搜索算法
直接了当,易设计易实现,这算不算“好”?而二分搜索算法虽然设计实现稍难一些,但因 无需检查每一个数据而大大提高了搜索效率,这又算不算“好”?
在解决数学问题时,不论是证明定理还是计算表达式,只要证明过程正确、计算结果精 确,问题就可以认为成功地解决了,即正确性、精确性是评价数学解法好坏的标准。而在用 计算机解决问题时,仅仅要求算法能正确、精确地解决问题,是不够的。试想,假如一个算 法虽然能够正确地解决问题,但需要在计算机上运行 100 年或者需要占用 100TB 的内存, 这样的算法有实际意义吗?不要以为 100 年或 100TB 是危言耸听,很多简单算法都可能轻 易地达到或突破这样的需求(参见稍后对 Hanoi 塔算法的分析)。可见,利用计算机解决问 题必须考虑算法的经济性,即算法所耗费的资源问题。计算机用户当然都希望能多快好省地 解决问题,因此好的算法应当尽量少地耗费资源。
通常只考虑算法所占用的 CPU 时间和存储器空间这两种资源①。所谓算法分析,就是分 析特定算法在运行时所耗费的时间和存储空间的数量,分别称为算法的时间复杂度和空间复 杂度。本节只讨论算法的时间复杂度,毕竟存储空间的大小在现代计算机中已经越来越不再 是一个问题。
虽然讨论的是算法耗费的“时间”,但我们并不是真的去测量程序在计算机中的实际运 行时间,因为实际运行时间依赖于特定机器平台(CPU、内存、操作系统、编程语言等), 同一算法在不同平台上执行可能得到不同的分析结果,故很难据此对算法进行分析和比较。 例如,在最先进的计算机上执行线性搜索也许比在老式计算机上执行二分搜索要快,据此得 出线性搜索优于二分搜索显然不合理。
实际上,算法分析指的是分析算法的代码,估计出为解决问题需要执行的操作(或语句、 指令等类似概念)的数目,或称算法的“步数”。之所以分析算法步数,是因为:第一,步 数确实能反映执行时间——步数越多执行时间就越长;第二,算法的步数不依赖于平台,更 容易分析和比较。
例如,下面的函数 f1()需要执行 11 次赋值操作,其中包含 10 次加法运算②:
```
def f1():
x = 0
for i in range(10):
x = x + 1
```
而下面的函数 f2()需要 21 次赋值操作(20 次加法):
```
def f2():
x = 0
for i in range(20):
x = x + 1
```
比较一下 f1 和 f2,显然 f1 运行时间更短,但这并不意味着 f1 比 f2 采用的算法“好”, 因为它们的“算法”显然是一样的,只不过 f1 要处理的数据更少:f1 将 10 个 1 相加,而 f2 将 20 个 1 相加。可见,算法复杂度是跟算法处理的数据量有关的。
算法通常都设计成能处理任意大小的输入数据,这就导致算法的步数并不是固定的,而 是随着问题规模的变化而变化,因此算法的步数可表示为问题规模的函数。假设用 n 表示问题规模,算法分析不仅要考虑算法步数与 n 的关系,更重要的是还要考虑“当 n 逐渐增大时” 算法复杂度会如何变化。例如,将上述 f1 和 f2 改写成更一般的形式:
> ① 不考虑开发算法的人力物力等代价。
> ② 注意我们分析的层次是源代码级别,而不是机器指令级别。
```
def f(n):
x = 0
for i in range(n):
x = x + 1
```
不难得出此函数需要执行的步数为 n+1。当 n 增大时,算法执行时间也会增加,而且是线性地增加,即:当 n 增加 1 倍变成 2n,执行时间变成 2n+1,大约比 n+1 增加 1 倍。
说 A 算法比 B 算法好,并不是指对于特定的 n,A 比 B 节省 50%的时间,而是指随着 n 的不断增大,A 对 B 的优势会越来越大。
算法复杂度的大 O 表示法
再次观察上面例子中函数 f()的步数表达式“n+1”,不难看出其中对执行时间起决定作 用的是 n,而 n 后面的+1 是可以忽略不计的。按照“当 n 逐渐增大时”进行分析的思想, 即便是 n+100、n+1000000 中,n 后面的常数也是可以忽略不计的,因为与逐渐增大趋于∞ 的 n 相比,任何常数都是浮云。事实上,分析算法复杂度时,我们只分析其增长的数量级, 而不是分析其精确的步数公式。
数学中的“大 O 表示法”根据函数的增长率特性来刻画函数,可以用来描述算法的复 杂度。令 f(n)和 g(n)是两个函数,如果存在正常数 c,使得只要 n 足够大(例如超过某个 n0), 函数 f(n)的值都不会超过 c×g(n),即当 n > n0 时,
```
f(n) <= c x g(n)
```
则可记为
```
f (n) = O(g(n))
```
在描述算法复杂度时,n 对应于问题规模,f(n)是算法需执行的步数,g(n)是表示增长数 量级的某个函数。说算法的复杂度为 O(g(n)),意思就是当 n 足够大时,该算法的执行步数(时间)永远不会超过 c×g(n)。
例如,假设一个算法当输入规模为 n 时需要执行 n+100 条指令,则当 n 足够大时(只 要大于 100),
```
n + 100 <= n + n = 2n
```
套用大 O 表示法的定义,取 g(n)=n,则可将此算法的复杂度表示为 O(n)。同理,如果一个 算法的步数为 n+1000000,它的复杂度仍然可表示为 O(n)。由此可见,两个不同的算法虽然 具有不同的代码和执行步数,但完全可能具有相同的复杂度,即当问题规模足够大时,它们 的执行时间按相同数量级的增长率增长,利用大 O 表示法即可描述这一点。
实际分析算法时,为了使 O(g(n))中的 g(n)函数尽量简单,在得到算法的步数表达式 f(n) 之后,可以利用两条规则来简化推导,直接得出 f(n)的大 O 表示。规则如下:
(1)如果 f(n)是若干项之和,则只需保留最高次项,省略所有低次项;
(2)如果 f(n)是若干项之积,则可省略任何常数因子(即与 n 无关的因子)。 例如,分析下列代码:
```
def f(n):
x = 0
for i in range(n):
for j in range(n):
for k in range(n):
x = x + 1
for i in range(n):
for j in range(n):
for k in range(n):
x = x + 1
for i in range(n):
x = x + 1
```
易知此算法的步数为 2n<sup>3</sup>+n+1。根据第一条规则,可只保留 2n3;再根据第二条规则,可只保留 n<sup>3</sup>。所以,此算法的复杂度为 O(n<sup>3</sup>)。当然我们也可以直接从 f(n)开始推导,利用大 O表示法的定义来验证这个结果是正确的:对于 n > 1,
![](https://box.kancloud.cn/2016-02-22_56cafce7f2c2f.png)
取 g(n)为 n3,c 为 4,即得 f(n) = O(n3)。 总之,以上两条规则告诉我们,在分析算法代码时可以忽略许多代码,而只关注那些嵌套层数最多、并且每一层循环的循环次数都与问题规模 n 有关的循环。
- 前言
- 第 1 章 计算与计算思维
- 1.1 什么是计算?
- 1.1.1 计算机与计算
- 1.1.2 计算机语言
- 1.1.3 算法
- 1.1.4 实现
- 1.2 什么是计算思维?
- 1.2.1 计算思维的基本原则
- 1.2.2 计算思维的具体例子
- 1.2.3 日常生活中的计算思维
- 1.2.4 计算思维对其他学科的影响
- 1.3 初识 Python
- 1.3.1 Python 简介
- 1.3.2 第一个程序
- 1.3.3 程序的执行方式
- 1.3.4 Python 语言的基本成分
- 1.4 程序排错
- 1.5 练习
- 第 2 章 用数据表示现实世界
- 2.1 数据和数据类型
- 2.1.1 数据是对现实的抽象
- 2.1.1 常量与变量
- 2.1.2 数据类型
- 2.1.3 Python 的动态类型*
- 2.2 数值类型
- 2.2.1 整数类型 int
- 2.2.2 长整数类型 long
- 2.2.3 浮点数类型 float
- 2.2.4 数学库模块 math
- 2.2.5 复数类型 complex*
- 2.3 字符串类型 str
- 2.3.1 字符串类型的字面值形式
- 2.3.2 字符串类型的操作
- 2.3.3 字符的机内表示
- 2.3.4 字符串类型与其他类型的转换
- 2.3.5 字符串库 string
- 2.4 布尔类型 bool
- 2.4.1 关系运算
- 2.4.2 逻辑运算
- 2.4.3 布尔代数运算定律*
- 2.4.4 Python 中真假的表示与计算*
- 2.5 列表和元组类型
- 2.5.1 列表类型 list
- 2.5.2 元组类型 tuple
- 2.6 数据的输入和输出
- 2.6.1 数据的输入
- 2.6.2 数据的输出
- 2.6.3 格式化输出
- 2.7 编程案例:查找问题
- 2.8 练习
- 第 3 章 数据处理的流程控制
- 3.1 顺序控制结构
- 3.2 分支控制结构
- 3.2.1 单分支结构
- 3.2.2 两路分支结构
- 3.2.3 多路分支结构
- 3.3 异常处理
- 3.3.1 传统的错误检测方法
- 3.3.2 传统错误检测方法的缺点
- 3.3.3 异常处理机制
- 3.4 循环控制结构
- 3.4.1 for 循环
- 3.4.2 while 循环
- 3.4.3 循环的非正常中断
- 3.4.4 嵌套循环
- 3.5 结构化程序设计
- 3.5.1 程序开发过程
- 3.5.2 结构化程序设计的基本内容
- 3.6 编程案例:如何求 n 个数据的最大值?
- 3.6.1 几种解题策略
- 3.6.2 经验总结
- 3.7 Python 布尔表达式用作控制结构*
- 3.8 练习
- 第 4 章 模块化编程
- 4.1 模块化编程基本概念
- 4.1.1 模块化设计概述
- 4.1.2 模块化编程
- 4.1.3 编程语言对模块化编程的支持
- 4.2 Python 语言中的函数
- 4.2.1 用函数减少重复代码 首先看一个简单的用字符画一棵树的程序:
- 4.2.2 用函数改善程序结构
- 4.2.3 用函数增强程序的通用性
- 4.2.4 小结:函数的定义与调用
- 4.2.5 变量的作用域
- 4.2.6 函数的返回值
- 4.3 自顶向下设计
- 4.3.1 顶层设计
- 4.3.2 第二层设计
- 4.3.3 第三层设计
- 4.3.4 第四层设计
- 4.3.5 自底向上实现与单元测试
- 4.3.6 开发过程小结
- 4.4 Python 模块*
- 4.4.1 模块的创建和使用
- 4.4.2 Python 程序架构
- 4.4.3 标准库模块
- 4.4.4 模块的有条件执行
- 4.5 练习
- 第 5 章 图形编程
- 5.1 概述
- 5.1.1 计算可视化
- 5.1.2 图形是复杂数据
- 5.1.3 用对象表示复杂数据
- 5.2 Tkinter 图形编程
- 5.2.1 导入模块及创建根窗口
- 5.2.2 创建画布
- 5.2.3 在画布上绘图
- 5.2.4 图形的事件处理
- 5.3 编程案例
- 5.3.1 统计图表
- 5.3.2 计算机动画
- 5.4 软件的层次化设计:一个案例
- 5.4.1 层次化体系结构
- 5.4.2 案例:图形库 graphics
- 5.4.3 graphics 与面向对象
- 5.5 练习
- 第 6 章 大量数据的表示和处理
- 6.1 概述
- 6.2 有序的数据集合体
- 6.2.1 字符串
- 6.2.2 列表
- 6.2.3 元组
- 6.3 无序的数据集合体
- 6.3.1 集合
- 6.3.2 字典
- 6.4 文件
- 6.4.1 文件的基本概念
- 6.4.2 文件操作
- 6.4.3 编程案例:文本文件分析
- 6.4.4 缓冲
- 6.4.5 二进制文件与随机存取*
- 6.5 几种高级数据结构*
- 6.5.1 链表
- 6.5.2 堆栈
- 6.5.3 队列
- 6.6 练习
- 第 7 章 面向对象思想与编程
- 7.1 数据与操作:两种观点
- 7.1.1 面向过程观点
- 7.1.2 面向对象观点
- 7.1.3 类是类型概念的发展
- 7.2 面向对象编程
- 7.2.1 类的定义
- 7.2.2 对象的创建
- 7.2.3 对象方法的调用
- 7.2.4 编程实例:模拟炮弹飞行
- 7.2.5 类与模块化
- 7.2.6 对象的集合体
- 7.3 超类与子类*
- 7.3.1 继承
- 7.3.2 覆写
- 7.3.3 多态性
- 7.4 面向对象设计*
- 7.5 练习
- 第 8 章 图形用户界面
- 8.1 图形用户界面概述
- 8.1.1 程序的用户界面
- 8.1.2 图形界面的组成
- 8.1.3 事件驱动
- 8.2 GUI 编程
- 8.2.1 UI 编程概述
- 8.2.2 初识 Tkinter
- 8.2.3 常见 GUI 构件的用法
- 8.2.4 布局
- 8.2.5 对话框*
- 8.3 Tkinter 事件驱动编程
- 8.3.1 事件和事件对象
- 8.3.2 事件处理
- 8.4 模型-视图设计方法
- 8.4.1 将 GUI 应用程序封装成对象
- 8.4.2 模型与视图
- 8.4.3 编程案例:汇率换算器
- 8.5 练习
- 第 9 章 模拟与并发
- 9.1 模拟
- 9.1.1 计算机建模
- 9.1.2 随机问题的建模与模拟
- 9.1.3 编程案例:乒乓球比赛模拟
- 9.2 原型法
- 9.3 并行计算*
- 9.3.1 串行、并发与并行
- 9.3.2 进程与线程
- 9.3.3 多线程编程的应用
- 9.3.4 Python 多线程编程
- 9.3.5 小结
- 9.4 练习
- 第 10 章 算法设计和分析
- 10.1 枚举法
- 10.2 递归
- 10.3 分治法
- 10.4 贪心法
- 10.5 算法分析
- 10.5.1 算法复杂度
- 10.5.2 算法分析实例
- 10.6 不可计算的问题
- 10.7 练习
- 第 11 章 计算+X
- 11.1 计算数学
- 11.2 生物信息学
- 11.3 计算物理学
- 11.4 计算化学
- 11.5 计算经济学
- 11.6 练习
- 附录
- 1 Python 异常处理参考
- 2 Tkinter 画布方法
- 3 Tkinter 编程参考
- 3.1 构件属性值的设置
- 3.2 构件的标准属性
- 3.3 各种构件的属性
- 3.4 对话框
- 3.5 事件
- 参考文献