递归研究

  所谓递归呢,就是原问题与他的子问题极其的相似,只是规模不同。

程序中的递归,也就是函数自己调用自己,为什么要自己调用自己?因为全局问题需要子问题先解决,而子问题的解决过程却与全局问题一模一样~那么就继续调用这个过程,只是规模减小罢了,那么当所有的子过程完成然后返回之时问题也就解决了。

以我所学,我发现递归是发现定理和真理的绝佳技巧,进而猜想定理,由于递归与数学归纳法之间的密切联系,可以使用数学归纳法来证明递归问题。

两者的结合天衣无缝~ 数学归纳法只能证明定理而不能发现定理 ,真正的创新却在递归,好啦~闲话少撤,说一些数学中出现的递归定义

自然数的递归定义:1是一个自然数,每个自然数都有一个后继,这一个后继也是自然数。

可以看到这是明显的递归定义 。由与1的后继是自然数,所以这个后继有一个后继,而这个后继的后继还是自然数。。。

树结构递归定义~:(1)至少有一个叫做根的节点, (2)剩下的节点被分成n>=0互不相交的集合T1,T2...Tn,这些集合的每一个又都是树 T1,T2...Tn被称作根的子树。

你可以清晰的看到,描述一个无限对象的集合,我们只需要很少的语句,这就是递归的威力!

链表,字符串,等等 你都可以找到他们的递归定义,定义就介绍到这里。


上一文提高这一节要解决的五个问题,由于本人时间有限,让我们快速的将他们实现,描述不清的地方欢迎提问


1)汉诺塔问题

为了偷懒,我不写出问题的原型,直接附上wiki 汉诺塔传说~ http://zh.wikipedia.org/wiki/%E6%B1%89%E8%AF%BA%E5%A1%94

代数分析:设n个盘从A移动到C借助B ,需要T(n)步,其中T(n)是一个关于n的函数,so easy

           那么n-1个盘从A移动到C借助B,需要T(n-1)步呗。

那么请你想下,要将A的盘都移动到C必然需要先将上面的n-1个盘移动到B,然后将最大的那个盘直接放入C中,让后将那n-1盘从B借助A移动到C 

所以给出问题递归解:

(1)T(1) = 1;因为一个盘子直接1步就完事了

(2) T(n)<=2T(n-1)+1 ;/这里我解释下,首先需要将n-1个移动到B盘,自然是T(n-1)步,然后我们将最大的直接放到C盘1步,然后将那n-1个盘从B借助A放到C对称性知也是T(n-1)步,所以等号是成立的,然后我们只是找到了这种解法,但这种解法我们还没有证明他就是最少的步数,只是说这个步数可以解决问题!

证明2T(n-1)+1是最少的步数非常简单,只需要认为T(n-1)就是对n-1个盘的最少移动步数就ok~,因为这只是一种抽象,你这样认为,他的意思自然就是最少,由于T(1) = 1是一个盘的最少步,经过递归,我们自然得出他就是最少步,如果中间你在移动的过程中不小心出现一些费操作,那步数自然要比这里的多。


所以得出最少移动步数 T(n) = 2T(n-1)+1;

两边除以 2^n 就得出 T(n)/2^n = T(n-1)/2^n-1 + 1/2^n; //至于我这里为什么要除以2的n次方来求解,这是基本的数学思想,我们这里不涉及的太深,当然这样的递归式求解的方法非常多,你想怎么解就怎么解,解的意思是求解出一个我们直接能算出答案的公式,而不是一个递归式,递归式我们无法直接得出答案,需要套用好几次。

令U(n) = T(n)/2^n; 则得出 U(n) = U(n-1)+1/2^n; U(n) = U(n-2)+(1/2^n-1)+1/2^n 不停的推下去(注意技巧,U(n-1) 对应于1/2^n  也就是这里的幂要比函数的参数大1)

得出 U(n) = U(1)+ 1/2^2+1/2^3+.....+1/2^n-1 + 1/2^n;//这个和式的求解很快(套用几何级数求和公式),但是我们发现 U(1) = 2U(0)+1;我们的递归式按理也可推广到0个盘子,如果推广到0个盘子的话,求和更加的方便

我在这里写出

U(0)  = 0 ;

U(n) = U(n-1)+1/2^n;

U(n)  - U(n-1) = 1/2^n; 这次这样来求

U(n-1)-U(n-2) = 1/2^n-1;

....................

U(1) - U(0) = 1/2^1; 将这个n个式子相加 U(n) - U(0) =  1/2^1 +1/2^2 + .....+1/2^n-1 + 1/2^n;

也就是说 U(n) = 1/2^1 +1/2^2 + .....+1/2^n-1 + 1/2^n;//为了让大家清楚,我这里不套用什么数学公式,强行求解此和式

两边乘以2则 2U(n) = 1 + 1/2^1+.......1/2^n-2 + 1/2^n-1;

2U(n) - U(n) = 1-1/2^n  //中间的项都被消去了

U(n) = 1-1/2^n;

T(n) = U(n)*2^n= 2^n - 1; //我们得到了正确的答案 可是我们看到 这是一个幂级别的函数,增长级数可谓之大~,所以我们的程序不输入过大的数,不然你就等到天荒地老吧,2^64-1是移动64个圆盘的步数哦亲

代码写起来 soeasy ,示例性的写出,syso(x)代表 System.out.println(x);

我这里还是只做代码的解释,而不是非要给你一个可以运行的版本,毕竟可运行的版本可在网上找到,我们谈论的是思想~

//java 伪代码

public static void hannuota(int n, Str a,Str b, Str c) {//汉诺塔函数,需要移动多少个圆盘?给我三个柱子!

if(n=1)//也就是递归式的第一项。

syso(”把圆盘从“+a+"移动到" +c);//如果只有1个圆盘,我将他直接从你给的a移动到你给的c

else

hannuota(n-1,a,c,b);//有n个 我先移动n-1个,那么以前的c就要当做b来看待,把b当做c来看待,先将这n-1个移动到b柱上

syso(“将圆盘”+n+"从" +a+"移动到"+c);//然后移动最下面那个大圆盘~

hannuota(n-1,b,a,c);//然后就没有然后了。将那n-1个从b住再移动到c住

}

这个算法不好,非常的不好,我们只是做学习用,因为他的运算太过缓慢。

继续下一题,如果数组中(暂时先研究数组吧。。)当然是已经排好序的,我们待查得元素 有重复,比如:0,1,2,3,4,4,4,4,8,9我们目前的二分查找只能随机的找出4的索引,我们要实现能找出这个元素第一次出现的位置,比如这里 位置就是第五个元素,而不是随意的 5 6 7 8位置。

我们还是用二分查找来实现他!为什么呢还用二分查找呢?因为一个字!快!

可是这次我们就要有点变化了,要将他挤出来,而不是查出来,二分挤出来超级宇宙无敌查找法~

please  look~

首先我们要明白一点,既然是找出来第一个与待查元素相等的元素,而数组还是排好序的,利用什么来让查找的速度提升呢?

我们需要两个索引 来加快速度 一个索引指向的元素必然小于待查的元素 设这个索引为p,待查元素为t。设这个排好序的数组为a

则我们要保持 a[p]<t; 另外一个索引设为q 我们要保持 a[q]>=t;

这两个索引我们只要让他们俩相减等于1 就说明 他们俩之间没有任何元素 而p之前(包括p) 都小于t,而q之后包括q都大于等于t

最后我们比较a[q]是否等于t。如果等于 他就是t的第一次出现,我想我说的很清楚了。。

上代码!

public static in  first_appear(int[] a,int t)//查找t的第一次出现。a是排好序的数组,只有这样我们才能利用二分法快速查到答案,不然我问你 这个数组1亿的长度 你想怎么玩?

{

//初始化两个索引,一个索引对应的值无限小-->p,一个索引对应的值无限大--->q,这样 我们就能保证 我们的初始化是遵循不变式a[p]<t, a[q]>=t

//那么该如何来初始化他们呢?让p = -1,让q = a.length 这两个越界的索引,我们认为他的初始化是正确的,只要我们后边不用到

//他们,程序就不会有问题,我们只是把他们看做哨兵。

while(p+1 !=  q)//必须让他俩之间只有1的距离,这样一个小于t,一个大于等于t,那么中间就没有漏掉的区间(循环退出证明见下面分析)

{

【循环不变量---->a[p]<t, a[q]>=t,数组是顺序的】

int mid =p+(q-p)/2//中间的元素

if(t<=a[mid)//如果这个t小于等于中间的元素 就让q = mid;

q = mid;

else 

p = mid;//如果t大于中间的元素了 就让p等于中间的元素

}

int ret = -1;

if(q<a.length && a[q] == t)//如果q不是他的最早初始化,因为最早初始化 是我们假定的,不是的话,就说明他是可用的,a[q]==t说明大于等于t的最小的一个元素等于t,说明这个数组中有这个元素,且这个元素的第一次出现就是q
ret = q;

return ret;

}

分析循环的退出条件。

p+1有一天会等于q吗?他们会相爱在一起吗?

mid取的一直是他们之间的数字,不可能越轨的,而p和q被改变也只是改变为mid,所以这个区间是不停的缩小的p总有一天必须等于q(下取整也是原因之一)。这个是毫无争议的。

那么p等于q的前一步是什么呢?就必然是p+1 = q  也就是p和q紧挨,q>=p+2下一步必然不会出现p == q (分析下mid会是多少,而他赋值给p和q也不会保证下次p==q)

完毕。

这个问题 我们只是说假定了一些元素,而我们没有用的这些元素的话,问题本身就是正确的。假定只是为了我能说清楚。。


接下来给出二分搜索的递归实现,这个非常简单,直接给出:

public static int binary_search(int  key,int lo, int hi)

{

if(lo>hi)

return -1;

int mid = (lo+hi)/2;

if(key<a[mid])

return binary_search(key,a,lo,mid-1);

else if(key>a[mid])

return binary_search(key,a,mid+1,hi);

else

return mid;

我想这个程序为了节约你我时间,就不解释了,看不懂就多看几遍。画图是理解递归的最好帮手~

首先我想说下,写博客真累!!介于这个文章快写完了,我就再忍耐一下,但是质量上我保证不会因为偷懒而缺失什么的。尽我所能将我所想毫无保留的解释出来!

约瑟夫问题。

约瑟夫的故事我就不说了,直接说出数学建模后的约瑟夫数学模型

其实就是一圈人围着一圈,编号从1到n,隔一个人就死一个人 比如 1 2 3 4 5 五个人 做成一圈 第一个挂掉的是2,

然后4 然后隔5 ,1挂掉 由于2已经挂掉了 我们隔3 挂掉5 存活着为3

问题:如果n个人做成一圈 请用程序实现隔一个人死掉一个人得情况下,最后存活着的编号是?

或许你会说,用一个链表将他们穿成一个圈,死一个 就让它旁边的两个人引用互相持有就ok,

然后依次的删除节点,最后剩下一个为止。你想的不错,但是我这里要给出代数的威力,

毕竟计算机科学的基础是数学,我们就看下数学的威力能让这个程序变为什么样子

直接猜想和归纳的过程我就不说了,直接给出浅显易懂的解决方案

如果为2个人 那么存活着 就是1这个人 对吧?

如果为4个人呢?

设J(n)为约瑟夫问题的解决函数,

那么J(2n) = 2J(n)-1这个对吧?第一圈 会把为偶数的那n个人全部杀死 剩下了1 3 5 7 。。。。

那么J(2n+1) = 2(n)+1 这个对吧? 第一圈 会把为偶数的那n个人全部杀死 然后杀死1位置的那个人 剩下n个人,也就是剩下 3 5 7 9.。。。。。。

这两个递归式,你可以求出他的封闭形式,但不简单,这个封闭形式就是问题的解,也就是你的程序代码

但是问题貌似还是不简单,有没有更简单的?

请思考

T(1) = 1//就1个人 他肯定存活 因为只能存活一个人,这就是问题的解

T(2) = 1//第2个人上去就被干掉

T(4) = 2T(2)-1 = 1;

T(8) = 2(4)-1;

我们看到 T(2^m) = 1

好的真相马上就要显现,我们用代数思想把n表示为如下形式

n=2^m+L(其中2^m表示为n以内最大的2的幂,0<=L<2^m); 我说的对吧?

那就是什么呢?这个答案就是 T(n) = 2L+1;

比如 有5个人 n = 2^2+1 T(5) = 2*1+1 = 3; 有100个人 n = 2*6+36 T(100) = 36*2+1 = 73

为什么呢?教你个简单的诀窍,刚开始有n个人 然后一个一个的杀掉,

当杀掉L个人的时候 正好剩下2^m个人 T(2^m) = 1 这个人在全体的号码是多少呢?

杀掉L个人 他前面就有2L个人, 那么2L+1就是他的号码(当然你要证明L必小于这一圈的一半,so easy 略过)

我们继续分析!!!坚持住!

T(n) = 2L+1 , n = 2^m +L 都牵扯到2 ,如果用2进制来表示n呢?

n = (bm bm-1...b1 b0)2进制 bm = 2^m对吧? 二进制定义嘛b0如果等于1 就是2^0 等等。。。

那么bm必然就等于1,L呢? L 必然就等于(bm-1....b1 b0)

2L=(bm-1 bm-2 ..... b1 b0 0) 2L+1 = (bm-1 bm-2 ... b1 b0 1)~ok 1= bm 

所以2L+1 = (bm-1 bm-2 ...b1 b0 bm )

你会发现他的解其实是他的二进制表示形式向左循环移动1位

5 = (1 0 1) 解就是(011) = 3

100 = (110100)解就是 (101001)=73

给出算法

public static int Jesf(int N) //N人数,返回存活着

{

int L = 消去这个数的二进制最高位(N);

int Lx2 = L<<1; //2L就等于他往左移动1位

return Lx2 + 1;//2L+1;

}

上面求L的函数我以后一定写出来,但是现在我只能写出很低效的代码,不写也罢。

有知道高效的请告诉我~3q,但是问题已经解决了我想。

现在如果你要测试 比如N= 100; L = N-64;

最后一个题目,欧几里得算法。。太简单了 但是有一点我要说清楚,就是欧几里得算法的证明

所谓欧几里得算法 就是求两个数的最大公约数 比如 20和10 最大公约数就是10 

(greatest common divisor,我英文很烂,查词典的哈哈,以下简称gcd)

21与28 gcd就是7 1与40 gcd就是1

我们用代数方法来说明欧几里得算法吧,m和n两个数的gcd是?

m = q*n +r 如果r = 0 则 m是n的倍数,显然在这样的一种情况下,n是m和n的最大公因子。

如果r!=0,注意整除m和n的任何数必定也整除 m-qn = r,因此整除n和r的任何数必定整除q*n+r=m

所以(m,n)的公因子集合和(n,r)的公因子集合是一样的,(m,n)的gcd与(n,r)的gcd也就一样了


public static  int gcd(int m, int n) {

if(m<0) m = -m;

if(n<0) n = -n;//正负不影响最大公因子,却影响算法,于是将他们转化为非负数

if(0==n) return m;

else return gcd(n,m%n);

}

递归与二分都说完了,下一文我打算写归并排序与快速排序,因为这两个排序刚好用到二分与递归。但是这两个都用到了分治思想,

我想先写个分治思想的分析,然后再探讨快排与归并,具体写什么等周末看吧。


我看到很多人的博客上写 转载请注明出处之类的,我想说我的博客大家可以随意转载,

当成自己的博客都没关系,目的都是为交流学习之用。




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值