什么是算法
算法是对计算过程的描述,是为了解决某个问题而设计的有限长的操作序列。通常认为算法具有以下性质。
有穷性: 一个算法必须可以用有穷条指令、伪指令或者自然语言语句描述,且必须在执行有穷次操作后终止。每次操作都必须在有穷时间内完成。算法终止后必须给出所处理问题的解或宣告问题无解。
确定性: 一个算法,对于相同的输入,无论运行多少次,总是得到相同的输出。也可以说,只要算法运行前的初始条件相同,那么算法运行的结果也相同。例如,算法无法产生真正的随机数序列,只能产生看上去像随机数序列的“伪随机数序列”。这是因为,真正的随机数序列,就如掷骰子得到的点数序列那样是不可预测的,而试图产生随机数序列的算法,只要初始条件(随机种子)相同,多次运行产生的随机数序列也必然相同,即可预测,因此不是真正的随机数序列。
可行性: 算法中的指令(或描述语句)含义明确无歧义,且可以被机械化地自动执行。
输入/输出: 这里的输入/输出,不应狭隘地理解成键盘输入和显示器或打印机的输出。输入指的是算法所处理的问题的数据,输出指的是描述该问题的答案的数据。算法可以不需要输入。但是没有输出的算法是没有意义的。算法变为程序运行起来后,从本质上说,输入和输出,都是存放在内存的数据,当然它们可能一开始从外存被读入内存,也可能最后从内存要写入外存或外部设备。
最常用的算法,或者说设计算法时最常用的思想有以下几种。
枚举法: 对所有可能的解进行逐个验证,直到发现真正的解。例如,用枚举法求大于n的最小质数,就可以从n+1开始验证每个数是否是质数,碰到的第一个质数就是问题的解。
二分法: 对于有些问题,将所有可能解排序,通过对位于解的查找区间中点的解进行一次验证,就可以找到解或缩小查找区间到原来的一半,这样就能很快找到解或宣告无解。二分法成立的前提条件是解的单调性,即如果一个可能解被发现是因为太大(或太小)而不能成立,则比其更大(或更小)的所有可能解,都必定不能成立。什么叫“大”或“小”,可以根据实际问题自行定义。
贪心法: 在寻找解的过程中,每一步都只选取眼前最优的做法,不考虑后续影响。这么做很可能导致找到的解并非全局最优,所以贪心法并不适用于所有需要求最优解的问题。有一些求最优解的问题,可以证明每一步取当前最优,最终也能取得全局最优,那么就可以用贪心算法来解决。
递归法和分治法: 为了解决问题,可以先采取一步行动,剩下的问题就变成和原问题形式相同但是规模更小的问题,这样就可以用递归解决。或者,将原问题分解为几个和原问题形式相同但是规模更小的子问题,子问题都解决了,原问题也就解决了,这就叫做分治。分治往往用递归实现。
深度优先搜索、回溯和分支限界法: 在许多问题中,搜索解的过程,可以抽象为在迷宫中找出口。走迷宫的一个策略就是能往前走就往前走,这就叫深度优先;走不动了就回退到上一个岔路口选没走过的岔道继续走,这就叫回溯。有的情况下有办法预判一个岔道走下去肯定没前途,于是就不会走它,这就叫分支限界法。回溯和分支限界都是深度优先搜索过程中使用的手段。
广度优先搜索法: 解决问题,可能需要采取多步行动,每步行动都有不同选择。先把第一步能采取的所有选择都试一遍,看看问题有没有解决。如果没有,再把采取两步行动的所有方案都试一遍,看看问题有没有解决……这样当问题解决时,一定采取的步数是最少的,这就是广度优先搜索。
动态规划法: 单纯采用深度优先搜索的办法,可能会导致大量重复计算,即相同的子问题被计算多次,这往往导致计算量呈指数级增长。在搜索过程中将求得的子问题的解保存下来,避免重复计算,用空间换时间,这就是动态规划的思想。
上述几种算法设计思想,有些其实没必要也无法严格区分。例如搜索法和二分法,本质上和枚举一样,都是用验证可能解的方式去寻找解,只不过要想办法跳过那些不需要验证就可以否定的可能解。实现分治和深度优先搜索的时候,一般都会用到递归。搜索往往也需要进行问题分解,那么说它是分治也未尝不可。
衡量算法优劣最主要的指标是运行效率。运行效率分为时间效率和空间效率两种。时间效率指的是算法运行时间的长短;空间效率则是算法需要存储空间的多少。时间效率和空间效率往往很难兼顾,可以用空间来换时间,也可以用时间来换空间。绝大多数情况下,时间效率更为重要,因此,用空间换时间的策略,在算法设计中应用很广,非常常用的动态规划算法,就是如此。
运行效率相同的不同算法,也有编程效率的高低之分,即将算法变为程序的难易之分。程序员能够用较短时间实现,且不容易写出隐错的算法更好。