Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

算法与正确性

复习

  • 数据为什么需要组织:数据怎么摆,会直接影响各种操作的代价
  • 抽象数据类型:它把接口和实现分开,便于替换
  • 算法:算法关注步骤怎么走,和数据组织常常配合

TL;DR

  • 算法是有限、明确、可执行的步骤,用来解决一类问题
  • 合格的算法要正确,也要能在可接受的时间内完成
  • 正确性需要论证,不能只靠“跑通几个例子”
  • 把问题本身说清楚,是设计算法的前提

正文

  数据结构解决“数据怎么摆”,接下来自然要问“步骤怎么走”。后者,就是算法(algorithm)。

算法是什么

  一个算法,通常要满足几条朴素的品质:

  • 有限:步骤是有限的,且一定会在有限步内结束
  • 明确:每一步都清晰无歧义
  • 可执行:每一步都是能真正做出来的操作
  • 有输入输出:对给定输入,产生确定的输出

  注意“有限”这一条有多重要。如果一段步骤永远停不下来,它就称不上算法——无论它看起来多聪明。

一个经典例子

  求两个数的最大公约数,有个流传两千多年的方法——辗转相除法(欧几里得算法):

当 b ≠ 0:
    (a, b) ← (b, a 除以 b 的余数)
答案就是最后的 a

  比如求 48 和 18 的最大公约数:

  • 48 ÷ 18 余 12,变成 (18, 12)
  • 18 ÷ 12 余 6,变成 (12, 6)
  • 12 ÷ 6 余 0,变成 (6, 0)
  • 此时 b = 0,答案是 6

  步骤简单,结果正确。

为什么它一定对

  可“跑出来对”和“一定对”是两回事。我们凭什么相信,这个方法对任何两个数都成立?

  关键在于一个事实:两个数的公约数,和“较小数”与“两数相除的余数”的公约数,是完全一样的。 也就是说,把 (a, b) 换成 (b, a mod b),不会漏掉也不会多出任何一个公约数。既然每一步都保持“公约数集合不变”,而过程又在不断变小、必然终止,那么最终剩下的那个数,就是我们要求的最大公约数。

  你看,正确的算法,背后往往有一条能讲清楚的理由,而不是碰巧试对了。

跑通例子不算证明

  这一点值得多说一句。我们之前在讲测试时提过:测试能发现错误,却不能证明没有错误。

  算法也是一样。你拿十几个输入跑一遍都通过了,只能说明这十几个情况没问题,不能保证下一个输入不出岔子。真正让人放心的,是像上面那样,把“为什么它对”讲明白

  当然,正确只是及格线。一个算法即使正确,如果慢到跑不完,也派不上用场。那么,该怎样衡量“快慢”?下一章开始,我们就来解决这个问题。

思考题 1

  为什么“在有限步内结束”是算法的必要条件?

思考题 2

  一个程序“跑通了几个例子”,能证明它正确吗?如果不能,那要怎样才更让人信服?

小结

知识点

  • 算法是有限、明确、可执行的步骤
  • 正确之外,还要考虑运行时间
  • 正确性依赖论证,而非枚举用例
  • 辗转相除法体现了“不变式 + 终止”的证明思路

参考资料

  1. Wikipedia(zh):算法:解决问题的有限步骤序列
  2. Wikipedia(zh):欧几里得算法:求最大公约数的经典方法

思考题答案(仅供参考)

思考题 1

  因为算法要能给出答案。如果步骤可能永远进行下去,就永远得不到结果,也就无法解决问题。有限且必然终止,是“这个方法真的能用”的基本保障。

思考题 2

  不能。枚举再多的用例,也只覆盖了有限情况,无法排除没试过的输入出错。更让人信服的做法是给出论证:说明每一步都保持某种正确性质(不变式),并证明过程一定终止,从而对全部合法输入都成立。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪