找零问题的四种解决方法(Java)

找零问题,四种方法

  • 1、贪心算法
    贪心算法的核心思想就是只取眼前的最佳值,不从整体看而是遵从局部最优选择,因此贪心算法不一定能得出全局最优解。
	/**
 	* 1、贪心算法:获取零钱数量的数组,其元素为不同面值类前所需的数量
	 * @param total 总金额
 	* @param changes 零钱面值表,要逆序排列(即从大到小)
	 * @return 返回一个零钱数量数组
	 */
	public static int[] getChangeByGreddy(int total, int[] changes) {
		int[] changeDetail = new int[changes.length]; // 记录不同面值的零钱要找多少张
		Arrays.fill(changeDetail, 0);
		
		for (int i = 0; i < changes.length; i++) {
			changeDetail[i] = total / changes[i];
			total -= changes[i] * changeDetail[i];
			if (total == 0) return changeDetail;
		}
		return null;
	}

代码执行结果如下:

total=155
changes[]={15, 10, 5}

输入总金额:
155
需要面值 15的数量为:10
需要面值 10的数量为:0
需要面值 5的数量为:1
贪心算法结果: 11
  • 2、递归:自顶向下,类似树形结构,要完成递归必须具备两个限制条件:
    1)终止递归的限制条件
    2)越来越接近终止条件
	/**
	 * 2、递归:获取找零数量,调用findMinAmount递归方法
	 * @param total 总金额
	 * @param changes 零钱面值表
	 * @return 返回要找的零钱数量
	 */
	public static int getChangeByRecursion(int total, int[] changes) {
		if (changes.length == 0) return -1;
		findMinAmount(total, changes, 0);
		if (result == Integer.MAX_VALUE) return -1; // 不能找零返回-1
		else {
			return result; // result是一个全局变量,代表最终结果
		}
	}
	
	/**
	 * 递归方法,在getChangeByRecursion中被调用,该方法目的是为了找到total金额可以兑换的最少零钱数量
	 * @param remain 剩余的金额(最开始为总金额total,在递归中不断减少)
	 * @param changes 零钱面值表
	 * @param count 所需零钱的数量
	 */
	public static void findMinAmount(int remain, int[] changes, int count) {
		if (remain < 0) return; // 递归的第一个限制条件:total<0时递归停止
		if (remain == 0) result = Math.min(result, count);
		for (int i = 0; i < changes.length; i++) {
			findMinAmount(remain - changes[i], changes, count + 1); // 第二个限制条件:每次递归调用之后会越来越接近第一个限制条件,即total越来越小
		}
	}

代码执行结果如下:

total=20 
changes[]={15, 10, 5}

输入总金额:
20
递归结果: 2

递归过程的树状图如下:
递归过程的树状图

  • 3、记忆化搜索:自顶向下,对于可能出现的重复计算,使用一个数组来保存其值,在进行重复的计算的时候,就可以直接调用数组中的值,减少不必要的递归。
	/**
	 * 3、记忆化搜索,调用递归方法findMinAmount2,比方法2直接递归快很多
	 * @param total 当前的金额
	 * @param changes 零钱面值表
	 * @param count 所需零钱的数量
	 * @return 返回findMinAmount2的结果,即最小零钱数
	 * 时间复杂度:O(total*changes.length)
	 * 空间复杂度:O(total)
	 */
	public static int getChangeByMemory(int total, int[] changes) {
		if (total < 1) return 0;
		return findMinAmount2(total, changes, new int[total]); // 调用递归方法
	}
	
	/**
	 * 递归方法,在getChangeByMemory中被调用
	 * @param remain 剩余的金额(最初为总金额total,在递归中不断减少)
	 * @param changes 零钱面值表
	 * @param counts 不同金额所需零钱数的最小值数组,counts[total-1]代表total金额所需最小零钱数,记忆化搜索的特点在此
	 * @return 某金额(<=total)所需最小零钱数
	 */
	public static int findMinAmount2(int remain, int[] changes, int[] counts) {
		if (remain < 0) return -1; 
		else if (remain == 0) {
			return 0;
		}
		else if (counts[remain-1] != 0){ // remain金额的最小零钱数不为零则直接返回该数,不再执行后面的递归
			return counts[remain-1];
		}
		
		//若counts[remain]不存在,则继续下面的步骤给counts[remain]赋值
		int min = Integer.MAX_VALUE; // 最小零钱数
		for (int change : changes) {
			int minNumber = findMinAmount2(remain - change, changes, counts); // 递归
			if (minNumber < min && minNumber >= 0) min = minNumber + 1; // 加一是算上total-change中change面值的一张数量
		}
		counts[remain-1] = min;
		return counts[remain-1];
	}

代码执行结果如下:

输入总金额:
155
记忆化搜索结果: 11
  • 4、动态规划:自底向上,记忆化搜索是从counts[total-1] 开始,而动态规划从counts[1] 开始,counts[i]counts[1] ~counts[i-1] 决定,根据局部的最优解获得整体的最优解。

采用自下而上的方式进行思考,定义 Counts(i) 为组成金额 i 所需最少零钱数,changes[j] 代表零钱数组中第j+1个零钱面值,Counts(i) 对应的状态转移方程如下:

C o u n t s ( i ) = m i n j = 0... n − 1 ( C o u n t s ( i − c h a n g e s [ j ] ) ) + 1 (1) Counts(i)= min_{j=0...n-1}(Counts(i-changes[j]))+1 \tag{1} Counts(i)=minj=0...n1(Counts(ichanges[j]))+1(1)

	/**
	 * 4、动态规划
	 * @param total 当前的金额
	 * @param changes 零钱面值表
	 * @return 返回最小零钱数
	 * 时间复杂度:O(total*changes.length)
	 * 空间复杂度:O(total)
	 */
	public static int getChangeByDp(int total, int[] changes) {
		if (total < 1) return -1;
		int max = total; 
		int[] counts = new int[total+1]; // 存储0~total所有金额分别所需的最小零钱数的数组
		Arrays.fill(counts, max); // 给counts的所有元素设置一个最大初始值
		counts[0] = 0;
		
		// for(i)确定counts[i]
		for (int i = 1; i <= total; i++) {
			// for(j)遍历changes[],找到能给金额i找零且所需零钱数最小的面值,并将零钱数赋给counts[i]
			for (int j = 0; j < changes.length; j++) {
				if (changes[j] <= i) {
					/*
					 * 将金额i拆分为i-changes[j]+changes[j],将该式子转换为对应的零钱数量:counts[i-changes[j]]+1,并与counts[i]比较,取较小值
					 * counts[i-changes[j]]是子问题的结果,即小于金额i的其他金额所需最小零钱数,加一代表的是changes[j]的那一张零钱
					 */
					counts[i] = Math.min(counts[i], counts[i-changes[j]]+1); 
				}
			}
		}
		return counts[total] > max ? -1 : counts[total]; // 若counts[total]比初始值小,返回该值为最终结果,否则返回-1
	}

代码执行结果如下:

total=10 
changes[]={15, 10, 51}
输入总金额:
10
动态规划结果: 1

具体执行过程如下图:
动态规划找零的具体执行过程
参考文章:
[1] https://leetcode.cn/problems/coin-change/solutions/137661/javadi-gui-ji-yi-hua-sou-suo-dong-tai-gui-hua-by-s/
[2] https://leetcode.cn/problems/coin-change/solutions/132979/322-ling-qian-dui-huan-by-leetcode-solution/

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值