2019独角兽企业重金招聘Python工程师标准>>>

在上一篇中,经过深入分析,已经得出一个能够递归的形式化的结果,现在则准备给出一个具体实现。

结果回顾

前述结果如下:

caseOfChange(amount, cashList) { // base caseif (amount.isNegative()) { // 负数 return 0; } if (amount.isZero()) { // 0元 return 1; }if (cashList.containsOnlyOneCash()) { // 只有一种零钱时 if (amount.canBeChangedWith(cashList.getTheOnlyOneCash())) { // 能换时,即能整除 return 1;} else { return 0; } }// recursive casereturn caseOfChange(amount.subtractBy(cashList.getFirstCash()), cashList)+ caseOfChange(amount, cashList.newCashListWithoutFirstCash());
}

其实已经大体OK了,基本就差确定一下类型就差不多了。

代码实现

下面将给出一个Java的实现。Java是一种面向对象的语言,有强大的类型抽象机制,我们可以将上述形式化结果翻译成面向对象的实现。

我不清楚大家平时喜欢面向什么编程,我反正是面向显示器编程,偶尔面向键盘一下。

面向对象的实现

上述结果基本上只是差了类型(type or class)没有确定。

抽象数据类型(ADT:Abstract Data Type)

对一个类型(类)来说,是对行为(behavior)属性(property)的一种抽象与封装。

行为刻画了一个类的功能,是对外的主体部分;(在Java中,即对应成员方法(member method)

属性则关系到内部的具体实现,通常应该对外隐藏。(在Java中,即对应成员变量(member field)

引入自定义类型

直接将amount视作Amount类型,cashList则为CashList类型。再仔细推敲一下,cashList中的cash其实也就是amount,或者说amount其实也就是cash。

这会更好地与现实接轨。

因此,可引入两种自定义类型Cash和CashList:

public class Cash {private int value;public Cash(int value) {this.value = value;}
}public class CashList {private List<Cash> list = new ArrayList<Cash>();public CashList(Cash ... cashes) {// TODO ...}
}

其实也就是分别对Integer以及Array(或者说ArrayList)的轻量封装,这样我们就能加入自定义的行为。

确定类型的行为(方法)

现在考虑行为,基本上,前述描述中已经基本刻画了两种类型的行为。

比如,isNegative,isZero是Cash类中的方法,containsOnlyOneCash等则是CashList中的方法。

将前述伪代码拷贝到类定义中,稍加修改就可以让IDE为我们自动生成那些方法定义了。

严格地说,对Cash类而言,其实只要对外提供一个方法即可,这是外界唯一关心的方法:

public int getCountOfChanges(CashList list)

至于isZero之类的,只是一些内部辅助实现,定为private即可。

public class Cash {// ...// 对外接口public int getCountOfChanges(CashList list) { /*...*/ }// 内部递归实现private int caseOfChanges(Cash cash, CashList list) { /*...*/ }private boolean isNegative() { /*...*/ }private boolean isZero() { /*...*/ }private boolean canBeChangedWith(Cash cash) { /*...*/ }private Cash subtractBy(Cash cash) { /*...*/ }
}public class CashList {// ...public boolean containsOnlyOneCash() { /*...*/ }public Cash getTheOnlyOneCash() { /*...*/ }public CashList newCashListWithoutFirstCash() { /*...*/ }public Cash getFirstCash() { /*...*/ }
}
引入枚举enum类型

再考虑到cash的种类是有限的,直接就用enum枚举代替了,只提供有限几个实例(instance)

public enum Cash {CASH_100(100), CASH_50(50), CASH_20(20), CASH_10(10), CASH_5(5), CASH_1(1);private int value;private Cash(int value) {this.value = value;}// ...
}

这样客户端代码就无法new出形形色色的Cash,只能用这里定义好的,我们也免去了检查输入合法性的麻烦。

泛化的零钱GenericCash

Cash变为enum后,由于递归演算涉及到了一些中间结果:

比如求coc(100, [20, 5, 1])时经约减会出现coc(80, [20, 5, 1])之类的,显然80不是enum中的一员。

为此,引入一个新的类型GenericCash,

Java中的范型称为Generic type,这里用generic表示一般化的零钱,也即递归运算过程中才会出现的那些。

也正好把isZero等方法也打包到它的内部去:

class GenericCash {// ...public int getCountOfChanges() {}private int caseOfChanges(GenericCash genericCash, CashList list) {}private boolean canBeChangedWith(Cash cash) {}private boolean isNegative() {}private GenericCash subtractBy(Cash cash) {}private boolean isZero() {}
}

这样,Cash类就显得很简洁了。

局部内部类(local inner class)

考虑到只是在方法内部才会用到GenericCash,外部根本不需要知道这个类的存在,直接把它扔在方法内部定义,

public enum Cash {// ...// 对外接口public int getCountOfChanges(final CashList list) {// 方法内部类class GenericCash {// ...}return new GenericCash().getCountOfChanges();}
}

这样,即便是Cash类中的其它地方也不知道这个类的存在。(也无需知道)

最终结果

一个完整的结果如下:

Cash类:

public enum Cash {CASH_100(100), CASH_50(50), CASH_20(20), CASH_10(10), CASH_5(5), CASH_1(1);private int value;private Cash(int value) {this.value = value;}// ================ 第二种方式,更加对象化,把第一种方式中的int类型的genericCash包装成类public int getCountOfChanges(final CashList list) {/*** 一般化的现金值,在运算中,我们会逐步分割要换的零钱,这会导致出现一些中间值* 举个例子,把100换成[20, 10, 5],在运算中将出现把100分成80+20的情况,然后将计算把“80”换成[20,10,5]的情况* 这里的80就是一般化的现金值,在实际中不会存在,但在我们的运算中会有意义。* * 这是一个方法内部类,仅仅只用于也只能用于这个方法中。**/class GenericCash {private int value;private GenericCash() {// 由于是内部类,可以直接引用所在类的私有成员变量value = Cash.this.value;}// 供递归调用时使用的构造函数private GenericCash(int value) {this.value = value;}private boolean canBeChangedWith(Cash cash) {// 由于是内部类,可以直接引用cash的私有成员变量return value % cash.value == 0;}private boolean isNegative() {return value < 0;}private GenericCash subtractBy(Cash cash) {return new GenericCash(value - cash.value);}private boolean isZero() {return value == 0;}private int getCountOfChanges() {// 由于是内部类,这里直接引用外围方法的list变量,这也是它为何要设置成final的原因return caseOfChanges(this, list);}private int caseOfChanges(GenericCash genericCash, CashList list) {if (genericCash.isNegative()) {return 0;}if (genericCash.isZero()) {return 1;}if (list.containsOnlyOneCash()) {return genericCash.canBeChangedWith(list.getTheOnlyOneCash()) ? 1 : 0;}return caseOfChanges(genericCash.subtractBy(list.getFirstCash()), list)+ caseOfChanges(genericCash, list.newCashListWithoutFirstCash());}}// 由于内部类的关系,在这里完全不用向里面传递任何参数return new GenericCash().getCountOfChanges();}
}

CashList类

public class CashList {private List<Cash> list = new ArrayList<Cash>();public CashList(Cash ... cashes) {if (cashes == null || cashes.length == 0) {throw new IllegalArgumentException("请传入至少一个参数!");}List<Cash> cashList = Arrays.asList(cashes);for (Cash cash : cashList) {if (!list.contains(cash)) {list.add(cash);} else {throw new  IllegalArgumentException("重复的参数!" + cash);}}}public boolean containsOnlyOneCash() {return list.size() == 1;}public Cash getTheOnlyOneCash() {return list.get(0);}/*** 获取不带第一个元素的新list* 如果当前是[20, 10, 5, 1],那么将得到[10, 5, 1]* @return*/public CashList newCashListWithoutFirstCash() {List<Cash> dest = new ArrayList<Cash>(list);dest.remove(0);// new Cash[0] 只是为能推导出泛型类型,本身并无其它意义//也可以使用强制类型转换: new CashSet((Cash[]) dest.toArray());return new CashList(dest.toArray(new Cash[0]));}public Cash getFirstCash() {return list.get(0);}
}

面向过程的实现

当然,如果觉得对于这个不大的问题,面向对象方式显得笨重低效,可以简单给出一个较为过程式的实现,比如直接使用int和array,也不需要对象了,直接就是static的方法。

public static int caseOfChanges(int cash, int[] cashes) {if (cash < 0) {return 0;}if (cash == 0) {return 1;}if (cashes.length == 1) {return cash % cashes[0] == 0 ? 1 : 0;}// 递归调用return caseOfChanges(cash - cashes[0], cashes)+ caseOfChanges(cash, Arrays.copyOfRange(cashes, 1, cashes.length));}

比起对象式的实现,这个显得简洁多了。(当然,考虑到健壮性,还需要多做些检查才行。)

另:这里还是进行了数组的复制,如果能再引入一个index参数,则可以避免复制数组。

测试

以下是一些测试,把100元换成50,20,10,5,1元总共有343种不同结果:

// ========== 对象式实现的测试
@Test
public void testGetCountOfChanges() {CashList list = new CashList(CASH_1, CASH_5);assertThat(CASH_10.getCountOfChanges(list)).isEqualTo(3);
}@Test
public void testManyCountNoOrder() {// 顺序并不重要CashList list = new CashList(CASH_10, CASH_5, CASH_20, CASH_1, CASH_50);assertThat(CASH_100.getCountOfChanges(list)).isEqualTo(343);
}// ========== 过程式,静态式实现的测试
@Test
public void testZeroCountStaticWay() {assertThat(Cash.caseOfChanges(50, new int[] {20})).isEqualTo(0);
}@Test
public void testOneCountStaticWay() {assertThat(Cash.caseOfChanges(100, new int[] {20})).isEqualTo(1);
}@Test
public void testManyCountStaticWay() {assertThat(Cash.caseOfChanges(100, new int[] {50, 20, 10, 5, 1})).isEqualTo(343);
}

注:零钱的顺序其实并不会影响最终结果。

结论?


再次,由于篇幅过长,推迟到下一篇中再去对比这两种实现的优劣,以及对前述的问题分析作些回顾总结。

转载于:https://my.oschina.net/goldenshaw/blog/361718

递归解决换零钱问题--代码实现相关推荐

  1. java零钱换整程序_透析递归应用-换零钱

    题目源于<SICP>,这里做一下调整,如下: 给了面值为50元.20元.10元.5元.1元的五种零钱若干,思考把面值100元人民币换成零钱一共有多少种方式? SICP给出的递归算法思想如下 ...

  2. python换零钱_黄哥Python, 贪心算法解决换零钱问题

    下面的文字来源于维基百科 贪心算法(英语:greedy algorithm) 又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法.比如 ...

  3. Java通过递归解决0-1背包问题的代码

    下面的内容段是关于Java通过递归解决0-1背包问题的内容. public class Knapsack { public static void main(final String... args) ...

  4. python解决换零钱问题_多种解法解决“零钱兑换”问题

    最近在LeetCode上刷算法题,准备秋招.刷了一些题之后,发现有些题非常棒,能够将多种知识点结合在一起.本文就以"零钱兑换"问题为例,展示同一道题的多种不同解法. 零钱兑换问题 ...

  5. 换零钱程序c语言,《SICP》换零钱的递归法与迭代法

    咳咳..先说一段废话.. 最近开始看SICP这本书,正看到了换零钱的部分.看到里面那么多简明生动的例子,还有作者的细心讲解,真是唤起了对学习的无限激情.之前也看过王垠的一些文章,提到了诸如Lisp.s ...

  6. 用递归解决冒泡排序问题

    2019独角兽企业重金招聘Python工程师标准>>> 冒泡排序是种很简单的排序方式.如果用循环方式,通常就是两层循环. 由于两层循环都是与元素个数N线性相关,所以可以简单估算出它的 ...

  7. 51nod 1101 换零钱 【完全背包变形/无限件可取】

    1101 换零钱  基准时间限制:1 秒 空间限制:131072 KB 分值: 20 难度:3级算法题  收藏  关注 N元钱换为零钱,有多少不同的换法?币值包括1 2 5分,1 2 5角,1 2 5 ...

  8. 人民币兑换c语言程序,C语言换零钱:把一元人民币兑换成硬币,共有多少种兑换方法?...

    C语言换零钱:把一元人民币兑换成硬币,共有多少种兑换方法? 答案:6  信息版本:手机版 解决时间 2019-10-07 06:29 已解决 2019-10-06 08:05 以下是我敲的代码,经Mi ...

  9. 汉诺塔(梵塔)问题递归解决

    汉诺塔(梵塔)问题描述:来自于百度百科 相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏.该游戏是在一块铜板装置上,有三根杆(编号A.B.C),在A杆自下而上.由大到小按顺序放置64个金盘( ...

  10. 小P的故事——神奇的换零钱

    小P的故事--神奇的换零钱 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述 已知A国经济很落后,他们只有1.2.3元三种面值的硬币 ...

最新文章

  1. 【GCN】万字长文带你入门 GCN
  2. tableview的两个重用cell方法
  3. 如何在生产环境部署K2的流程
  4. linux测试网页装载时间,如何用Flood测试Web服务器响应时间
  5. WebAPI(part1)--API及DOM
  6. SUSE Linux 维护笔记一
  7. java线程三部分_java 多线程三
  8. 【hive】hive常见的几种文件存储格式与压缩方式的结合-------Parquet格式+snappy压缩 以及ORC格式+snappy压缩文件的方式
  9. shell下利用运算方式编写倒计时脚本
  10. 恭喜你,2018 中国开发者有奖大调查“榜上有名”!
  11. 数据库建表设计规范及原则
  12. js 表格动态增加行通用函数
  13. IDEA调试代码F7、F8、F9
  14. Android控件——TextView与EditText
  15. java首行缩进两个字符,CSS 首行缩进两个文字
  16. 天津科技大学计算机专业,天津科技大学计算机科学与信息工程学院介绍
  17. B15 - 999、大数据组件学习⑫ - Hue
  18. 移动端开发之Web App开发
  19. android ftp 链接不上去,安卓手机无法连接电脑上的ftp(vsftpd)服务器
  20. 自用 学习BCR 免疫组化

热门文章

  1. Windows7中安装内存与可用内存不一致的解决办法
  2. Asp.net 在线转Flv
  3. kubernetes滚动更新
  4. vs2015环境搭建与简单程序测试
  5. 软件测试——JUnit基础
  6. 20145307第二次JAVA学习实验报告
  7. Ruby module ---模块,组件
  8. C# 只允许运行一个实例
  9. CA SDK 使用简介
  10. android平板电脑维修电路图,《图解windows10平板电脑电路原理和维修》大家可以读读看看...