编程斐波那契数列_通过斐波那契序列记忆和制表法进行动态编程的简介

编程斐波那契数列

Lately I have been studying algorithms and data structures while trying to prepare for technical interviews. Some of it comes easier than the rest, but I always enjoy a good challenge. I love the whiteboard problems that feel like a puzzle you’re so close to putting together. At some point it just clicks in your brain and you know exactly what you need to do. For example, I really enjoyed my first problem using the Fibonacci sequence. If you are not familiar, a general Fibonacci problem could look something like this:

最近,我在尝试准备技术面试时一直在研究算法和数据结构。 其中有些比其他的要容易一些,但我始终面临着很好的挑战。 我喜欢白板上的问题,感觉就像是一个难题,您很难将它们放在一起。 在某个时候,它只是在您的大脑中发出咔嗒声,您便确切地知道需要做什么。 例如,我真的很喜欢使用斐波那契数列的第一个问题。 如果您不熟悉,一般的斐波那契问题可能看起来像这样:

You have an array with the following values: 
array = [0,1,1,2,3,5,8,13,21,34]Given any index, n, determine what the value at that index in the array would be, array[n].

At the time, I had not been exposed to the sequence so the problem was very challenging for me to solve. After the challenge was over, I wanted to better understand the solution to the sequence. After some practice, I worked it out to the following:

当时,我还没有接触过这个序列,所以这个问题对我来说很难解决。 挑战结束后,我想更好地了解序列的解决方案。 经过一些练习,我将其解决如下:

The relationship between values in the array is:
array[n] = array[n-1] + array[n-2]function fibonacci(n){
if(n <= 2) return 1
return fibonacci(n-1) + fibonacci(n-2)
}

I was satisfied with this solution. It made sense to me, passed my tests, and utilized recursion, which I had just learned (if you want a refresher on recursion, check out my blog post here). Later on, I realized there was a problem with my solution. If I ran my solution with a number above 35, it would take a while to return a value, and if I ran it with a number above 50, it would freeze up my computer or crash the application. What was happening? Well, the time complexity for this solution is very bad. Its O(2^n). So as my input grew, the amount of work my computer had to do grew exponentially. I had to reduce the time complexity somehow. This is when I learned about Dynamic Programming.

我对此解决方案感到满意。 这对我来说很有意义,通过了我的测试,并利用了我刚刚学到的递归(如果您想对递归进行复习,请在此处查看我的博客文章)。 后来,我意识到我的解决方案存在问题。 如果我使用大于35的数字运行解决方案,则需要一段时间才能返回值;如果我使用大于50的数字运行解决方案,则会冻结计算机或使应用程序崩溃。 发生了什么事? 嗯,此解决方案的时间复杂度非常差。 它的O(2 ^ n)。 因此,随着输入的增加,计算机必须完成的工作量成倍增加。 我不得不以某种方式降低时间复杂度。 这是我了解动态编程的时候。

Dynamic Programming is a problem solving method that takes advantage of overlapping subproblems and optimal structure in order to reduce the amount of time or work it takes in order to solve a problem. It optimises the solution. How does this apply to Fibonacci? Well, the fibonacci problem has optimal structure and overlapping subproblems, because the same input should always give the same result, and there are repeated subproblems. For example, fibonacci(6) will always return 8. But look how this is calculated through my recursive function:

动态编程是一种解决问题的方法,它利用重叠的子问题和最佳结构来减少解决问题所需的时间或工作量。 它优化了解决方案。 这如何适用于斐波那契? 好吧,斐波那契问题具有最优的结构和重叠的子问题,因为相同的输入应始终给出相同的结果,并且存在重复的子问题。 例如,fibonacci(6)将始终返回8。但是请看一下如何通过我的递归函数计算得出:

fib(6) =
fib(5) + fib(4) =
fib(4) + fib(3) + fib(3) + fib(2) =
fib(3) + fib(2) + fib(2) + fib(1) + fib(2) + fib(1) + 1 =
fib(2) + fib(1) + 1 + 1 + 1 + 1 + 1 + 1 =
1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
8

There’s an issue here. The sequence will always return the same number given the same input, so why do I make my computer run fib(4) twice, fib(3) three times, and fib(2) four times? 6 was a very small input, can you imagine how many time I would make my computer run fib(3) if my original input was fib(30). We’re programmers, we don’t like repeating ourselves in our code and we don’t like repeating work either.

这里有个问题。 给定相同的输入,该序列将始终返回相同的数字,那么为什么要让我的计算机运行两次fib(4),fib(3)三次和fib(2)四次? 6是非常小的输入,您能想象如果我的原始输入是fib(30)时,我可以让计算机运行fib(3)多少次。 我们是程序员,我们不喜欢在代码中重复自己,也不喜欢重复工作。

Enter Memoization. Not ‘memorization’. That’s what I initially assumed it was, but that’s not a terrible way to think of it. You are kind of ‘memorizing’ the return value for inputs you’ve already ran, that way, if your computer tries to calculate fib(4) for a second time, like it does when you input fib(6), you stop it from recursively breaking it down all the way again, and just say “Hey you’ve already done this! The answer is 3 remember!?”.

输入备忘。 不是“背诵”。 那就是我最初的想法,但这并不是想到它的糟糕方法。 您有点“记住”已经运行的输入的返回值,这样,如果您的计算机尝试第二次计算fib(4),就像您输入fib(6)一样,就停止它再次递归地将其分解,然后说“嘿,您已经做到了! 答案是3记住!?”。

You have to keep in mind that your computer isn’t magic. It’s computing the answer to a fib(n) call one at a time. So, unlike the code snippet above, where we are reading it left to right in rows, your computer is working more so in columns. It doesn’t look to the next fib(n) call until it has a definitive answer for what the first one is. Let me try to visualize it. If you call fib(6), here’s what your computer will run in order of its call stack, with the bold function being the next step your computer runs on the next line:

您必须记住,您的计算机不是魔术。 它的计算回答一次一个 FIB(N)调用之一 。 因此,与上面的代码段不同,我们在行中从左到右读取它,而在列中,您的计算机工作得更多。 在找到第一个是什么的明确答案之前,它不会寻找下一个fib(n)调用。 让我尝试形象化它。 如果调用fib(6),则计算机将按照调用堆栈的顺序运行, 粗体函数是下一步,计算机将在下一行运行:

fib(6) = fib(5) + fib(4)
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = 1 //YAY we removed a fib call from the stack and bubble up
fib(3) = 1 + fib(1) //we now get to run the 2nd fib call from herefib(1) = 1 //another fib call done, we bubble up again!
fib(3) = 1 + 1 = 2 //Now those first 2 calls let us finish this call
fib(4) = 2 + fib(2) //We solved fib(2) before but are doing it again
fib(2) = 1 //same answer we got last time we did fib(2)
fib(4) = 2 + 1 = 3 //and now we bubble up again to finish this call
fib(5) = 3 + fib(3) //we already solved fib(3) once! Why again?!
fib(3) = fib(1) + fib(2) //See how repetitive this is?
fib(1) = 1 //same solution we got the first time
fib(3) = 1 + fib(2) //have I made my point yet?
fib(2) = 1 //This isn't new information
fib(3) = 1 + 1 = 2 //yep, fib(3) still equals 2
fib(5) = 3 + 2 = 5 //Finally finish 1/2 of the original problem
fib(6) = 5 + fib(4) //you've got to be kidding me...
fib(4) = fib(3) + fib(2) //we could've finished this a long time ago
fib(3) = fib(1) + fib(2) //all of this is not fun to write out
fib(1) = 1 //We meet again fib(1)
fib(3) = 1 + fib(2) //this is the 4th time we are doing fib(2)
fib(2) = 1 //who would've thought!? (sarcasm)
fib(3) = 1 + 1 = 2 //dejavu
fib(4) = 2 + fib(2) //Yeah sure, a 5th time for good measurefib(2) = 1 //I genuinely hope all this is helpful to you
fib(4) = 2 + 1 = 3
fib(6) = 5 + 3 = 8 //FINALLY WE HAVE AN ANSWER

As you can see, our fib(6) call made our computer run fib(n) 15 times, and only 6 of those fib(n) calls were unique! If we could us memoization (memorization) we could drastically cut down the work and skip almost every repeated fib(n) call:

如您所见,我们的fib(6)调用使我们的计算机运行fib(n)15次,而这些fib(n)调用中只有6个是唯一的! 如果我们可以进行记忆(记忆),则可以大大减少工作量,并跳过几乎所有重复的fib(n)调用:

fib(6) = fib(5) + fib(4)
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = 1 //lets remember that
fib(3) = 1 + fib(1)fib(1) = 1 //lets remember that too
fib(3) = 1 + 1 = 2 //Cool, lets remember that as well
fib(4) = 2 + fib(2) //oh yeah! fib(2) = 1
fib(4) = 2 + 1 = 3 // sweet lets remember that
fib(5) = 3 + fib(3) //oh yeah! fib(3) = 2
fib(5) = 3 + 2 = 5
fib(6) = 5 + fib(4) //oh yeah! fib(4) = 3
fib(6) = 5 + 3 = 8 //DONE

As you can see, we are drastically cutting down the amount of times we are calling fib(n) by remembering our return values. How do we do something like this? How do we use memoization? We do this by persisting an array of return values through our recursive calls, and checking that array every time in order to see if we have already solved that fib(n) before. How do we know where in the array to store and retrieve our values? We simply store the answer to fib(n) at array[n]. Heres what that looks like:

如您所见,我们通过记住返回值来大幅度减少调用fib(n)的时间。 我们如何做这样的事情? 我们如何使用记忆化? 为此,我们通过递归调用保留一个返回值数组,并每次都检查该数组,以查看以前是否已经解决过fib(n)。 我们如何知道数组中存储和检索值的位置? 我们只需将对fib(n)的答案存储在array [n]处。 如下所示:

function fib(n, memo=[]){
if(memo[n] !== undefined) return memo[n];
if(n <= 2) return 1
let num = fib(n-1, memo) + fib(n-2, memo)
memo[n] = num
return num
}

Here’s whats happening. We pass in our array that stores our return values (called ‘memo’ for ‘memoization’) and default it for only the first call as an empty array. Then, we check our memo array to see if we already have an answer stored in its nth spot; if so, we return it. We then return 1 if n is either 1 or 2 just like we originally did. Then, we recursively call fib(n-1) and fib(n-2) like we use to, but this time, passing our persistent memo array along with it, and we set the sum to a variable. We then place that sum in our memo array at the nth spot, and return the sum.

这是怎么回事。 我们传入存储了我们的返回值的数组(对于“ memoization”来说称为“ memo”),并且仅在第一次调用时将其默认为空数组。 然后,检查备忘录数组以查看是否已经在第n个位置存储了答案。 如果是这样,我们将其退回。 然后,如果n等于1或2,则返回1,就像我们最初所做的那样。 然后,我们像以前一样递归调用fib(n-1)和fib(n-2),但是这次,将持久性备忘录数组与其一起传递,并将和设置为变量。 然后,我们将该总和放在备忘录数组的第n个位置,并返回总和。

Image for post
Photo by Keith Luke on Unsplash
Keith LukeUnsplash拍摄的照片

So with this solution, any time fib(n) is called where n has already been solved once, memo[n] will already hold the answer, and return it, instead of continuing to recursively call fib(n-1) + fib(n-2). This brings the time complexity from O(2^n) to O(n) which is a lot better. This means where fib(39) use to take my computer over a minute to solve, it now takes less than a second!

因此,使用此解决方案,只要在n已经被解决一次的任何时间调用fib(n),memo [n]就已经保存了答案并返回了答案,而不是继续递归调用fib(n-1)+ fib( n-2)。 这使时间复杂度从O(2 ^ n)到O(n)更好。 这意味着在使用fib(39)花费一分钟时间来解决我的计算机问题时,现在只需不到一秒钟的时间!

Don’t celebrate too soon…. Go ahead and try to run fib(8349) on repl.it. It will return infinity in less that a second. So the time is pretty good, and the returned number is not ideal but hey we’re using javascript and thats the way it is. The real problem is when you add 1 to n…run fib(8350) and you get:

不要过早庆祝…。 继续尝试在repl.it上运行fib(8349)。 它会在不到一秒钟的时间内返回无穷大。 所以时间非常好,返回的数字也不理想,但是嘿,我们正在使用javascript,那就是它的样子。 真正的问题是,当您将n加1时…运行fib(8350)并得到:

RangeError: Maximum call stack size exceeded
Image for post
Photo by Kristopher Roller on Unsplash
Kristopher RollerUnsplash拍摄的照片

Classic stack overflow. We have way too many recursive calls waiting on our stack and its too much to handle. We’ve been betrayed by memoization. Classic Revenge of the Sith — ‘You were suppose to be the chosen one’.

经典堆栈溢出。 我们在堆栈上等待的递归调用太多,无法处理。 我们被回忆背叛了。 西斯经典复仇-“您原本是被选中的人”。

It’s okay, here comes ‘Tabulation’ to save the day! Tabulation is a process in which you store the results in a table (usually an array) while iterating instead of recursively calling. This saves on space complexity and prevents a stack overflow. Here’s what it looks like:

没关系,这里有“制表”来节省时间! 制表是在迭代过程中而不是递归调用时将结果存储在表(通常是数组)中的过程。 这节省了空间复杂性并防止了堆栈溢出。 看起来是这样的:

function fib(n){
if(n <= 2) return 1
const fibNums = [0,1,1]
for(let i = 3; i <= n; i++){
fibNums[i] = fibNums[i-1] + fibNums[i - 2]
}
return fibNums[n]
}

This time, instead of passing an array as an argument, it is assigned in the function just after the edge case. It is initialized with [0,1,1] because we know those values and we need to give our loop something to calculate off of. In our loop, we start i = 3 since we already filled in the array indices 0–2. We then run the loop while i is less than or equal to n. This is key for reaching the nth value correctly. We are now iterating up in the loop, incrementing i each time, instead of recursively breaking down. This ensures we are not recomputing any value, and allows us to return the current value in O(n) time, without causing a stack overflow. So we can now run fib(8350) without issue.

这次,不是在传递数组作为参数的情况下,而是在边缘情况之后在函数中分配了它。 它使用[0,1,1]进行初始化,因为我们知道这些值,并且需要给我们的循环一些计算的依据。 在我们的循环中,由于我们已经填写了数组索引0–2,因此开始i = 3。 然后在i小于 等于 n时运行循环。 这是正确达到第n个值的关键。 现在,我们在循环中迭代,每次递增i,而不是递归分解。 这确保了我们不重新计算任何值,并允许我们以O(n)时间返回当前值,而不会引起堆栈溢出。 因此,我们现在可以毫无问题地运行fib(8350)。

As we get better at problem solving, we are able to identify patterns, and come up with solutions on our own with ease. We may be able to write a solution that passes every test case, but as programmers, thats not where our job ends. We need to care about time and space complexity…which is complex. A great way to try to improve a solution’s time complexity, is to take a dynamic programming approach. Look for smaller problems within your problem. Look for repeated calculations, loops, and function calls. Try to implement memoization when working recursively or tabulation when looping and attempt to improve your BigO. Balance the trade offs between time complexity and space complexity, as one can often aid the other. I hope you found these concepts as interesting as I did, and I hope it sparks another lightbulb next time you’re facing off against another algorithm. Thanks for reading!

随着我们在解决问题方面的能力越来越强,我们能够识别模式,并轻松地自己提出解决方案。 我们也许可以编写一个通过每个测试用例的解决方案,但是作为程序员,那不是我们工作的终点。 我们需要关心时间和空间的复杂性……这是复杂的。 尝试提高解决方案时间复杂度的一种好方法是采用动态编程方法。 在您的问题中寻找较小的问题。 寻找重复的计算,循环和函数调用。 递归工作时尝试实现备忘录,或在循环时尝试列表化,并尝试改进BigO。 在时间复杂度和空间复杂度之间权衡取舍,因为一个通常可以帮助另一个。 我希望您能像我一样发现这些概念很有趣,并且希望您下次遇到另一种算法时,它会激发另一个灯泡。 谢谢阅读!

翻译自: https://medium.com/dev-genius/an-introduction-to-dynamic-programming-through-the-fibonacci-sequence-memoization-and-tabulation-67a8624be61a

编程斐波那契数列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值