你的位置:首页 > 软件开发 > Java > 透析递归应用

透析递归应用

发布时间:2017-09-28 15:00:07
题目源于《SICP》,这里做一下调整,如下:给了面值为50元、20元、10元、5元、1元的五种零钱若干,思考把面值100元人民币换成零钱一共有多少种方式?SICP给出的递归算法思想如下:将总数为a的现金换成n种不同面值的不同方式的数目等于:将现金a换成除了第一种面值之外的所有其他 ...

透析递归应用

题目源于《SICP》,这里做一下调整,如下:

给了面值为50元、20元、10元、5元、1元的五种零钱若干,思考把面值100元人民币换成零钱一共有多少种方式?

SICP给出的递归算法思想如下:

将总数为a的现金换成n种不同面值的不同方式的数目等于:

  • 将现金a换成除了第一种面值之外的所有其他面值的不同方式数目,加上
  • 将现金a-d换成所有种类的面值的不同方式数目,其中d是第一种面值的钱币

下面有解释到,递归的思想是要将问题归约到对更少现金数或更多种类面值钱币的同一个问题。有如下的约定:

  • 如果a==0,应该算作是有1种换零钱的方式
  • 如果a<0,应该算作是有0中换零钱的方式
  • 如果n=0,应该算作是有0种换零钱的方式

大家先不要纠结于为何要有这种约定,只需要记住这个约定就好了,先看看Lisp代码的实现:

(define (count-change amount)  (cc amount 5))(define (cc amount kinds-of-coins) (cond ((= amount 0) 1)    ((or (< amount 0) (= kinds-of-coins 0)) 0)    (else ( + (cc amount (- kinds-of-coins 1))      (cc (- amount (first-denomination kinds-of-coins) kinds-of-coins))      ) ))(define (first-denomination kinds-of-coins) (cond ((= kinds-of-coins 1) 1)    ((= kinds-of-coins 2) 5)    ((= kinds-of-coins 3) 10)    ((= kinds-of-coins 4) 20)    ((= kinds-of-coins 5) 50) ))

如果对Lisp有点儿晕,可以看看等价的Java实现:

 //换零钱 public static int countChange(int mount){  return cc(mount,5); } /**  * @param mount 整钱数量  * @param coinKinds 零钱类型数量  */ private static int cc(int mount, int coinKinds) {  if(mount == 0 ) return 1;  if(mount<=0 || coinKinds == 0) return 0;    return cc(mount,coinKinds - 1) + cc(mount - denomination(coinKinds),coinKinds); } private static int denomination(int coinKind){  switch(coinKind){  case 1:return 1;  case 2:return 5;  case 3: return 10;  case 4: return 20;  default: return 50;  } }

 

SICP大赞递归是如何的强大,能将问题简化,初看上面的递归觉得确实如此,但要真正彻底理解上面的代码好像还没那么容易,更别说要自己空手写出上面的代码。 

我再看到代码之后,就是不明白为什么会出现下面的代码:

 if(mount == 0 ) return 1; if(mount<=0 || coinKinds == 0) return 0;

 

因为程序是递归的,程序其他地方没出现过return 1,所以可以大概的知道,方法最终得到的换零钱方式数目肯定是这些个1相加得到。

那为什么是mount等于0的时候返回1呢? 需要找个例子,来真正看看程序递归树才知道其中的原因。

为了把问题简化,假设我手头有一张100元的,另外只有两种零钱,分别是50的和20的。这样一来结果好像很明显了,因为换零钱的方式就两种:两个50的或者5个20的。

其实可以更简化,比如就只有一种50的零钱,但那样暂时的递归树对帮助我们理解程序不是很明显。

看看下面的递归树:

透析递归应用

 

树节点中左边数字表示amount,右边表示零钱种类。

每一个完整的右斜线代表了全部换成某种面值的尝试;

这些右斜线的左分支代表了换了N个某种面值之后再尝试换其他面值的尝试;

看明白了这个递归树之后,就知道了下面判断条件的意义了:

 if(mount == 0 ) return 1;//整数面值的钱刚好被换完了 if(mount<=0 || coinKinds == 0) return 0; //该种尝试失败了(零钱加起来比整钱多了),没有可换的零钱种类了

 

 

似乎可以把这棵树称为测试树,每个叶子节点代表了测试结果,归结起来就知道成功了多少次。神奇的是递归巧妙地完成了遍历并进行测试。

知道了这种递归其实是在做遍历测试,那我们可以用一种简单而粗暴的测试:

 private static int countChange2(int mount){  int count = 0;  int d1 = denomination(1);  int d2 = denomination(2);  int d3 = denomination(3);  int d4 = denomination(4);  int d5 = denomination(5);  for(int i=0;i*d1<=mount;i++){   for(int j=0;j*d2<=mount;j++){    for(int k=0;k*d3<=mount;k++){     for(int l=0;l*d4<=mount;l++){      for(int m=0;m*d5<=mount;m++){       int test = i * d1          + j * d2          + k * d3          + l * d4          + m * d5;       if(test==mount){        count++;       }      }     }    }    }  }  return count; }

 

 

如果要画出上述算法的运行轨迹,恐怕跟递归树是一样的。并且性能上跟上述递归代码也是一样的。

思考另外一个问题,如果要打印出所有换零钱的方式呢?(而不是方式的总数)

对于上述for循环的遍历,很容易就能得到:

       if(test==mount){        String str = format(d1,i);        str += format(d2,j);        str += format(d3,k);        str += format(d4,l);        str += format(d5,m);        str = str.substring(0,str.length() - 1);        System.out.println(str);        count++;       }

 

format方法如下:
private static String format(int d,int count){  if(count==0){   return "";  }  return " ("+d + "x" + count + ") +"; }

 

计算countChange2(10)得到如下结果:

 (10x1) (5x2) (1x5) + (5x1) (1x10) 

 

而使用递归调用的程序要得到这个结果就稍微麻烦点儿了,因为每次测试成功的时候,“手头”并没有想for循环这样方便的数据。这些数据分布在了递归调用链上。要想拿到这些数据,需要新增一个参数,将调用过程“记录”在这个参数中。

 /**  * @param mount 整钱数量  * @param coinKinds 零钱类型数量  */ private static int cc(int mount, int coinKinds,String str) {  if(mount == 0 ) {   format2(str);   return 1;  }  if(mount<=0 || coinKinds == 0) return 0;    return cc(mount,coinKinds - 1,str) + cc(mount - denomination(coinKinds),coinKinds,str += "," + coinKinds); }

 

 private static void format2(String str) {  String[] ds = str.split(",");  int[] dCount = new int[6];  for(String dStr :ds){   if(dStr==null || dStr.equals("")) continue;   dCount[Integer.parseInt(dStr)]++;  }  String res = "";  for(int i = 1;i<dCount.length;i++){   if(dCount[i]==0) continue;   res += " (" + denomination(i) +"x"+dCount[i] + ") +" ;  }  if(res.length()>0) res = res.substring(0,res.length() - 1);  System.out.println(res); }

 

用一个字符串来记录经过的节点,仔细观察:

cc(mount,coinKinds - 1,str) + cc(mount - denomination(coinKinds),coinKinds,str += "," + coinKinds)

 

发现为什么左树上面的str没有进行"记录”?原因是,仔细看看递归树就会发现,仅当树往右边走一步的时候才是真正地开启了一次测试之旅。往左的分支表示减少一种面值的钱币,并没开始进行这种测试。

(完)

 原创作品,转载时请标注出处地址

原标题:透析递归应用

关键词:

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。

可能感兴趣文章

我的浏览记录