递归行为
“大事化小” 讲的就是一个递归的思想,把一个规模大的问题通过划分成小问题去解决,小问题继续划分
递归的联想理解
递归像是你在金字塔的顶端,要收集最底的宝藏,你每下一层看没有宝藏,找到了去往下一层的门,继续去下一层,层和层之间的楼梯都是一样的,看上去走的行程是一样的,实际上它们是不同的楼梯,等你走到底拿了宝藏,你还要一层一层再爬回去。
递归的基本思想
“有来有回” 以及常听到的 “自己调用自己”
当划分为多个子问题处理时,重要的一点是子问题解决了,才可以继续解决基于子问题的问题
举个例子
一个数组左边界是L,右边界是R,(L和R都是位置),我们想要寻找这个数组的最大值,我们现在将数组一分为二,分别找出数组左部分的最大值,和数组右部分的最大值,最后返回二者之中的最大值。
5,3,1,8,6,2
L,1,2,3,4,R
var maxArr = [5,3,1,8,6,2];
function getMax(arr,L,R){
if(L == R){
return arr[L];
}
var mid = (L+R)/2;
var maxLeft = getMax(arr,L,mid);
var maxRight = getMax(arr,mid+1,R);
return max = Math.max(maxLeft,maxRight);
}
getMax(maxArr,0,maxArr.length-1);
console.log(max);
可以看到提示代码错误:
“Maximum call stack size exceeded” 超过最大调用栈大小
原来系统用的是栈结构来解决这个问题,不过我们先改写代码使它通过随后详细讲解。
我们将var mid = (L+R)/2;
改为var mid = L + ((R - L) >> 1);
打印出数组最大值8
因为L,R表示下标时,L不会溢出,R不会溢出,L+R可能溢出,mid = (L+R)/2算出的mid可能是错误的,这样是不安全的,但mid = (L+(R-L)/2)<R,R不溢出,所以绝对不会有溢出问题,最后用右移运算抖机灵。
深入剖析递归行为
递归就是系统帮你压栈
递归函数getMax(arr,L,R)开始,系统准备一个栈,首先L和R相等吗,相等则说明数组只有一个元素则直接返回,否则继续。首先L=0,R=5,产生一个变量mid = 2,随后变量maxLeft需要子过程来产生,系统做的工作是把getMax(arr,0,5)以及代码跑到第几行,mid = 2等信息全部压入栈中,然后调用子过程,先当与这个函数被归挡,先跑子过程,继续跑getMax(arr,0,2),一直继续直到触发L == R,即执行 return arr[L]返回。然后进行弹栈操作,读取弹栈后的栈顶的信息,完全还原现场当时的信息,即发生到哪个子过程,代码跑到第几行,再继续执行之后的子过程,最终串起来子过程和父过程的通信。
任何递归行为都可以改成非递归
将系统压栈改成自己压栈,就变成了非递归,即迭代
递归行为的复杂度
Master公式
T(N) = aT(N/b) + O(Nd)
T(N)表示样本量为N的时间复杂度
T(N/b)表示子样本的样本量
a表示子过程发生的次数
O(Nd)表示除过子样本发生之后剩下的操作的时间复杂度
以之前的例子为例,在一个数组中寻找最大值,数组一切为2,整个数组样本量为N,左边一半,样本量为N/2,右边一般,样本量为N/2,左边跑完跑右边,样本量为N/2的过程发生了2次,跑完子过程之后,比对最大的数值然后返回,是常数量的复杂度。
即:T(N) = 2T(N/2)+O(1)
Master公式的使用
T(N) = a*T(N/b) + O(Nd)
- log(b,a) > d -> 复杂度为O(Nlog(b,a))
- log(b,a) = d -> 复杂度为O(Nd * logN)
- log(b,a) < d -> 复杂度为O(Nd)
划分子问题的规模一样才可以用master公式
上面的例子,a = 2, b = 2,d = 0 则O(N) = O(N)
一些补充
我们在分析一个问题写算法的时候,只关心一步,即父问题转换为子问题那一步,我们只需要知道它们大层面的关系,子问题继续转换子问题以及如何转换,转换多少次,不需要去关心。
划分是一个大范围对等的划分,例如:如果是奇数个数数组,划分为两部分,数据不对等也属于正确。