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

news2024/9/22 19:29:54

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

结果回顾

前述结果如下:

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<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, 只能用这里定义好的, 我们也免去了检查输入合法性的麻烦.

7元纸币

泛化的零钱 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);
}

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

结论

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1925133.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vscode终端(控制台打印乱码)

乱码出现的两种可能&#xff08;重点是下面标题2&#xff09; 1、文件中的汉字本来就是乱码&#xff0c;输出到控制台(终端)那就当然是乱码 在vscode中设置文件的编码格式为UTF-8&#xff0c; 2、输出到控制台(终端)之前的汉字不是乱码&#xff0c;针对此种情况如下设置 原因…

MySQL卸载 - Windows版

MySQL卸载 - Windows版 1. 停止MySQL服务 winR 打开运行&#xff0c;输入 services.msc 点击 “确定” 调出系统服务。 2. 卸载MySQL相关组件 打开控制面板 —> 卸载程序 —> 卸载MySQL相关所有组件 3. 删除MySQL安装目录 4. 删除MySQL数据目录 数据存放目录是在 …

C++从入门到起飞之——缺省参数/函数重载/引用全方位剖析!

目录 1.缺省参数 2. 函数重载 3.引⽤ 3.1 引⽤的概念和定义 3.2 引⽤的特性 3.3 引⽤的使⽤ 3.4 const引⽤ 3.5 指针和引⽤的关系 4.完结散花 个人主页&#xff1a;秋风起&#xff0c;再归来~ C从入门到起飞 个人格言&#xff1a;悟已往之不谏…

端到端自动驾驶系列(一):自动驾驶综述解析

端到端自动驾驶系列(一)&#xff1a;自动驾驶综述解析 End-to-end-Autonomous-Driving Abstract Abstract—The autonomous driving community has witnessed a rapid growth in approaches that embrace an end-to-end algorithm framework, utilizing raw sensor input to …

免费的ssh工具

1.Quickstart - kitty 2 Download Termius for Windows 3. MobaXterm Xserver with SSH, telnet, RDP, VNC and X11 - Download

基于Android平台开发,备忘录记事本

相关视频教程在某站上面(&#x1f50d;浩宇软件开发) 1. 项目功能思维导图 2. 项目涉及到的技术点 使用CountDownTimer实现开屏页倒计时使用SQLite数据库存储数据使用BottomNavigationView实现底部导航栏使用ActivityFragment实现底部导航栏页面切换使用RecyclerViewadapter实…

【人工智能】线性回归

目录 一、使用正规化方法计算下列样本的预测函数 1. 没有归一化之前 2. 归一化之后 二、读取ex1data2.txt中的数据&#xff0c;建立样本集&#xff0c;使用正规化法获取&#xff08;房屋面积&#xff0c;房间数量&#xff09;与房屋价格间的预测函数 1. 读取数据&#xff…

【OpenCV】BGR三色通道的提取与合并--超详细解读

在OpenCV中&#xff0c;处理图像时经常需要提取或合并图像的RGB&#xff08;红、绿、蓝&#xff09;三色通道。OpenCV默认使用BGR&#xff08;蓝、绿、红&#xff09;顺序来存储图像的颜色通道&#xff0c;这一点与很多图像处理库&#xff08;如PIL/Pillow&#xff09;不同&…

【项目计划】软件项目计划(Word)

项目开发计划包括项目描述、项目组织、成本预算、人力资源估算、设备资源计划、沟通计划、采购计划、风险计划、项目过程定义及项目的进度安排和里程碑、质量计划、数据管理计划、度量和分析计划、监控计划和培训计划等。 软件资料清单列表部分文档&#xff1a; 工作安排任务书…

Renesas R7FA8D1BH (Cortex®-M85) 读取芯片内部温度值

目录 概述 1 软硬件 1.1 软硬件环境信息 1.2 开发板信息 1.3 调试器信息 2 FSP和KEIL配置ADC 2.1 ADC硬件接口 2.2 FSP配置ADC 3 软件功能实现 3.1 FSP生成项目 3.2 FSP ADC模块库函数介绍 3.2.1 库函数列表 3.2.2 函数介绍 4 读Temperature sensor 4.1 初始化ADC…

Matlab-Simulink模型保存为图片的方法

有好多种办法将模型保存为图片&#xff0c;这里直接说经常用的 而且贴到Word文档中清晰、操作简单。 simulink自带有截图功能&#xff0c;这两种方法都可以保存模型图片。选择后直接就复制到截切板上了。直接去文档中粘贴就完事了。 这两个格式效果不太一样&#xff0c;第一种清…

麒麟系统开发笔记(十四):在国产麒麟系统上编译libmodbus库、搭建基础开发环境和移植测试Demo

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/140387947 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

多数元素-哈希表

169. 多数元素 - 力扣&#xff08;LeetCode&#xff09; 哈希表来找出这个数出现几次 class Solution { public:int majorityElement(vector<int>& nums) {unordered_map<int,int> counts;int cnt 0, majority 0;for(int num : nums){counts[num];if(counts…

Python写api程序批量自动抓取商品评论数据演示

要实现一个Python程序批量自动抓取商品评论数据&#xff0c;你可以使用requests库来发送HTTP请求&#xff0c;并使用BeautifulSoup库来解析HTML页面。以下是一个简单的示例&#xff1a; 首先&#xff0c;确保已经安装了所需的库&#xff1a; pip install requests pip instal…

【Python学习笔记】Optuna + Transformer B站视频实践

【Python学习笔记】Optuna Transformer 实践 背景前摇&#xff08;省流可不看&#xff09;&#xff1a; 之前以泰坦尼克号数据集为案例&#xff0c;学习了Optuna的基本操作&#xff0c;为了进一步巩固知识和便于包装简历&#xff0c;决定找个唬人一点的项目练练手。 ————…

《揭秘深度强化学习》:一本揭示AI前沿技术的必读书籍

在人工智能&#xff08;AI&#xff09;领域飞速发展的今天&#xff0c;深度强化学习作为一种革命性技术&#xff0c;正在改变我们的世界。今天要向大家推荐的是《揭秘深度强化学习》这本书&#xff0c;它不仅为读者提供了深度强化学习的全面指南&#xff0c;还揭示了这一技术的…

常用I/O复用模型 --> 一、单线程Accept(无IO复用)

文章目录 一、前言二、I/O复用中最基础的知识点1、流2、I/O操作3、阻塞等待4、非阻塞&#xff0c;忙轮询5、多路I/O复用 三、单线程Accept(无IO复用)1、服务端2、客户端 一、前言 单线程Accept(无IO复用)是网络最基础的模型&#xff0c;常供学习使用。 下面是我的GitHub仓库&…

韦东山嵌入式linux系列-驱动设计的思想(面向对象/分层/分离)

1 面向对象 字符设备驱动程序抽象出一个 file_operations 结构体&#xff1b; 我们写的程序针对硬件部分抽象出 led_operations 结构体。 2 分层 上下分层&#xff0c;比如我们前面写的 LED 驱动程序就分为 2 层&#xff1a; ① 上层实现硬件无关的操作&#xff0c;比如注册…

各种Attention|即插即用|适用于YoloV5、V7、V8、V9、V10(一)

摘要 本文总结了各种注意力&#xff0c;即插即用&#xff0c;方便大家将注意力加到自己的论文中。 SE import torch from torch import nn class SEAttention(nn.Module): """ SENet&#xff08;Squeeze-and-Excitation Networks&#xff09;中的注意力…

浅析 VO、DTO、DO、PO 的概念

文章目录 I 浅析 VO、DTO、DO、PO1.1 概念1.2 模型1.3 VO与DTO的区别I 浅析 VO、DTO、DO、PO 1.1 概念 VO(View Object) 视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。DTO(Data Transfer Object): 数据传输对象,这个概念来源于J2EE的设…