斐波那契与二分查找
引子
今天我们来探讨一个有趣的问题,如果兔子遇到莱昂纳多·斐波那契,那明年有几只兔子呢?
我们知道意大利数学家莱昂纳多·斐波那契在《算法全书》中提出了 斐波那契兔子问题(Fibonacci rabbit problem) 一道著名数列难题~
注意,我们的兔永远不会去世~而且一个月后就成年,成年之后就继续生一对
- 第一个月:一小兔1
- 第二个月:小兔1长成大兔1
- 第三个月:大兔1生了一只小兔2
- 第四个月:大兔1又生了一直小兔3,小兔2长成了大兔2
- ......
斐波那契兔子数列问题建模
对上述的斐波那契“兔”问题进行建模可以得到斐波那契数列:
1,1,2,3,5,8,13,21,34,55,89
数列从第三项开始,每一项等于前两项之和,我们要求明年月亮上拥有多少兔,也就是数列的第12项~
我们将数学问题转换成计算机编程问题:
写一个函数,输入 nnn ,求斐波那契(Fibonacci)数列的第 nnn 项(即 F(N)F(N)F(N))。
斐波那契数列的定义如下: F(0)=0,F(1)=1F(0) = 0, F(1) = 1F(0)=0,F(1)=1 F(N)=F(N−1)+F(N−2)F(N) = F(N - 1) + F(N - 2)F(N)=F(N−1)+F(N−2), 其中 N>1N > 1N>1.
斐波那契数列由 第三项 开始,之后的斐波那契数就是由之前的两数相加而得出。
解析
法一:暴力递归
就直接暴力递归,就可以算出我们需要的答案
创造一个函数,这个函数的作用就是查找当月的兔子数量,而当月的兔子数量又是前两个月的兔子之和,所以我们可以在返回值里面递归调用这个函数,输入n-1与n-2,也就是求前两个月的兔子,但是这个递归要有一个退出条件,如果没有的话,就会无限调用堆栈,造成死循环。而在这个方法里面退出的条件就是找到第0个月或者第一个月的时候,直接返回1,这里返回一的意思是已经找到最初的兔子数量,然后这里递归到最后的fib函数已经有返回值了,会把这个返回值直接返回上一层fib的调用,这样一层层逆着将返回值全部返回来,就求出目前月的兔子数量
首先一个问题就是这里重复递归的次数实在是太多了,疯狂的调用堆栈,导致计算机的性能急剧下降,比如说我要求fib(5), 如图可视,我们重复求解了多次fib(2)和fib(3)其实这是没有必要的,我们可以用一个缓存cache,来将我们求过了的fib(n)保存在缓存中,下次用到他的时候直接读取他的值就可以了,而不是再重新递归求值。
递归 + 缓存
法二:循环三指针
由于从第三个月开始,当月兔子等于前两个月兔子之和,那么我们可以创造三个指针,第a个指针指向第一个月,第b个指针指向第二个月,第c个指针指向第三个月,第三个月的兔子等于一二月兔子加起来,就是c指针等于ab指针之和。当月数增加的时候,只需让abc三个指针向后移动一位即可,随后三个指针发生相应的变化,a指针到了之前b指针的位置,所以就是a=b,b指针到了c指针的位置,所以是b=c,c指针还是等于ab之和,所以c=a+b,这样将其带入之后,就可以得到第n个月之后的兔子,同时因为循环不会疯狂调用堆栈,多以也不会产生效率底下的问题。
二分查找原理
二分查找(Binary Search)算法,也叫折半查找算法。二分查找的思想非常简单,有点类似分治的思想。二分查找针对的是一个有序的数据集合,每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0。
为了方便理解,我们以数组1, 2, 4, 5, 6, 7, 9, 12, 15, 19, 23, 26, 29, 34, 39,在数组中查找26为例,制作了一张查找过程图,其中low标示左下标,high标示右下标,mid标示中间值下标
二分查找的过程就像上图一样,如果中间值大于查找值,则往数组的左边继续查找,如果小于查找值这往右边继续查找。二分查找的思想虽然非常简单,但是查找速度非常长,二分查找的时间复杂度为O(logn)。虽然二分查找的时间复杂度为O(logn)但是比很多O(1)的速度都要快,因为O(1)可能标示一个非常大的数值,比例O(1000)。
局限性
二分查找依赖数组结构
二分查找需要利用下标随机访问元素,如果我们想使用链表等其他数据结构则无法实现二分查找。
二分查找针对的是有序数据
二分查找需要的数据必须是有序的。如果数据没有序,我们需要先排序,排序的时间复杂度最低是 O(nlogn)。所以,如果我们针对的是一组静态的数据,没有频繁地插入、删除,我们可以进行一次排序,多次二分查找。这样排序的成本可被均摊,二分查找的边际成本就会比较低。
但是,如果我们的数据集合有频繁的插入和删除操作,要想用二分查找,要么每次插入、删除操作之后保证数据仍然有序,要么在每次二分查找之前都先进行排序。针对这种动态数据集合,无论哪种方法,维护有序的成本都是很高的。
所以,二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中。针对动态变化的数据集合,二分查找将不再适用
数据量太小不适合二分查找
如果要处理的数据量很小,完全没有必要用二分查找,顺序遍历就足够了。比如我们在一个大小为 10 的数组中查找一个元素,不管用二分查找还是顺序遍历,查找速度都差不多,只有数据量比较大的时候,二分查找的优势才会比较明显。
数据量太大不适合二分查找
二分查找底层依赖的是数组,数组需要的是一段连续的存储空间,所以我们的数据比较大时,比如1GB,这时候可能不太适合使用二分查找,因为我们的内存都是离散的,可能电脑没有这么多的内存。
代码实现
递归
递归的概念
在程序中函数直接或间接调用自己
- 直接调用自己
- 间接调用自己 跳出结构,有了跳出才有结果
递归的思想
递归的调用,最终还是要转换为自己这个函数
- 如果有个函数foo,如果他是递归函数,到最后问题还是转换为函数foo的形式
- 递归的思想就是将一个未知问题转换为一个已解决的问题来实现
递归的步骤(技巧)
- 假设递归函数已经写好
- 寻找递推关系
- 将递推关系的结构转换为递归体
- 将临界条件加入到递归体中