js递归

js递归调用

方法一:

  1. // 一个简单的阶乘函数
  2. var f = function (x) {
  3. if (x === 1) {
  4. return 1;
  5. } else {
  6. return x * f(x - 1);
  7. }
  8. };

Javascript中函数的巨大灵活性,导致在递归时使用函数名遇到困难,对于上面的变量式声明,f是一个变量,所以它的值很容易被替换:

  1. var fn = f;
  2. f = function () {};

函数是个值,它被赋给fn,我们期待使用fn(5)可以计算出一个数值,但是由于函数内部依然引用的是变量f,于是它不能正常工作了。

所以,一旦我们定义了一个递归函数,便须注意不要轻易改变变量的名字。

上面谈论的都是函数式调用,函数还有其它调用方式,比如当作对象方法调用。

我们常常这样声明对象:

  1. var obj1 = {
  2. num 5
  3. fac function (x) {
  4. // function body
  5. }
  6. };

声明一个匿名函数并把它赋值给对象的属性(fac)。

如果我们想要在这里写一个递归,就要引用属性本身:

  1. var obj1 = {
  2. num : 5,
  3. fac : function (x) {
  4. if (x === 1) {
  5. return 1;
  6. } else {
  7. return x * obj1.fac(x - 1);
  8. }
  9. }
  10. };

当然,它也会遭遇和函数调用方式一样的问题:

  1. var obj2 = {fac: obj1.fac};
  2. obj1 = {};
  3. obj2.fac(5); // Sadness

方法被赋值给obj2的fac属性后,内部依然要引用obj1.fac,于是…失败了。

换一种方式会有所改进:

  1. var obj1 = {
  2. num : 5,
  3. fac : function (x) {
  4. if (x === 1) {
  5. return 1;
  6. } else {
  7. return x * this.fac(x - 1);
  8. }
  9. }
  10. };
  11. var obj2 = {fac: obj1.fac};
  12. obj1 = {};
  13. obj2.fac(5); // ok

通过this关键字获取函数执行时的context中的属性,这样执行obj2.fac时,函数内部便会引用obj2的fac属性。

可是函数还可以被任意修改context来调用,那就是万能的call和apply:

  1. obj3 = {};
  2. obj1.fac.call(obj3, 5); // dead again

于是递归函数又不能正常工作了。

我们应该试着解决这种问题,还记得前面提到的一种函数声明的方式吗?

var a = function b(){};

这种声明方式叫做内联函数(inline function),虽然在函数外没有声明变量b,但是在函数内部,是可以使用b()来调用自己的,于是

  1. var fn = function f(x) {
  2. // try if you write "var f = 0;" here
  3. if (x === 1) {
  4. return 1;
  5. } else {
  6. return x * f(x - 1);
  7. }
  8. };
  9. var fn2 = fn;
  10. fn = null;
  11. fn2(5); // OK
  12. // here show the difference between "var f = function f() {}" and "function f() {}"
  13. var f = function f(x) {
  14. if (x === 1) {
  15. return 1;
  16. } else {
  17. return x * f(x - 1);
  18. }
  19. };
  20. var fn2 = f;
  21. f = null;
  22. fn2(5); // O 
  23. var obj1 = 
  24. num : 5,
  25. fac : function f(x) {
  26. if (x === 1) {
  27. return 1;
  28. } else {
  29. return x * f(x - 1);
  30. }
  31. }
  32. };
  33. var obj2 = {fac: obj1.fac};
  34. obj1 = {};
  35. obj2.fac(5); // ok
  36. var obj3 = {};
  37. obj1.fac.call(obj3, 5); // ok

就这样,我们有了一个可以在内部使用的名字,而不用担心递归函数被赋值给谁以及以何种方式被调用。

Javascript函数内部的arguments对象,有一个callee属性,指向的是函数本身。因此也可以使用arguments.callee在内部调用函数:

  1. function f(x) {
  2. if (x === 1) {
  3. return 1;
  4. } else {
  5. return x * arguments.callee(x - 1);
  6. }
  7. }

但arguments.callee是一个已经准备被弃用的属性,很可能会在未来的ECMAscript版本中消失,在ECMAscript 5中"use strict"时,不能使用arguments.callee。

最后一个建议是:如果要声明一个递归函数,请慎用new Function这种方式,Function构造函数创建的函数在每次被调用时,都会重新编译出一个函数,递归调用会引发性能问题——你会发现你的内存很快就被耗光了。

js递归函数应用

最近在做项目的时候,用到了递归函数,用来调用json的子节点,把所有json中的子节点中包含某个数的object,都push到一个数组中,然后对其进行绑定。

我是通过如下递归调用的

  1. var new_array=[];
  2. function _getChilds(data){
  3. if(data.ObjType=="某个数"){
  4. new_array.push(cs_data);
  5. }
  6. if(data.Childs){
  7. if(data.Childs.length>0){
  8. getChilds(data.Childs)
  9. }
  10. }
  11. }
  12. function getChilds(data){
  13. for(var i=0;i<data.length;i++){
  14. _getChilds(data[i]);
  15. }
  16. }
  17. 使用方法:getChilds"json数据"

就把json中所有包含某个数的数据push到new_array数组当中了。

 

方法二:

 

递归函数是在一个函数通过名字调用自身的情况下构成的,如下所示。

  1. function factorial(num) 
  2. if (num <= 1){
  3. return 1;
  4. } else {
  5. return num * factorial(num-1);
  6. }
  7. }

这是一个经典的递归阶乘函数。虽然这个函数表面看来没什么问题,但下面的代码却可能导致它出错。

  1. var anotherFactorial = factorial;
  2. factorial = null;
  3. alert(anotherFactorial(4)); //出错!

以上代码先把 factorial()函数保存在变量 anotherFactorial 中,然后将 factorial 变量设置为 null,结果指向原始函数的引用只剩下一个。但在接下来调用 anotherFactorial()时,由于必须执行 factorial(),而 factorial 已经不再是函数,所以就会导致错误。在这种情况下,使用 arguments.callee 可以解决这个问题。
我们知道, arguments.callee 是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用,例如:

  1. function factorial(num){
  2. if (num <= 1){
  3.  
    return 1;
  4.  
    } else {
  5. return num * arguments.callee(num-1);
  6. }
  7. }

使用 arguments.callee 代替函数名,可以确保无论怎样调用函数都不会出问题。因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误。不过,可以使用命名函数表达式来达成相同的结果。例如:

  1. var factorial = (function f(num){
  2. if (num <= 1){
  3. return 1;
  4. } else {
  5. return num * f(num-1);
  6. }
  7. });

var factorial = (function f(num){
  if (num <= 1){
    return 1;
  } else {
    return num * f(num-1);
  }
})(3);
console.log(factorial);

以上代码创建了一个名为 f()的命名函数表达式,然后将它赋值给变量 factorial。即便把函数赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。

方法三:

Question1—Fibonacci数列第N项

  1. var fib = function (n){
  2. if(n<= 2){
  3. return 1;
  4. return fib(n -1) + fib(n -2);
  5. }
  6. console.log(fib( 5));
上面是递归实现。
  1. var fib = function (n){
  2. var a1= 1,a2= 1,a3= 0;
  3. if(n<= 2){
  4. return 1;
  5. }
  6. for( var i = 0;i < n -1;i++){
  7. a3 = a1 + a2;
  8. a1 = a2;
  9. a2 = a3;
  10. }
  11. return a3;
  12. }
  13. console.log(fib( 5));
上面是循环实现。

Question2—一共10级楼梯,每次可以走一步或两步,求一共多少种走法。

思路:

要想走到N(N=10)级,可以分为2种情况。

  1. 从n-2级迈两步
  2. 从n-1级迈一步

那么对于n-2和n-1的情况也是各自分为两种,以此类推。

那么走法的和就是n-2的走法和n-1的走法之和。

那么递归到最基本的(当前人在第0阶台阶)

第0阶台阶:0

第1阶台阶:1

第2阶台阶:2(1+1或者2)

得到公式,也就是斐波那契数列。

 
  1. var fib = function (n){ 
  2. if(n == 1){
  3. return 1;
  4. else if(n== 2){
  5. return 2;
  6. else if(n> 2){
  7. return fib(n -1) + fib(n -2); 
  8. }
  9. }
  10. console.log(fib( 10));

Question3—1个细胞,一个小时分裂一次,生命周期是3小时,求n小时后容器内,有多少细胞。

思路:

细胞的生存周期是3个小时,那我们就可以把细胞在题目中状态分为以下几个状态:

  • a:刚分裂态——由前一小时的a,b,c分裂出
  • b:分裂1小时态——由前一小时a长成
  • c:分裂2小时态——由前一小时b长成
  • d:分裂3小时态——死亡的细胞。由前一小时c长成,和之前的d一起组成。

那么,我们就可以根据细胞状态设定函数。分析每一个状态的来源是哪里即可。

 
容器中存活的细胞数目就是a、b、c三种状态数量的总和。
 
  1. var afib = function (n){
  2. if(n=== 0){ return 1;} //初始的那个细胞
  3. return afib(n -1)+bfib(n -1)+cfib(n -1);
  4. }
  5. var bfib = function(n){
  6. if(n=== 0){ return 0;} //一个小时之后才会生成
  7. return afib(n -1);
  8. }
  9. var cfib = function(n){
  10. if(n=== 0||n=== 1){ return 0;} //前两小时还没生成
  11. return bfib(n -1);
  12. }
  13. var time = 3;
  14. console.log(afib(time)+bfib(time)+cfib(time));

 

总结:

递归的两个必要因素:

     递归方程,递归结束条件。

算法核心:

  • 在有限次可预见性结果中,找到结果与上一次结果之间的关系。
  • f(n)与f(n-1)的关系有时候很简单,如同走楼梯,状态单一;又有时如同细胞分裂,多种状态组合影响结果。
  • 关键在于梳理清楚本次结果和上一次结果的关系有哪些方面或是因素。
  • 在草稿纸上写出前几次的结果,或者画图,这样更容易找到规律,这种规律实际上就是递归方程。
  • 在算法的分析中,当一个算法中包含递归调用时,其时间复杂度的分析会转化成为一个递归方程的求解

 

 

posted on 2021-01-15 17:00  ranyonsue  阅读(590)  评论(0编辑  收藏  举报

导航