首页 > 代码库 > 常用算法设计技术总结

常用算法设计技术总结

   算法,即计算的方法,使用计算的思想、方法、工具和技术来实现问题求解的思路和途径。计算机提供了计算的能力和硬件设施;算法则提供了计算的思想和软件技术,更好地发挥计算机的潜能。

                                                                                                                            —— 引

 

      有人说,算法无用,这种观点就如同盲人看不到花开就说世界上没有花一样,是一个长眼睛的人无论如何也难以接受的。举个简单的例子,假如你要对1000,000条记录进行排序,你的计算机每秒可处理1000,000 条记录, 那么, 如果你选择 O(nlogn)的快速排序,所花时间大约是 log2(1000000) = 20s, 如果你选择O(n^2)的插入排序,所花时间大约是 1000000^2/1000000 = 1000000s = 11.57天;如果你选择O(n^3)的XX排序,所花时间大约是1000000^2 = 31709.791年。很明显,拥有子弹的不如拥有导弹的,拥有导弹的不如拥有核弹的。你很容易就可以知道,谁才是未来世界的王者。

      算法大师Knuth曾说过:“……算法作为一种一般的智能性工具,有助于对各学科知识的理解……只有将知识教给计算机,才能真正理解知识……比起常规的问题理解来说,将知识表述成算法的形式将使理解更为深刻……”所以,算法不仅可以决定统治世界的王者(除非你将世界上的所有计算机全部销毁),还可以有助于个人的知识学习和理解,甚至,对生活的领悟;算法有助于高效工作的学习工作。

 

      现在,就让我们来梳理一下常用的算法设计技术吧!

 

     一、 分治法:

   将问题实例 P(n) 分解为 b 个子问题实例 P(n/b) , 其中有 a 个子问题需要求解。 然后将子问题的解合并起来得到原问题的解。

   一言以蔽之, 即“分而合之”。

 

   例子: 

    ①   归并排序: 将待排序列表先分成若干的子列表,分别排序,然后合并起来。归并排序 的关键在于“合”的过程;

    ②   快速排序: 选取列表中的某种元素,放在列表中的某个位置,使列表分为 小于该元素 和 不小于该元素 的两个子列表。然后, 分别对两个子列表重复上述步骤,直到整个列表有序为止。 快速排序法 关键在“分”的过程。

    ③    二叉树遍历: 分别遍历根结点、左子树和右子树。具有天然的分治和递归特性。

 

   分治法的效率:

   T(n) = aT(n/b) + f(n) , 其中  f(n) 是“分”或者“合”的过程中所耗费的时间。对于归并排序来说, f(n) 是将两个有序列表合并成一个更大的有序列表所耗费的时间; 对于快速排序来说, f(n) 是选取分割元素并将其放在最终位置所耗费的时间。

 

   根据主定理: 令 f(n) = O(n^d),   

   ① 当 d > log(a)/log(b) 时, T(n) = n^d; ② 当 d = log(a)/log(b) 时, T(n) = (n^d)log(n)

   ③ 当 d < log(a)/log(b) 时, T(n) = n^(log(a)/log(b))

 

   主定理其实很好记: T(n) 的效率 由 aT(n/b) 和 f(n) 的效率比较而得, 其中 aT(n/b) 的效率可看成: n^(log(a)/log(b)),

   只要比较幂数取大者就可以了。

 

   常用的分治法,是将问题实例分成规模大致相等的2个子问题实例。根据主定理:

 

     T(n) = 2T(n/2) + O(C) , C 是常数 --------- T(n) = O(n)

     T(n) = 2T(n/2) + n   -----  T(n) = O(nlog2(n))

     T(n) = 2T(n/2) + n^2 ------------------- T(n) = O(n^2)

 

   可以看出, f(n) 也是起很重要作用的, f(n) 不能太大, 最好是线性的,否则, 采用分治法的效率就可能不高了。合理采用分治法的效率一般应该可以达到O(nlog2(n)),至少应该比采取其他算法技术的效率要高一个级别。

 

   二、减治法:

   当 a = 1 时, 即 T(n) = T(n/b) + f(n) 时, 分治法就退化为减治法。 此时,f(n) 的大小就起非常重要的影响了。

 

   减治法有三种形式:

   ① 减常量: T(n) = T(n-b) + f(n)  eg. 

                          T(n) = T(n-1) + C, C为常数 -----  T(n) = O(n)

                          T(n) = T(n-1) + n  -----------  T(n) = O(n^2) 

 

      减常量的例子: 插入排序 -----  T(n) = T(n-1) + n   T(n) = O(n^2)

 

  ② 减常因子: T(n) = T(n/b) + f(n), eg. 

                             T(n) = T(n/2) + C, C为常数 ------ T(n) = O(log2(n))   折半查找

                             T(n) = T(n/2) + n ---------------- T(n) = O(n^2)

 

  ③ 减可变规模: 例如 求最大公约数 gcd(m,n) = gcd(n, m%n)

 

   总之,在使用分治法或者减治法时,应当合理地进行子问题分割,尽量使分和合的过程简单, 使 f(n) 保持线性,这样,才能达到分治法和减治法的高效。

 

   三、动态规划法

   动态规划法的解通常具有如下递推形式: S(n) = G(S(n-i) , f(n)) i = 1,2,……,n 其中  S(n-i) 和 S(n-j) 往往具有重合的子问题  S(n-k), k>i,k>j,此时,可以采用自底向上的迭代法或基于记忆功能的递归法来一步步地构造解S(1),S(2),……,S(n)。由于避免了对子问题的重复求解,因此,动态规划法显示出了比较高的效率。

    动态规划法与分治法的区别: 分治法所分成的子问题实例通常没有重合的子问题实例,而动态规划法所分成的子问题实例通常有重合的地方,而且,动态规划法的最优解往往蕴含着子问题实例的最优解,因此,可以由子问题实例的最优解逐步构造最终的最优解。

 

   动态规划法的典型例子: 求斐波那契数列、求二项式系数、背包问题、最长子序列问题、矩阵连乘问题。

 

    四、变治法

    变治法基于问题转化的思想,将所要求解的问题实例转化为更为特殊形式的问题实例,或者已被求解的其它问题实例。变治法要求对问题之间的联系具备非凡的洞察力。

    典型的变治法包括预排序。将待处理列表先进行排序,然后再对有序列表进行其它处理。比如查找问题, 如果采用顺序查找,则需要O(n)的时间,而先排序后二分查找,则需要至少O(nlog2(n)),显然,预排序似乎是多余的。不过,如果要进行多次查找,则预排序就开始起作用了。假设要对1000条记录进行 C 次查找, 则有 Cn > nlog2(n) + Clog2(n), n=1000 可解得 C > 10. 也就是说,仅仅要查找十次以上,预排序就开始起作用了。

    另一个变治法的简单例子是求解最小公倍数lcm(m,n)。如果你知道 lcm(m,n) = m*n/gcd(m,n) , gcd(m,n) 是最大公约数, 有一个非常高效的欧几里得算法,那么,这个问题就显得容易得多了。前提是,你要能够具备丰富的知识和良好的洞察力。该问题的最简单的算法是,从1开始,1,2,……,n, 逐个计算直到能被m和n整除为止。 效率为 O(mn); 较好的算法是,取其中最大数 max = max(m,n), 从1开始,逐个进行相乘,max, 2max,……,kmax, 直到能被n 整除位置,效率为 O(min(m,n)).

 

      采用变治法往往可以收到意想不到的效果。要善于进行问题分析,转化问题。

 

     五、 时空权衡

     时空权衡基于“空间充裕廉价,时间宝贵”以及“空间极其有限,时间要求不高”的思想,要么以牺牲空间的代价换取时间的高效,要么以牺牲时间的代价换取空间的要求。前者在桌面PC领域更加常见,后者在单片机、嵌入式等产品中体现较明显。

 

     典型的例子:

     计数排序: 使用一个数组来存储比元素大或者小的元素个数,根据这些值来确定元素在最终列表中的位置。

     散列法:    使用数组和链表的结合来实现字典的高效。首先,采用某种哈希函数hash(x) 将 基于键key 的记录映射到哈希表的相应位置hashtable[hash(key)]中,如果有冲突(多个键的记录被映射到同一个下标),则使用附在该下标的链表存储冲突的记录。这也是一个显示数据结构组合应用能够产生强大功能的令人深刻的例子。

     递归/非递归遍历: 使用额外的栈或者标记来存储遍历过程中已经遍历了的元素,从而避免重复遍历。

 

     六、 贪婪技术

     总是从当前的可行选择中选取局部最优的部分解,一步步构造最终的解。贪婪技术逐步构造最优解的三个条件:

     1. 可行:满足问题的约束条件

     2. 局部最优:在当前可行的选择中总是选择局部最优的部分解。

     3. 不可撤销:一旦做出选择,就不可撤销。    

 

     贪婪技术的典型例子: 找硬币、最小生成树的 Prim算法 和 Kruskal算法 , 单起点最短路径的 Dijskra算法。

 

     七、 蛮力法

     不要小看蛮力法。蛮力法总是可以找到一个解,虽然这个解通常并不实用。蛮力法的作用是,找到一个解,然后对其进行改进。蛮力法有时可以给人带来惊喜。比如 子字符串匹配(判断字符串str 中是否含有子字符串pattern) 的蛮力法版本,其最差效率O(mn),平均效率为O(m), m,n  分别是 str 和 pattern 的长度。也就是说,蛮力法版本的子字符串匹配算法是比较实用的。

 

     蛮力法的典型例子: 排列组合问题的穷举查找法。

 

      八、 迭代改进

      迭代改进法,总是最先找到一个可行解,然后通过一些小的、局部的变化来改进这个可行解,使之趋向最优解。迭代改进的关键是稳定性、收敛性。

      迭代法的典型例子: 遗传算法

 

      九、 回溯法

      回溯法是穷举查找法的改进版本,穷举查找法通常先列举出所有的候选解,然后根据目标问题来找出满足要求的解或最优解。而回溯法则“聪明”了一些,一步一步地构造解的分量,一旦有分量不可行,则此路不通,马上转道。回溯法通常根据问题构造一棵状态树,通过遍历状态树的结点来求解。叶子节点代表无解或可行解。

      回溯法的典型例子: n 皇后问题,图的深度遍历

 

      十、 分枝定界技术

      分枝定界技术与回溯法非常相似。由于我还没有学到这里,因此,暂不作讨论。分枝定界技术常用来求解最优化问题。

 

      十一、 求近似解

      退而求其次,不失为一种明智的选择。如果花20分钟可以获得旅行商问题的实用解,那么,再多花十个小时,使解获得0.1%的改进,是否有必要?对于理论工作者来说,确实如此。但如果是实践工作者,我会选择前者。要学会权衡投资/收益比。

 

       结语:

      总结算法,应用算法。算法,当之无愧是计算机科学的灵魂和基石。无论是硬件还是软件,无论是专业还是生活,算法无处不在。不能因为暂时用不到就贬低它的价值,——事实上,这是一种掩耳盗铃的做法。你不用,别人总会用的,而且,别人可以从中体验乐趣,获取丰厚的回报。千万不要跟算法过不去。     

 

       参考书籍:

       1. 《算法设计与分析基础》[第2版] ,  (美)Anany Levitin , 潘彦译, 2004/06, 清华大学出版社

       2. 《算法导论》[第2版],Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest,Clifford Stein, 潘金贵 等译, 2006/09, 机械工业出版社

常用算法设计技术总结