算法思想
算法:
任何良定义的计算过程,该过程取某个值或值的集合作为输入并产生某个值或值的集合作为输出。也可以理解为把输入转换为输出的计算步骤的一个序列。一个程序的灵魂。
程序:
算法+数据结构=程序
程序的设计与分析:
设计:算法思想,比如分治,贪心,动态规划等等;
分析:时间和空间两个层面衡量程序的优劣。
时间复杂度:会对用户体验性。
降低时间复杂度:
1.采用更高效的算法,比如采用动态规划,归并的方式;
2.利用空间换时间,可以采用哈希表,栈,队列进行缓存中间结果。
空间复杂度:面对嵌入式开发,会增加成本。硬件的发展遵循摩尔定律,数个月就会翻一番。
两者之间好比鱼和熊掌,不可兼得,达到平衡即可,根据实际项目而定,我们应该询问面试官,根据需求定方案。我们尽可能的用空间换时间,用户体验度较为重要。
1.枚举算法
思想:在面对任何问题时它会去尝试每一种解决方法。将问题的所有可能的答案一一列举,然后根据条件判断此答案是否合适,保留合适的,丢弃不合适的。在进行归纳推理时,如果逐个考察了某类事件的所有可能情况,因而得出一般结论,那么这个结论是可靠的,这种归纳方法叫作枚举法。枚举算法一般使用while、for循环实现。
使用枚举算法解题的基本思路如下:
① 确定枚举对象、枚举范围和判定条件;
② 逐一列举可能的解,验证每个解是否是问题的解。
枚举算法一般按照如下3个步骤进行:
① 题解的可能范围,不能遗漏任何一个真正解,也要避免有重复;
② 判断是否是真正解的方法;
③ 使可能解的范围降至最小,以便提高解决问题的效率。
for (int i=0;i<length;i++){ //loop,range from 0 to length-1
if (i==?){ //condition
m //operate
}
}
2.递推算法
通过已知条件,利用特定关系得出中间推论,直至得到结果的算法。递推算法分为顺推和逆推两种。
① 顺推法:从已知条件出发,逐步推算出要解决问题的方法。例如斐波那契数列就可以通过顺推法不断递推算出新的数据;
② 逆推法:从已知的结果出发,用迭代表达式逐步推算出问题开始的条件,即顺推法的逆过程。
3.递归算法
递归算法对解决大多数问题是十分有效的,它能够使算法的描述变得简洁而且易于理解。递归算法有如下3个特点。
① 递归过程一般通过函数或子过程来实现;
② 递归算法在函数或子过程的内部,直接或者间接地调用自己的算法;
③ 递归算法实际上是把问题转化为规模缩小了的同类问题的子问题,然后再递归调用函数或过程来表示问题的解。
在使用递归算法时,读者应该注意如下4点。
① 递归是在过程或函数中调用自身的过程;
② 在使用递归策略时,必须有一个明确的递归结束条件,这称为递归出口;
③ 递归算法通常显得很简洁,但是运行效率较低,所以一般不提倡用递归算法设计程序;
④ 在递归调用过程中,系统用栈来存储每一层的返回点和局部量。如果递归次数过多,则容易造成栈溢出,所以一般不提倡用递归算法设计程序。
递归和循环:
递归的本质是将大的复杂问题转换成多个小的子问题,虽然递归的方式会使得代码较为简洁,但是会占用一定的内存和时间。
我们可以采用递归的思路来思考分析问题,花一些存储空间来保存一定的中间结果基于循环实现。
动态规划算法的分析和实现都是分这两个步骤完成的。
递归的通用代码:
4.分治算法
思想:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
策略:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
分治法的基本步骤:
分治法在每一层递归上都有三个步骤:
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
step3 合并:将各个子问题的解合并为原问题的解。
它的一般的算法设计模式如下:
Divide-and-Conquer(P)
if |P|≤n0 then return(ADHOC(P)) #如果P的规模小于一个阈值,则直接解决 #将P分解为较小的子问题 P1 ,P2 ,...,Pk for i←1 to k do yi ← Divide-and-Conquer(Pi) #△ 递归解决Pi T ← MERGE(y1,y2,...,yk) #△ 合并子问题 return(T)
分治法算法分析:
生成k个子问题,每个子问题的规模是原问题规模的1/m,分解和合并步骤总共花费时间为f(n),用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
T(n)= k T(n/m)+f(n)
通过迭代法求得方程的解:
递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。通常假定T(n)是单调上升的,从而当mi≤n<mi+1时,T(mi)≤T(n)<T(mi+1)。
通过主方法求得方程的解:
用f(n)和n倍log以m为底k为幂的值进行比较,最终结果选大即可。
举例子,极端的分了n个子问题,每个子问题都是以前的一半,采用主定理方法求解是nlgn和f(n)进行比较,选择大作为最终的结果。
3、找到求解的递归函数式后(各种规模或因子),设计递归程序即可。
5.动态规划(Dynamic programming)
一、概念、思想和策略:
每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。用来解决最优化问题。首先具有最优子结构,其次子问题重叠。
思想和策略:基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
即采用了分治思想+解决冗余。相较于贪心算法思想是每步的选择是局部最优,而动态规划是全局最优。
二、分治法和动态规划特点:
分治法:互不相交的子问题,递归求解。
动态规划:子问题重叠,具有公共子问题。
三、动态规划的四个步骤:
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
四、能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。采用备忘录机制保存中间过程产生的值。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
五、算法实现的说明
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
六、动态规划算法基本框架
代码 for(j=1; j<=m; j=j+1) // 第一个阶段 xn[j] = 初始值; for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段 for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式 xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])}; t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案 print(x1[j1]); for(i=2; i<=n-1; i=i+1) { t = t-xi-1[ji]; for(j=1; j>=f(i); j=j+1) if(t=xi[ji]) break; }
6、贪心算法
一、基本概念:
二、贪心算法的基本思路:
三、贪心算法适用的问题
四、贪心算法的实现框架
五、贪心策略的选择
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
7.回溯法
1、概念
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
2、基本思想
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
3、用回溯法解题的一般步骤:
(1)针对所给问题,确定问题的解空间:
首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
4、算法框架
(1)问题框架
设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。
下图为深度优先搜索算法。
(2)非递归回溯框架
1:int a[n],i; 2: 初始化数组a[]; 3: i = 1; 4: while (i>0(有路可走) and (未达到目标)) // 还未回溯到头 5: { 6: if(i > n) // 搜索到叶结点 7: { 8: 搜索到一个解,输出; 9: } 10: else // 处理第i个元素 11: { 12: a[i]第一个可能的值; 13: while(a[i]在不满足约束条件且在搜索空间内) 14: { 15: a[i]下一个可能的值; 16: } 17: if(a[i]在搜索空间内) 18: { 19: 标识占用的资源; 20: i = i+1; // 扩展下一个结点 21: } 22: else 23: { 24: 清理所占的状态空间; // 回溯 25: i = i –1; 26: } 27: }
(3)递归的算法框架
回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:
1: int a[n]; 2: try(int i) 3: { 4: if(i>n) 5: 输出结果; 6: else 7: { 8: for(j = 下界; j <= 上界; j=j+1) // 枚举i所有可能的路径 9: { 10: if(fun(j)) // 满足限界函数和约束条件 11: { 12: a[i] = j; 13: ... // 其他操作 14: try(i+1); 15: 回溯前的清理工作(如a[i]置空值等); 16: } 17: } 18: } 19: }
八、分支限界
一、基本描述
类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。但在一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出T中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
(1)分支搜索算法
所谓“分支”就是采用广度优先的策略,依次搜索E-结点的所有分支,也就是所有相邻结点,抛弃不满足约束条件的结点,其余结点加入活结点表。然后从表中选择一个结点作为下一个E-结点,继续搜索。
选择下一个E-结点的方式不同,则会有几种不同的分支搜索方式。
1)FIFO搜索
2)LIFO搜索
3)优先队列式搜索
(2)分支限界搜索算法
二、分支限界法的一般过程
由于求解目标不同,导致分支限界法与回溯法在解空间树T上的搜索方式也不相同。回溯法以深度优先的方式搜索解空间树T,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树T。
分支限界法的搜索策略是:在扩展结点处,先生成其所有的儿子结点(分支),然后再从当前的活结点表中选择下一个扩展对点。为了有效地选择下一扩展结点,以加速搜索的进程,在每一活结点处,计算一个函数值(限界),并根据这些已计算出的函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。
分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。问题的解空间树是表示问题解空间的一棵有序树,常见的有子集树和排列树。在搜索问题的解空间树时,分支限界法与回溯法对当前扩展结点所使用的扩展方式不同。在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,那些导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被子加入活结点表中。此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所求的解或活结点表为空时为止。
和广度优先搜索策略类似,下图为广度优先搜索策略代码:
三、回溯法和分支限界法的一些区别
有一些问题其实无论用回溯法还是分支限界法都可以得到很好的解决,但是另外一些则不然。也许我们需要具体一些的分析——到底何时使用分支限界而何时使用回溯呢?
回溯法和分支限界法的一些区别:
方法对解空间树的搜索方式存储结点的常用数据结构结点存储特性常用应用
回溯法深度优先搜索堆栈活结点的所有可行子结点被遍历后才被从栈中弹出找出满足约束条件的所有解
分支限界法广度优先或最小消耗优先搜索队列、优先队列每个结点只有一次成为活结点的机会找出满足约束条件的一个解或特定意义下的最优解
总结
沟通、学习能力
沟通:不卑不亢和面试官交流,逻辑清晰,详略得当,清晰有条理的表达自己。重点突出,观点明确。知之为知之,不知为不知,不清楚的概念勇敢的承认,不能不懂装懂。
学习:IT技术更新较快,考察时候通过提出新的概念,应该通过思考,提问,在思考,理解的基础上解决问题。带着问题去学习解决问题,明确目标是什么,去解决该问题。也可理解为知识迁移能力,举一反三,知识肯定是学不完的,不能说等学完了某个知识再去解决问题,新的技术都是基于原有的知识点上发展而来,应该找到原有知识与现有知识的联系,用我们已经掌握的知识背景去更快速的理解新的知识,转换为技能,应用到实际工作中去,万变不离其宗。
提问:提问是为了搞清楚我们的目标是什么,当我们对问题感兴趣,对未知的世界求知欲望较为强烈,便会不断的提问,更新自己的知识库。