递归解决换零钱问题--代码实现

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

结果回顾

前述结果如下:

java 复制代码
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:

java 复制代码
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 即可.

java 复制代码
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).

java 复制代码
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 等方法也打包到它的内部去:

java 复制代码
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, 外部根本不需要知道这个类的存在, 直接把它扔在方法内部定义,

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

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

最终结果

一个完整的结果如下:

Cash 类:

java 复制代码
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 类

java 复制代码
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 的方法.

java 复制代码
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 种不同结果:

java 复制代码
// ========== 对象式实现的测试
@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);
}

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

结论

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

相关推荐
rolt1 天前
长得像用例图的类图-《软件方法》8.2.3.4
软件工程·uml·面向对象
职创未来官方1 天前
大话C++:第26篇 类模板
c++·面向对象·模板·模板类·嵌入式物联网·隐式实例化·显示实例化
FreakStudio2 天前
全网最适合入门的面向对象编程教程:58 Python字符串与序列化-序列化Web对象的定义与实现
python·单片机·嵌入式·面向对象·电子diy
小卡皮巴拉9 天前
【力扣刷题实战】相同的树
c语言·算法·leetcode·二叉树·递归
一直学习永不止步12 天前
LeetCode题练习与总结:4的幂--342
java·数据结构·数学·算法·leetcode·递归·位运算
职创未来官方15 天前
大话C++:第23篇 输入输出
c++·物联网·面向对象·格式化·嵌入式开发·1024程序员节·输入输出
熬夜学编程的小王19 天前
【C++篇】剖析内存底沉:C/C++高效内存管理剖析
c++·内存管理·面向对象·new/delete·malloc/free
LabVIEW开发20 天前
LabVIEW提高开发效率技巧----VI继承与重载
面向对象·labview开发技巧·继承与重载
西瓜本瓜@21 天前
在 Android 开发中,如何实现蓝牙连接设备?
android·java·开发语言·面向对象·蓝牙链接
rolt22 天前
有向无环图的约束怎么表达-《分析模式》漫谈39
ddd·uml·领域驱动设计·面向对象