第二章 算法的基本概念和算法的衡量
一.算法的特征
算法(Algorithm):是对特定问题求解方法(步骤)的一种描述,是指令的有限序列,其中每一条指令表示一个或多个操作。
算法具有以下五个特性
① 有穷性: 一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。
② 确定性:算法中每一条指令必须有确切的含义。不存在二义性。且算法只有一个入口和一个出口。
③ 可行性: 一个算法是能行的。即算法描述的操作都可以通过已经实现的基本运算执行有限次来
④ 输入: 一个算法有零个或多个输入,这些输入取自于某个特定的对象集合。
⑤ 输出: 一个算法有一个或多个输出,这些输出是同输入有着某些特定关系的量。
一个算法可以用多种方法描述,主要有:使用自然语言描述;使用形式语言描述;
使用计算机程序设计语言描述。
算法和程序是两个不同的概念。一个计算机程序是对一个算法使用某种程序设计语言的具体
实现。算法必须可终止意味着不是所有的计算机程序都是算法。
1).算法的评价
评价一个好的算法有以下几个标准
1. 正确性(Correctness ): 算法应满足具体问题的需求。
2. 可读性(Readability): 算法应容易供人阅读和交流。可读性好的算法有助于对算法的理解和修改。
3. 健壮性、鲁棒性(Robustness): 算法应具有容错处理。当输入非法或错误数据时,算法应能适当地作出反应或进行处理,而不会产生莫名其妙的输出结果。
4. 通用性(Generality): 算法应具有一般性 ,即算法的处理结果对于一般的数据集合都成立。
5. 效率与存储量需求: 效率指的是算法执行的时间;存储量需求指算法执行过程中所需要的最大存储空间。一般地,这两者与问题的规模有关。(时间复杂度与空间复杂度)
2).算法的"正确性"
-
对算法是否"正确"的理解可以有以下四个层次:
a.程序中不含语法错误;
b.程序对于几组输入数据能够得出满足要求的结果;
c.程序对于精心选择的、典型、苛刻且带有刁难性的几组输入数据能够得出满足要求的结果;
d.程序对于一切合法的输入数据都能得出满足要求的结果;
-
通常以第 c 层意义的正确性作为衡量一个算法是否合格的标准。
3).算法效率的度量
算法执行时间需通过依据该算法编制的程序在计算机上运行所消耗的时间来度量。其方法通常有两种:
事后统计:计算机内部进行执行时间和实际占用空间的统计。
问题:必须先运行依据算法编制的程序;依赖软硬件环境,容易掩盖算法本身的优劣;没有实际价值。
事前分析:求出该算法的一个时间界限函数。
算法分析感兴趣的不是具体的资源占用量,而是与具体的平台无关、具体的输入实例无关,且随输入规模增长的值是可预测的。
与此相关的因素有:
-
依据算法选用何种策略;
-
问题的规模;
-
程序设计的语言;
-
编译程序所产生的机器代码的质量;
-
机器执行指令的速度;
撇开软硬件等有关因素,可以认为一个特定算法"运行工作量 "的大小,只依赖于问题的规模(通常用n表示),或者说,它是问题规模的函数。
4).渐进分析:大O记号
算法中基本操作重复执行的次数 是问题规模n的某个函数,其时间量度记作T(n)=O(f(n)),称作算法的渐近时间复杂度 (Asymptotic Time complexity),简称时间复杂度。
渐进分析:在问题规模足够大后,计算成本如何增长?
n >> 2后,对于规模为n输入,算法
需执行的基本操作次数:T(n) = ?
需占用的存储单元数:S(n) = ?
一般地,常用最深层循环内 的语句中的原操作的执行频度(重复执行的次数)来表示。
大O记号(big-O notation)(渐近上界O)
**"O"**表示法的一般提法是:当且仅当存在正整数c和n0 ,使得T(n) ≤ 𝑐f(n)对于所有的?n ≥ n0
成立,则称该算法的时间增长率在O(f(n))中,记为T(n) = O(f(n)) 。
与T(n)相比,f(n)更为简洁,但依然反映前者的增长趋势
常系数可忽略:O(f(n)) = O(c ∙ f(n))
低次项可忽略:O(n^a + n^b) = O(n^a), a > b > 0
1.大O记号常见的表示时间复杂度的阶
5).算法的空间分析
空间复杂度(Space complexity) :是指算法编写成程序后,在计算机中运行时
所需存储空间大小的度量。
记作: S(n) = O(f(n))
其中: n为问题的规模(或大小)
该存储空间一般包括三个方面:
-指令常数变量所占用的存储空间;
输入数据所占用的存储空间;
辅助(存储)空间。
一般地,算法的空间复杂度 指的是辅助空间。
一维数组a[n]: 空间复杂度 O(n)
二维数组a[n] [m]: 空间复杂度 O(n ∗ m)
算法的存储空间需求:
-
n 若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。
-
n 若所需额外空间相对于输入数据量来说是常数,则称此算法为原地工作。
-
n 若所需存储量依赖于特定的输入,则通常按最坏情况考虑。
第三章 常见算法思想及案例
1.常见算法思想
1)穷举法
n 穷举法 也称为枚举法 (Exhaustive Attack method),或称暴力破解法 ,又称为强力法* *(Brute-force method),完全试凑法(complete trial-and --error method)。它的基本思想是不重复、不遗漏地穷举所有可能情况,或把信息条理化、系统化、或进行分类,寻找规律,引出信息,以便从中寻找满足条件的结果。
n 穷举法常用于解决"是否存在 "、"有多少种情况 "等类型的问题。 对于一些数学问题、逻辑推理问题,穷举法看来也是一种"笨"方法,但它恰好利用了计算机高速运算的特点,可以避免复杂的逻辑推理过程,使问题简单化。
运用实例:
-
求1~100的素数
-
计算1~100一共多少个9
-
求所有的水仙花数的个数
-
求10000以内的完数
-
马克思手稿中有一道趣味数学题:有30个人,其中有男人、女人和小孩,在一家饭馆里吃饭共花了50先令,每个男人各花3先令,每个女人各花2先令,每个小孩各花1先令,问男人、女人和小孩各有几人?
2)递归与分治策略
将要求解的较大规模的问题分割成k个更小规模的子问题。
对这k个子问题分别求解。如果子问题的规模仍然不够小,则再划分为k个子问题,如此递归的进行下去,直到问题规模足够小,很容易求出其解为止。
递归的概念 :直接或间接地调用自身的算法称为递归算法 。用函数自身给出定义的函数称为递归函数。
由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。
3)动态规划法
- n 动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题
- n 但是经分解得到的子问题往往不是互相独立的 。不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。
总体思想:
如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。
为了达到这个目的,可以用一个表来记录所有已经解决的子问题的答案,不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。
-
找出最优解的性质,并刻划其结构特征。
-
递归地定义最优值。
-
以自底向上的方式计算出最优值。
-
根据计算最优值时得到的信息,构造最优解。
运用案例:
-
斐波拉契数列
-
台阶问题
-
0-1背包问题
4)贪心法
-
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。
-
当然,希望贪心算法得到的最终结果也是整体最优的 。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如选择排序 ,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
1. 贪心选择性质
所谓贪心选择性质 是指所求问题的整体最优解 可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上 的方式解各子问题,而贪心算法则通常以自顶向下 的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2. 最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
问题的最优子结构性质是该问题可用动态规划算法 或贪心算法求解的关键特征。
3. 贪心算法与动态规划算法的差异
贪心算法和动态规划算法都要求问题具有最优子结构性质,这是两类算法的一个共同点。
贪心法基本思路:
- 建立数学模型来描述问题。
- 把求解的问题分成若干个子问题。
- 对每一子问题求解,得到子问题的局部最优解。
- 把子问题的解局部最优解合成原来解问题的一个解。
运行实例:
-
冒泡排序
-
选择排序
-
背包问题
-
找零钱问题
-
活动安排问题
-
最优装载问题
-
哈夫曼编码
-
单源最短路径(Dijkstra算法)
-
最小生成树(Prim算法与Kruskal算法)
-
多机调度(最长处理时间作业优先)
5)回溯法
回溯算法是所有搜索算法中最为基本的一种算法,是一种能避免不必要搜索的穷举式的搜索算法 ,其基本思想就是穷举搜索。
算法思想:
采用了一种"走不通就掉头 "的思想。搜索时往往有多分支,按某一分支为新的出发点,继续向下探索,当所有可能情况都探索过且都无法到达目标的时候,再回退到上一个出发点,继续探索另一个可能情况,这种不断回头寻找目标的方法称为"回溯法" 。
搜索的方式
主要采用深度优先搜索的方式
回溯三要素:
-
解空间:该空包含问题的解
-
约束条件
-
状态树
回溯法基本思想:
(1) 针对所给问题,定义问题的解空间;
(2) 确定易于搜索的解空间结构;
(3) 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
运行实例:
-
装载问题
-
批处理作业问题
-
N皇后问题
-
0-1背包问题
-
最大团问题
-
图的m着色问题
-
旅行售票员问题
-
圆排列问题
-
连续邮资问题
6)分支限界法
1. 分支限界法与回溯法的不同
(1)求解目标不同 :回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
(2)搜索方式不同 :回溯法以深度优先 的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。
2. 分支限界法基本思想
分支限界法常以广度优先 或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有孩子结点。在这些孩子结点中,导致不可行解或导致非最优解的孩子结点被舍弃,其余孩子结点被加入活结点表中。
此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。
3. 常见的两种分支限界法
(1)队列式(FIFO)分支限界法
按照队列先进先出(FIFO)原则选取下一个节点为扩展节点。
(2)优先队列式分支限界法
按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。
7)概率算法
-
概率算法也叫随机化算法。概率算法允许算法在执行过程中随机地选择下一个计算步骤。在很多情况下,算法在执行过程中面临选择时,随机性选择比最优选择省时,因此概率算法可以在很大程度上降低算法的复杂度。
-
概率算法的一个基本特征是对所求解问题的同一实例用同一概率算法求解两次可能得到完全不同的效果。这两次求解问题所需的时间甚至所得到的结果可能会有相当大的差别。
确定算法的平均执行时间:
输入规模一定的所有输入实例是等概率出现时,算法的平均执行时间。
概率算法的期望执行时间:
反复解同一个输入实例所花的平均执行时间。
因此,对概率算法可以讨论如下两种期望时间
① 平均的期望时间:所有输入实例上平均的期望执行时间
② 最坏的期望时间:最坏的输入实例上的期望执行时间
cpp
//求π近似值的算法
//为简单起见,只以上图的右上1/4象限为样本
Darts (n) {
k ← 0;
for i ← 1 to n do
{
x ← uniform(0, 1);
y ← uniform(0, 1); // 随机产生点(x,y)
if (x2 + y2 ≤ 1) then k++; //圆内
}
return 4k/n;
}
// 实验结果: π=3.141592654
// n = 1000万: 3.140740, 3.142568 (2位精确)
// n = 1亿: 3.141691, 3.141363 (3位精确)
// n = 10亿: 3.141527, 3.141507 (4位精确)