当前位置: 首页 > 科技观察

全面的动态规划入门指南,助你赢得技术面试_0

时间:2023-03-13 20:40:09 科技观察

译者|赵庆尧审稿人|动态规划通常是技术面试、设计审查会议或与开发人员互动中的关键话题。本文将解释什么是动态规划以及为什么要使用它。为了清楚地解释动态规划的概念,我将使用Swift代码示例来说明,其他语言也适用。一种思维方式与特定的编码语法或设计模式不同,动态规划不是一种特定的算法,而是一种思维方式。因此,具体到实现层面,动态规划有很多表现形式。动态规划的核心思想是将一个大问题分解成多个部分。同时,推荐的动态规划方式也需要数据的存储和复用,以提高算法效率。软件开发中的许多问题都可以用各种形式的动态规划来解决。关键是知道何时使用简单变量以及何时使用复杂数据结构或算法来设计最佳解决方案。例如,代码变量可以被认为是动态规划的一种基本形式。变量的目的是在内存中保存一个特定的位置,以便稍后调用。//非记忆函数funcaddNumbers(lhs:Int,rhs:Int)->Int{returnlhs+rhs}//记忆函数funcaddNumbersMemo(lhs:Int,rhs:Int)->Int{letresult:Int=lhs+rhsreturnresult}addNumbersMemo上面做了简单的介绍,而动态规划解决方案旨在保留以前看到的值,一种称为记忆的设计技术。代码挑战-数字对多年来,我对数十名准备在Apple、Facebook和亚马逊等顶级公司面试的开发人员进行了模拟面试,大多数人都非常乐意跳过那些可怕的现场白板面试或带回家编程项目。但事实是,其中许多问题旨在测试一个人对计算机科学基础知识的基本理解。例如下面的情况:/*在一次技术面试中,给你一个数组,需要找到一对等于给定目标值的数字。数字可以是正数、负数或两者兼而有之。你能设计一个在O(n)线性时间内工作的算法吗?letsequence=[8,10,2,9,7,5]letresults=pairValues(sum:11)=//returns(9,2)*/对于开发者来说,解决问题通常有很多种方法。在这种情况下,我们的目标是找到数字对以达到预期的结果。人们可以用肉眼快速浏览数字序列,很容易找到一对9和2。但是,算法需要检查和比较序列中的每个值,或者开发更精简的解决方案来帮助我们找到了我们正在寻找的价值。接下来,我将分别介绍这两种技术。BruteForce第一种方法是查看第一个值,然后逐个检查每个值以确定差异是否达到目的。例如,我们的算法检查数组中的第一个值8,然后在其余值中找到3(例如11-8=3)。但是我们发现这一组中没有3,那么算法继续以同样的方式寻找下一个值(即例子中的10),直到找到匹配成功的一对。在不深入大O符号的细节的情况下,我们可以认为此类解决方案的平均时间复杂度为O(n^2)或更大,这主要是因为我们的算法的工作方式使得每个值都与其他值进行比较.该过程可以通过如下代码实现:letsequence=[8,10,2,9,7,5]//非记忆法-O(n^2)funcpairNumbers(sum:Int)->(Int,Int){forainsequence{letdiff=sum-aforbinsequence{if(b!=a)&&(b==diff){return(a,b)}}}return(0,0)}memoryApproach接下来,让我们使用记忆的思维方式来解决问题。在执行代码之前,我们需要考虑存储先前计算的值如何帮助简化流程。使用标准数组是可行的,但集合对象(也称为哈希表或哈希表)也可以提供优化的解决方案。//记忆方法-O(n+d)funcpairNumbersMemoized(sum:Int)->(Int,Int){varaddends=Set()forainsequence{letdiff=sum-aifaddends.contains(diff){//O(1)-恒定时间查找return(a,diff)}//storepreviouslyseenvalueelse{addends.insert(a)}}return(0,0)}通过使用记忆方法,我们将之前看到的值添加到集合对象中,将算法的平均运行时效率提高到O(n+d)。熟悉哈希结构的人会知道,插入和检索项目的时间复杂度是O(1)——常数时间。这进一步简化了我们的解决方案,因为集合旨在以优化的方式检索值,而不管它们的大小。斐波那契数列递归是人们在学习各种编程技术时会遇到的一个话题。递归解决方案通过引用自身的模型工作,因此递归技术是通过算法或数据结构实现的。一个著名的递归案例是斐波那契数列,其中每一项等于前两项(0、1、1、2、3、5、8、13、21等)的和:publicfuncfibRec(_n:Int)->Int{ifn<2{returnn}else{returnfibRec(n-1)+fibRec(n-2)}}检查上面代码时,代码并没有报错,按预期实施。不过,这个算法的性能还是有些值得注意的:如上表所示,函数被调用的次数明显增加。与我们之前的示例类似,算法的性能随着输入大小呈指数下降,因为该操作不存储先前计算的值。如果存储的变量是不可访问的,我们可以通过递归的方式获得我们之前需要的值的唯一方法。假设此代码用于生产环境,此功能可能会引入错误或性能错误。让我们重构代码以使用记忆方法:funcfibMemoizedPosition(_n:Int)->Int{varsequence:Array=[0,1]varresults:Int=0vari:Int=sequence.count//trivialcaseguardn>ielse{returnn}//所有其??他情况..whilei<=n{results=sequence[i-1]+sequence[i-2]sequence.append(results)i+=1}returnresults}修改后的方案支持使用存储变量memoization,重构后的代码不再需要递归。前两个值相加,和相加得到结果,结果相加到主数组序列中。虽然算法的性能仍然取决于序列大小,但我们的修改将算法的时间复杂度增加到O(n)-线性时间。此外,我们的迭代解决方案更易于修改、测试和调试,因为只向调用堆栈添加了一个函数,从而降低了内存管理和对象范围的复杂性。总结通过本文我们了解到动态规划并不是一种具体的设计模式,而是一种思维方式。它的目标是创建一个解决方案,保留以前看到的值以提高时间效率。尽管这些示例涵盖了基本算法,但动态规划为几乎所有程序提供了基础,同时使用简单的变量和复杂的数据结构。译者介绍赵庆尧,一位从事驱动开发多年的社区编辑。他的研究兴趣包括安全操作系统和网络安全。曾获陕西赛区数学建模奖,发表网络相关专利。原标题:动态规划的完整初学者指南,韦恩·毕晓普(WayneBishop)