
2.5 常用的算法思想
对于计算机科学而言,算法是一个非常重要的概念;它是程序设计的灵魂,是将实际问题同解决该问题的计算机程序建立起联系的桥梁。算法思想有很多,例如枚举、递归、分治、贪心、试探、动态迭代等。本节将详细讲解几种常用算法思想的基本知识,为步入本书后面实践案例的学习打下基础。

↑扫码看视频(本节视频课程时间:3分46秒)
2.5.1 枚举算法思想
枚举算法也称穷举算法,最大特点是在面对任何问题时会尝试每一种解决方法。在进行归纳推理时,如果逐个考察了某类事件的所有可能情况,因而得出一般结论,那么这个结论是可靠的,这种归纳方法称为枚举法。
枚举算法思想是:将问题所有可能的答案一一列举,然后根据条件判断此答案是否合适,保留合适的,丢弃不合适的。在Python语言中,枚举算法一般使用while循环或if语句来实现。使用枚举算法解题的基本思路如下:
(1)确定枚举对象、枚举范围和判定条件;
(2)逐一列举可能的解,验证每个解是否是问题的解。
枚举算法一般按照以下3个步骤进行:
(1)题解的可能范围,不能遗漏任何一个真正解,也要避免有重复;
(2)判断是否是真正解的方法;
(3)使可能解的范围降至最小,以便提高解决问题的效率。
枚举算法的主要流程如图2-9所示。

图2-9 枚举算法流程图
2.5.2 递归算法思想
因为递归算法思想往往用函数的形式来体现,所以递归算法需要预先编写功能函数。这些函数是独立的功能,能够实现解决某个问题的具体功能,当需要时直接调用这个函数即可。在计算机编程应用中,递归算法对解决大多数问题十分有效,它能够使算法的描述变得简洁且易于理解。递归算法有以下3个特点:
(1)递归过程一般通过函数或子过程来实现;
(2)递归算法在函数或子过程的内部,直接或者间接地调用自己的算法;
(3)递归算法实际上是把问题转化为规模缩小了的同类问题的子问题,然后再递归调用函数或过程来表示问题的解。
在使用递归算法时,应注意以下4点:
(1)递归是在过程或函数中调用自身的过程;
(2)在使用递归策略时,必须有一个明确的递归结束条件,这称为递归出口;
(3)递归算法通常显得很简洁,但是运行效率较低,所以一般不提倡用递归算法设计程序;
(4)在递归调用过程中,系统用栈来存储每一层的返回点和局部量。如果递归次数过多,则容易造成栈溢出,所以一般不提倡用递归算法设计程序。
2.5.3 分治算法思想
分治算法也采取了各个击破的方法,将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。只要求出子问题的解,就可得到原问题的解。
在编程过程中,经常遇到处理数据相当多、求解过程比较复杂、直接求解法会比较耗时的问题。在求解这类问题时,可以采用各个击破的方法。具体做法是:先把这个问题分解成几个较小的子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个大问题的解。如果这些子问题还是比较大,还可以继续把它们分成几个更小的子问题,依此类推,直至可以直接求出解为止。这就是分治算法的基本思想。
使用分治算法解题的一般步骤如下:
(1)分解,将要解决的问题划分成若干个规模较小的同类问题;
(2)求解,当子问题划分得足够小时,用较简单的方法解决;
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。
2.5.4 贪心算法思想
贪心算法也称贪婪算法,它在求解问题时总想用在当前看来是最好的方法来实现。这种算法思想不从整体最优上考虑问题,仅仅是在某种意义上的局部最优求解。虽然贪心算法并不能得到所有问题的整体最优解,但是面对范围相当广泛的许多问题时,能产生整体最优解或者是整体最优解的近似解。由此可见,贪心算法只是追求某个范围内的最优,可以称为“温柔的贪婪”。
贪心算法从问题的某一个初始解出发,逐步逼近给定的目标,以便尽快求出更好的解。当达到算法中的某一步不能再继续前进时,就停止算法,给出一个近似解。由贪心算法的特点和思路可看出,贪心算法存在以下3个问题:
(1)不能保证最后的解是最优的;
(2)不能用来求最大解或最小解问题;
(3)只能求满足某些约束条件可行解的范围。
贪心算法的基本思路如下:
(1)建立数学模型来描述问题;
(2)把求解的问题分成若干个子问题;
(3)对每一子问题求解,得到子问题的局部最优解;
(4)把子问题的局部最优解合并成原来解问题的一个解。
实现该算法的基本过程如下:
(1)从问题的某一初始解出发;
(2)while能向给定总目标前进一步;
(3)求出可行解的一个解元素;
(4)由所有解元素组合成问题的一个可行解。
2.5.5 试探法算法思想
试探法也称回溯法,试探法的处事方式比较委婉,它先暂时放弃关于问题规模大小的限制,并将问题的候选解按某种顺序逐一进行枚举和检验。当发现当前候选解不可能是正确的解时,就选择下一个候选解。如果当前候选解除了不满足问题规模要求外能够满足所有其他要求时,则继续扩大当前候选解的规模,并继续试探。如果当前候选解满足包括问题规模在内的所有要求时,该候选解就是问题的一个解。在试探算法中,放弃当前候选解,并继续寻找下一个候选解的过程称为回溯。扩大当前候选解的规模,并继续试探的过程称为向前试探。
使用试探算法解题的基本步骤如下:
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
试探法为了求得问题的正确解,会先委婉地试探某一种可能的情况。在进行试探的过程中,一旦发现原来选择的假设情况是不正确的,立即会自觉地退回一步重新选择,然后继续向前试探,如此这般反复进行,直至得到解或证明无解时才死心。
假设存在一个可以用试探法求解的问题P,该问题表达为:对于已知的由n元组(y1,y2,…,yn)组成的一个状态空间E={(y1,y2,…,yn)∣yi∈Si,i=1,2,…,n},给定关于n元组中一个分量的一个约束集D,要求E中满足D的全部约束条件的所有n元组。其中,Si是分量yi的定义域,且|Si|有限,i=1,2,…,n。E中满足D的全部约束条件的任一n元组为问题P的一个解。
解问题P的最简单方法是使用枚举法,即对E中的所有n元组逐一检测其是否满足D的全部约束,如果满足,则为问题P的一个解。但是这种方法的计算量非常大。
对于现实中的许多问题,所给定的约束集D具有完备性,即i元组(y1,y2,…,yi)满足D中仅涉及y1,y2,…,yj的所有约束,这意味着j(j<i)元组(y1,y2,…,yj)一定也满足D中仅涉及y1,y2,…,yj的所有约束,i=1,2,…,n。换句话说,只要存在0≤j≤n-1,使得(y1,y2,…,yj)违反D中仅涉及y1,y2,…,yj的约束之一,则以(y1,y2,…,yj)为前缀的任何n元组(y1,y2,…,yj,yj+1,…,yn)一定也违反D中仅涉及y1,y2,…,yi的一个约束,n≥i>;j。
因此,对于约束集D具有完备性的问题P,一旦检测断定某个j元组(y1,y2,…,yj)违反D中仅涉及y1,y2,…,yj的一个约束,就可以肯定,以(y1,y2,…,yj)为前缀的任何n元组(y1,y2,…,yj,yj+1,…,yn)都不会是问题P的解,因而就不必去搜索、检测它们。试探法是针对这类问题而推出的,比枚举算法的效率更高。
2.5.6 迭代算法
迭代法也称辗转法,是一种不断用变量的旧值递推新值的过程,在解决问题时总是重复利用一种方法。与迭代法相对应的是直接法(或者称为一次解法),即一次性解决问题。迭代法又分为精确迭代和近似迭代。“二分法”和“牛顿迭代法”属于近似迭代法,功能都比较类似。
迭代算法是用计算机解决问题的一种基本方法。它利用计算机运算速度快、适合做重复性操作的特点,让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值。
在使用迭代算法解决问题时,需要做好以下3个方面的工作。
(1)确定迭代变量
在可以使用迭代算法解决的问题中,至少存在一个迭代变量,即直接或间接地不断由旧值递推出新值的变量。
(2)建立迭代关系式
迭代关系式是指如何从变量的前一个值推出其下一个值的公式或关系。通常使用递推或倒推的方法来建立迭代关系式,迭代关系式的建立是解决迭代问题的关键。
(3)对迭代过程进行控制
在编写迭代程序时,必须确定在什么时候结束迭代过程,不能让迭代过程无休止地重复执行下去。通常可分为以下两种情况来控制迭代过程:
①所需的迭代次数是个确定的值,可以计算出来,可以构建一个固定次数的循环来实现对迭代过程的控制;
②所需的迭代次数无法确定,需要进一步分析出用来结束迭代过程的条件。