算法之递归

什么是递归?

百度百科:程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件递归前进段递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

一个比喻:知乎上的某位同行对递归觉得最恰当的比喻,就是查词典。我们使用的词典,本身就是递归,为了解释一个词,需要使用更多的词。当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词,可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思(来源——知乎李鹏

 
几个小案例 原地址
阶乘的运算
描述: n! = n*(n-1)*...2*1
function test(n){
    if(n <= 1){
        return 1;
    }else{
        return n*test(n-1);
    }
}
console.log(test(5));  //120

//下面使用arguments.callee来代替被递归的函数,
//目的是为了防止函数名的紧密耦合。同时简化了代码
function test(n){
    if(n <= 1) return 1;
    return n*arguments.callee(n-1);
}
console.log(test(5)); //120
斐波那契数列
描述:斐波纳契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……求第n个数是多少
function test(n){
    if(n <= 0) return 0;
    if(n <= 1) return 1;
    return arguments.callee(n-1) + arguments.callee(n-2);
}
console.log(test(6)); //8

走楼梯问题

描述:楼梯有n阶台阶,上楼可以一步上1阶,也可以一步上2阶或者3阶,计算共有多少种不同的走法。

function test(n){
    if(n <= 0) return 0;
    if(n == 1) return 1;
    if(n == 2) return 2;
    if(n == 3) return 4;
    return arguments.callee(n-1) + arguments.callee(n-2)+arguments.callee(n-3);
}
console.log(test(6)); //24


思路:
这其实就是一个斐波那契数列的一种实现。我们分析的时候,可以转化成小规模的子类问题。
当到达指定阶梯的最后一步的时候,可以有三种种情况,一是上一步,二是上两步,三是上三步。
所以总的方法是F(n)
= F(n-1) + F(n-2) + F(n-3)。然后自然就成了各自的小计算,不断循环下去,直到判断条件的发生。

最大公约数

描述:给两个数,如果两个数相等,最大公约数是其本身。如果不等,取两个数相减的绝对值和两个数中最小的数比较,相等则为最大公约数,不等则继续上面的算法,直到相等。

function test(a,b){
    if(a == b) return a;
    return arguments.callee(Math.abs(a-b),Math.min(a,b));
}
console.log(test(15,12)); //3

汉诺塔

描述:百度百科

function test(n,src,aux,dest){
    if(n > 0){
        test(n-1,src,dest,aux);
        console.log("移动第"+n+"个圆盘从"+src+"到"+dest);
        test(n-1,aux,src,dest);
        }
}
console.log(test(3,"A","B","C")); 

思路:
在我没有体会到递归的精粹前,我对这个问题简直百思不得其解。我一直问自己,我怎么知道下一个该去哪里?
后来,我就知道,我其实更关心的是,最后那一个该怎么走。这个怎么说呢?我们可以从头想起,我们如果只有1个盘,
我们可以让它到C柱,也可以到B柱。自然两个盘,也可以实现。3个盘,也是可以的吧。那我们就讲4个盘的情况。
4个盘要完成就要将A上的圆盘,完全转移到C上。我们把前3个盘当作一个整体放到B上,然后第4个盘就可以到C上了,
然后我们再将前三个放到C上,就成功了。那前三个盘,又可以重新当作一个新游戏,让前两个盘,当一个整体,依次类推。
这样我们只需要关心大的整体的事,其它的就转成小规模问题解决就好了。

二分法快排

描述:使用二分法,对一个数组进行由小到大的排序。参考博客

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <script>
      function quickSort(arr) {
        if(arr.length <= 1) return arr;
        var leftArr = [];
        var rightArr = [];
        var pivot = Math.random(arr.length / 2);
        var baseNum = arr.splice(pivot, 1);

        arr.forEach(function(num) {
          if(num < baseNum) {
            leftArr.push(num);
          } else {
            rightArr.push(num);
          }
        });
        return quickSort(leftArr).concat(baseNum, quickSort(rightArr));
      }
      console.log(quickSort([1, 4, 5, 6, 2, 7, 13, 8, 9]));
    </script>
  </body>
</html>
DOM树的递归
描述:获取一个节点的所有父节点的tagName
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <div id="box">
      <div id="content">
        <div id="last"></div>
      </div>
    </div>

    <script>
      var arr = [];
      var getParent = function(node) {
        node = node.parentNode;
        if(node.tagName) {
          arr.push(node.tagName);
          getParent(node);
        }
      }
      getParent(document.getElementById("last"));
      console.log(arr); //["DIV", "DIV", "BODY", "HTML"]
    </script>
  </body>
</html>
看了以上这么多算法题,不知道各位老铁掌握了多少。如果还是晕晕的,没关系,接下来还有大量的习题加强理解。
 
1、一个递归算法必须包括()?
终止条件和递归部分
View Code
 
2、任何一个递归过程是否都可以转换成非递归过程?
正确
详细讨论地址:https://www.zhihu.com/question/20418254
View Code

3、给定下列程序,那么执行的结果为多少?

function foo(x,y){
   if(x <= 0||y <= 0){
          return 1;
   }else{
          return 3*foo(x-6,y/2);
   }          
}
console.log(foo(20,13));
解析:
foo(20, 13) = 3 * foo(14, 6) = 3 * 3 * foo(8, 3) = 3 * 3 * 3 * foo(2, 1) = 3 * 3 * 3 * 3 * foo(-4, 0) =3 * 3 * 3 * 3 * 1 = 81
View Code

4、给定下列程序,那么执行的结果为多少?

function f(x){
     if(x <= 2){
          return 1;
      }else{
          return f(x-2)+f(x-4)+1;
     }
}
console.log(f(10));
针对这样的题目在别的地方看到的比较好的方法是用树来表示
                                     10
                             8                  6
                       6         4        4       2
                   4     2    2  0    2   0
                2   0
图中树的节点数是15,所以是调用了15次
View Code

5、只有那种使用了局部变量的递归过程在转换成非递归过程时才必须使用栈?

错误
解析:递归工作栈里面包括返回地址、本层的局部变量和递归调用的形参代换用实参,所以正常情况下,无论递归过程有没有使用局部变量,转换为非递归过程都需要用栈来模拟这个递归调用过程
View Code

6、有一段楼梯台阶有15级台阶,以小明的脚力一步最多只能跨3级,请问小明登上这段楼梯有多少种不同的走法?

解析:请查看案例“走楼梯问题”

7、4个圆盘的Hanoi塔,总的移动次数为多少?

设f(n)为n个圆盘的hanoi塔总的移动次数,其递推方程为f(n)=f(n-1)+1+ f(n-1)=2*f(n-1)+1。理解就是先把上面n-1个圆盘移到第二个柱子上(共f(n-1)步),再把最后一个圆盘移到第三个柱子(共1步),再把第二柱子上的圆盘移动到第三个柱子上(共f(n-1)步)。
而f(1)=1;于是f(2)=3,f(3)=7,f(4)=15。故答案为C。
进一步,根据递推方程其实可以得出f(n) = 2^n - 1。
View Code

8、下列函数的执行结果为多少?

function f(i){
     if(i>1){
          return i*f(i-1);
    }else{
          return 1;
    }
}
console.log(f(5));
这里展开如下:
f(5) = 5*f(4)
      = 5*4*f(3)
      = 5*4*3*f(2)
      = 5*4*3*2*f(1)
      = 5*4*3*2*1
      = 120
View Code

9、递归次数与各元素的初始排列有关。如果每一次划分后分区比较平衡,则递归次数少;如果划分后分区不平衡,则递归次数多。递归次数与处理顺序无关。

10、n!后面有多少个0,6!=1*2*3*4*5*6=720.720后面有1个0,n=10000,求n!

10000/5=2000 有2000个能被5整除
2000/5=400     这2000个里面能被5整除有400个(2000个已被5除过1次。能除第二次的有400)
400/5=80         同理 80个
80/5=16           同理 16个
16/5=3余1        同理 3个
结果2000+400+80+16+3=2499
View Code

11、对递归程序的优化的一般的手段是什么?

尾递归优化
View Code

12、一个递归算法如下,那么f(f(9))需要计算多少次f函数(注:这种题目,画树图最直观快速)

function f(i){
    if(i<=3){
          return 1;
    }else{
          return f(i-2)+f(i-6)+1;
   }
}
一、先算内层f(9)
    [1] 计算 f(9) = f(7) + f(3) + 1;
    [2] 计算[1]中 f(7) = f(5) + f(1) + 1;
    [3] 计算[2]中 f(5) = f(3) + f(-1) + 1;
    [4] 计算[3]中 f(3) = 1;
    [5] 计算[3]中 f(-1) = 1;
        {至此f(5)可计算得: f(5) = 1 + 1 + 1 = 3}
    [6] 计算(1)中f(1) = 1;
        {至此f(7)可计算得 :f(7) = 3 + 1 + 1 = 5}
    [7] 计算[1]中f(3) = 1;
        {至此f(9)可计算得:f(9) = 5 + 1 + 1 = 7}
    计算f(9)一共调用了7次函数

二、计算外层f(7)
    由上面步骤可知,计算f(7)调用了5次函数
    所以一共调用了函数7+5=12次
View Code
13、递归函数最终会结束,那么这个函数一定有一个分支不调用自身
 
14、对n个记录的线性表进行快速排序为减少算法的递归深度,每次分区后,先处理较短的部分
 
15、当n=5时,下列函数的返回值是多少
function f(i){
    if(i<2){
          return i;
   }else{
          return f(i-1)+f(i-2);
   }
}
console.log(f(5));
解法一:
f(0)=0;
f(1)=1;
f(2)=f(1)+f(0)=1;
f(3)=f(2)+f(1)=2;
f(4)=f(3)+f(2)=3;
f(5)=f(4)+f(3)=5;
View Code

解法二:

16、设有递归算法如下,那么f(f(8))时需要计算多少次f函数?

function x(i){
   if(i<=3){
          return i;
   }else{
          return x(i-n)+x(i-4)+1;
   }
}
解法一:
x(8)=x(6)+x(4)+1  递归计算x(8)第一次调用
x(6)=x(4)+x(2)+1  递归计算x(6)第二次调用
x(4)= x(2)+x(0)+1  递归计算x(4)第三次调用
x(4)= x(2)+x(0)+1   递归计算x(4)第四次调用 
之后再调用x()计算黑体部分的结果(5次,加上前面4次,一共9次),最后x(8)返回值为9

接着计算x(9)
x(9)=x(7)+x(5)+1  递归计算x(9)第一次调用
x(7)=x(5)+x(3)+1  递归计算x(7)第二次调用
x(5)=x(3)+x(1)+1  递归计算x(5)第三次调用
x(5)=x(3)+x(1)+1  递归计算x(5)第四次调用
之后再调用x()计算黑体部分的结果(5次,加上前面4次,一共9次),最后x(8)返回值为9

解法二:
根据题意,易得x(3) = x(2) = x(1) = x(0) = 1
x(8) = x(6) +x(4) +1
       = x(4) + x(2) +1 + x(2) + x(0) +1 + 1 
       = x(2) + x(0) +1 + 1 + 1 +1 + 1 +1 + 1 
       = 9 
x(8)  这个就调用了9次函数x(int n)
同理可得x(9)也是调用了9次函数x(int n)
所以总共18次。
View Code
 17、在递归算法执行过程中,计算机系统必定会用到的数据结构是
 
 18、递归函数中的形参是自动变量
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/fengxiongZz/p/8053712.html