在上一篇中,经过深入分析,已经得出一个能够递归的形式化的结果,现在则准备给出一个具体实现。
结果回顾
前述结果如下:
caseOfChange(amount, cashList) {
// base case
if (amount.isNegative()) { // 负数
return 0;
}
if (amount.isZero()) { // 0元
return 1;
}
if (cashList.containsOnlyOneCash()) { // 只有一种零钱时
if (amount.canBeChangedWith(cashList.getTheOnlyOneCash())) { // 能换时,即能整除
return 1;
} else {
return 0;
}
}
// recursive case
return 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 list = new ArrayList();
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
}
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 list = new ArrayList();
public CashList(Cash ... cashes) {
if (cashes == null || cashes.length == 0) {
throw new IllegalArgumentException("请传入至少一个参数!");
}
List 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 dest = new ArrayList(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
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);
}
注:零钱的顺序其实并不会影响最终结果。
结论?
再次,由于篇幅过长,推迟到下一篇中再去对比这两种实现的优劣,以及对前述的问题分析作些回顾总结。