咳咳..先说一段废话..

最近开始看SICP这本书,正看到了换零钱的部分。看到里面那么多简明生动的例子,还有作者的细心讲解,真是唤起了对学习的无限激情。之前也看过王垠的一些文章,提到了诸如Lisp、scheme等语言,不过当时对此是零概念(其实现在对scheme的了解,也止于SICP换零钱这部分章节)。在了解了mit-scheme的一些基本用法后,虽然也觉得用‘括号’这种语法的确很优雅,但当我真的写完一个长长的定义之后,游荡在许许多多括号中间,找个小bug,真心觉得头疼。

换零钱

回到正题,先说一下题目:

给了50、25、10、5、1美分的硬币,将1美元(100美分)换成零钱,总共有多少种换法?

在这之前,书中也提到了一般有两种方法,线性递归和线性迭代,书中给出的解法是一个最为直观的,效率也最低的解法——线性递归。

递归法

书中的思路这样描述:

将总数为a的现金换成n种硬币的不同方式的数目等于

将现金数a换成除第一种硬币之外的所有其他硬币的不同方式数目,加上

将现金数a-d换成所有种类的硬币的不同方式数目,其中的d是第一种硬币的币值

按照这种规则,则可以总结出如下算法:

如果a就是0,应该算作是有1种换零钱的方式。

如果a小于0,应该算作是有0种换零钱的方式。

如果n是0,应该算作是有0种换零钱的方式。

n应该代表的是,第几种硬币(有1、2、3、4、5这五种币种,0就是没有可分配的币种)。

这个递归过程的代码为:

(define (first-denomination kinds-of-coin)

(cond ((= kinds-of-coin 1) 1)

((= kinds-of-coin 2) 5)

((= kinds-of-coin 3) 10)

((= kinds-of-coin 4) 25)

((= kinds-of-coin 5) 50)))

(define (cc amount kinds-of-coin)

(cond ((= amount 0) 1)

((or (< amount 0) (= kinds-of-coin 0)) 0)

(else (+ (cc amount

(- kinds-of-coin 1))

(cc (- amount

(first-denomination kinds-of-coin))

kinds-of-coin)))))

(define (count-change amount)

(cc amount 5))

所以测试了一下,结果为:

1 ]=> (count-change 100)

;Value: 292

这个递归运算是指数级的,现在看起来没什么问题,但是当总钱数比较大时,问题就明显了。

迭代法

又是一段废话...

作者说,这一解法留给读者作为一个挑战。看到'挑战'这个字眼,凭着对求知的好胜心,我的心就痒痒了,然后接下来就是自虐的过程。虽然出身于数学系,但是大学期间,基本都把青春献给dota了。

面对这个一层套一层的问题,脑袋已经不够用了,想了许多种思路,几乎都是半途发现行不通又重新思考。在想了几个小时后,终于忍不住去网上搜索答案,英文不好,只能搜搜中文的。发现有个答案,大概看了一下,说是用数组先把一部分结果算出来,然后复用。因为我的scheme水平实在有限,没接触到那些用法,所以也没细看,而且我觉得这个解法,也不符合我想要的那种答案。

只好回来继续虐自己吧,光空想,脑袋是转不过来了。突然想起,书中一般都是通过画一些推导图之类的,来寻求规律,于是我也拿起笔画起来,终于找到了一种思路。

思路

递归法是通过把每一个大问题划分成两个小问题,从而生一个树形解法,思路虽然比较好理解,但是运算时就要把树全部展开,而且子树当中存在大量重复计算,效率可想而知。而迭代法不能延续这种思路,只能通过自身的参数,来分析当前迭代的情况,并计算出来,进行下一次迭代。所以我的想法是,可不可以每次迭代通过每种币种的数量,计算出剩余的钱,然后分析出当前情况是否是一个有效的分法,然后进行下一次迭代,直至每种币种的所有数量情况遍历完毕。

怎么判断,什么是一个有效的分法呢?就是每种硬币,它的币值乘以它的数量,最后相加在一起,正好等于总钱数时,即剩余钱数是0时,就是一种有效的分法。

设有如下对应关系:

币种

币值

数量

1

50

c1

2

25

c2

3

10

c3

4

5

c4

5

1

c5

总钱数为amount;

那么,当amount = coin1*c1+coin2*c2+coin3*c3+coin4*c4+coin5*c5;时,

就是一种有效的分法。

有几个问题需要考虑:

按照怎么样顺序去遍历各币种的数量,直至当剩余钱数符合一种特殊情况(小于0或者等于0)。

当剩余钱数等于或者小于0的时候,怎样安排下一次顺序。

怎么判断结束情况。

分钱这种事,大概一想也知道,如果你有100块,分别可分成2张50块的,4张25块的,10张10块的,20张5块的,100张1块的。币值越大,可分的数量就越少,如果给定有一张是50块的,那令外50又可分成,2张25块的,5张10块的等等...

按这个规律,我们又发现,全分成50的,有2张;50、25混着分,有3张;全分成25的,有4张。这样就是说,币种平均值越小,可分的张数就越多。

那我们就先从平均值最大的币种开始遍历(没试过从平均值最小的遍历),迭代就是只要剩余钱数大于0,就让数量增加,每当遇到特殊情况,也就是剩余钱数小于0或者等于0时,就开始遍历平均值第二大的,这样可分的张数就越来越多。直到只剩下平均值最小的币种了,那么就结束了。

方法

先从币值最大的币种(第一种币种)开始遍历,每次数量加1,计算出剩余钱数,直到剩余钱数等于0或者小于0时,就将此次的最大币种的数量减1,并将币值第二大的币种(第二种币种)数量加1;接下来每次第一种的数量保持不变,将第二种的数量加1,再次直到剩余钱数等于或者小于0,然后将第二种数量减1,第三种数量加1,此时重复之前的步骤,直至到最后一个币种。可以发现,我们需要有个标识记录,当前是哪种硬币的数量应该加1。总结后,遍历情况如下图:

图1.jpg

思考:

到了图中最后一步时,游标已经走到了最后一个币种,但此时遍历还远没有结束。接下来就是需要找到比现在的币种平均值要小的搭配了。一开始,我直接把第一个币种的数量减1,并且后面的币种数量清0,进而开始迭代,结果发现,漏掉了许多情况。漏掉的情况就是当第一个币种数量不变(即上图中数量为1)时,除第一种币种之外,剩余其他币种的随意搭配。所以,只能从后往前,开始减少币种的数量,这样就不会漏掉情况了。

接下来继续,目前游标已经是第五种硬币了,所以要将前面离的最近的且数量不为0的币种数量减1,将下一币种数量加1(将游标移动到此币种),再将这之后的币种硬币数清零,然后继续用之前的方法迭代。

接着图1继续迭代,如下图:

图2.jpg

最后一个问题,怎么判断结束呢?

由于是一步一步减少币值较大的币种的数量,所以最后会看见这样一幅画面:

图3.jpg

很明显,当前四种硬币的数量为0时,第5种硬币数量为100,这就是最后一种有效的分法,可以结束了。

代码

方法已经有了,接下来就是还原成代码吧。由于对scheme不熟,如果直接用scheme,估计我就崩溃了。为了快速验证我的想法,我首先用C语言实现了一下(我用的是Xcode),代码如下:

// 获取对应币种的币值

int get_coin(int index){

switch (index) {

case 1:{

return 50;

break;

}

case 2:{

return 25;

break;

}

case 3:{

return 10;

break;

}

case 4:{

return 5;

break;

}

case 5:{

return 1;

break;

}

default:

break;

}

return 0;

}

// 为了计算执行了多少次迭代

static long long cn2 = 0;

// 剩余钱数 有效分法 游标

static int c_c(int leftAmount, int count, int cur, int c1, int c2, int c3, int c4, int c5){

cn2++;

// 当剩余钱数等于或者小于0时

if (leftAmount == 0 || leftAmount < 0){

// 1.当游标走到4时,其实是从前往后遍历的时候

// 2.当游标在5时,但是第4种硬币的数量大于0,属于从后往前遍历

// 这两种情况处理是一样的,之后的判断与处理跟此处类似

// 这里还有一个稍微特殊一点的地方:

// 当属于第2种情况时,由于第5种硬币已经是最后一种硬币,所以直接加1,且之后并没有需要数量清零的币种

if ((cur==5 && c4>0) ||

cur==4){

return c_c(leftAmount+get_coin(4)-get_coin(5),

(leftAmount<0)?count:count + 1,// 如果剩余钱数等于0,有效次数加1,反之不变

5,

c1,

c2,

c3,

c4-1,

c5+1);

}

else if ((cur==5 && c4==0 && c3>0) ||

cur==3){

return c_c(leftAmount+get_coin(3)-get_coin(4)+get_coin(5)*c5,

(leftAmount<0)?count:count + 1,

4,

c1,

c2,

c3-1,

c4+1,

0);

}

else if ((cur==5 && c4==0 && c3==0 && c2>0) ||

cur==2){

return c_c(leftAmount+get_coin(2)-get_coin(3)+get_coin(4)*c4+get_coin(5)*c5,

(leftAmount<0)?count:count + 1,

3,

c1,

c2-1,

c3+1,

0,

0);

}

else if ((cur==5 && c4==0 && c3==0 && c2==0 && c1>0) ||

cur==1){

return c_c(leftAmount+get_coin(1)-get_coin(2)+get_coin(3)*c3+get_coin(4)*c4+get_coin(5)*c5,

(leftAmount<0)?count:count + 1,

2,

c1-1,

c2+1,

0,

0,

0);

}

// 结束情况: (cur==5 && c4==0 && c3==0 && c2==0 && c1==0)

else{

// 如果剩余钱数等于0,有效次数加1,并返回

return (leftAmount<0)?count:count+1;

}

}

// 剩余钱数大于0

else{

// 继续将游标处的币种数量加1

return c_c(leftAmount-get_coin(cur),

count,

cur,

(cur==1)?c1+1:c1,

(cur==2)?c2+1:c2,

(cur==3)?c3+1:c3,

(cur==4)?c4+1:c4,

(cur==5)?c5+1:c5);

}

}

int getCountChange(int amount){

cn2 = 0;

// 将第一种搭配当参数传入

int count = c_c(amount-get_coin(1),0,1,1,0,0,0,0);

printf("执行了%lld次",cn2);

return count;

}

然后进行测试

printf("count: %d",getCountChange(100));

控制台打印:

->] 执行了1554次

->] count: 292

其实我也把递归法实现了,有对比才好说话,递归法 (getCountChange2) 的测试如下:

printf("count: %d",getCountChange2(100));

控制台打印:

->] 执行了15499次

->] count: 292

看来差距还不是非常的大(10倍),那增加到500试试...

printf("迭代法count: %d",getCountChange3(500));

printf("递归法count: %d",getCountChange2(500));

控制台打印:

->] 迭代法执行了346730次

->] 迭代法count: 59576

->] 递归法执行了12822611次

->] 递归法count: 59576

恩,看来差距还是有的。

细心的朋友可能发现了,这个迭代法用的是getCountChange3,这个并不是bug。其实当我用getCountChange方法时,总钱数到200多的时候,Xcode就阻止继续运行了,此时是迭代了3万次左右,这个原因我不太懂,可能是会检查函数的最大循环调用次数。所以我写了个getCountChange3方法,方法里面用一个循环代替了函数递归,其他方式都不变,以此模拟函数迭代法。

看来没什么问题了,可以用scheme测试了。代码如下:

(define (get-coin index)

(cond ((= index 1) 50)

((= index 2) 25)

((= index 3) 10)

((= index 4) 5)

((= index 5) 1)))

(define (c-c leftAmount count cursor c1 c2 c3 c4 c5)

(cond ((or (= leftAmount 0) (< leftAmount 0)) ; 当剩余的钱小于或等于0的时候

(cond ((or (= cursor 4)

(and (= cursor 5) (> c4 0))) ;如果cursor=4,或者 cursor=5且c4>0

(c-c (- (+ leftAmount

(get-coin 4))

(get-coin 5)) ;leftAmount将第4种硬币钱数加回来一个,且c4减1,紧接着加上第5种硬币钱数,且c5加1

(if (< leftAmount 0)

count

(+ count 1)) ;如果leftAmount=0,说明正好分完一次,所以count加1;反之,count不变

5 ;将游标指向第五种硬币

c1

c2

c3

(- c4 1) ;c4减1

(+ c5 1))) ;c5加1,不清零是因为c5是最后一种硬币,无需从零开始计算

((or (= cursor 3)

(and (= cursor 5) (= c4 0) (> c3 0)))

(c-c (- (+ leftAmount

(get-coin 3)

(* c5 (get-coin 5)));此时c5需要清零,所以把第五种硬币的钱数都加回来

(get-coin 4))

(if (< leftAmount 0)

count

(+ count 1))

4

c1

c2

(- c3 1)

(+ c4 1)

0))

((or (= cursor 2)

(and (= cursor 5) (= c4 0) (= c3 0) (> c2 0)))

(c-c (- (+ leftAmount

(get-coin 2)

(* c4 (get-coin 4))

(* c5 (get-coin 5)))

(get-coin 3))

(if (< leftAmount 0)

count

(+ count 1))

3

c1

(- c2 1)

(+ c3 1)

0 ;清零原理同上

0))

((or (= cursor 1)

(and (= cursor 5) (= c4 0) (= c3 0) (= c2 0) (> c1 0)))

(c-c (- (+ leftAmount

(get-coin 1)

(* c3 (get-coin 3))

(* c4 (get-coin 4))

(* c5 (get-coin 5)))

(get-coin 2))

(if (< leftAmount 0)

count

(+ count 1))

2

(- c1 1)

(+ c2 1)

0

0

0))

(else (if (< leftAmount 0); 此时c4=0,c3=0,c2=0,c1=0,所以全部遍历完毕,结束。

count

(+ count 1)))))

(else (c-c (- leftAmount

(get-coin cursor));此时如果leftAmount>0,继续将游标指向的硬币数量加1

count

cursor

(if (= cursor 1)

(+ c1 1)

c1)

(if (= cursor 2)

(+ c2 1)

c2)

(if (= cursor 3)

(+ c3 1)

c3)

(if (= cursor 4)

(+ c4 1)

c4)

(if (= cursor 5)

(+ c5 1)

c5)))))

(define (count-change2 amount)

(c-c (- amount

(get-coin 1))

0

1

1

0

0

0

0))

进行测试

1]=> (count-change2 100)

;Value: 292

为了与递归法对比,我分别执行了钱数为1000的情况,迭代法也是过了20秒左右才出结果(801451种),但是递归法至今还没有算出来(也许是我等的时间太短了?)...

优化

仔细研究研究这个算法,还是有优化的余地的,比如当游标处的币种数量减1时,下一币种数量不是加1,而是直接加上当前剩余钱数(因为剩余钱数有可能小于0)与此游标处币种的币值与需要清零的所有币值的和,然后再除以游标下一币种的币值最后得到的商,这样可以减少迭代次数。而且按照这个思路,第一次传入参数的时候,也直接把第一种硬币的数量设置为总钱数与第一种币值的商,也减少了一些迭代次数(总钱数越大,减少的也越多,不过这个第一次的计算在计算机面前可以忽略的,只是为了追求完美)。

这个方法我试了,对于1000这个总钱数,从递归法的333082021次调用,到上述迭代法的4732860次调用,最后到这个优化迭代法的801871调用(总换法是801451,个人觉得对于这种遍历的算法,真是优化到了极致

换零钱程序c语言,《SICP》换零钱的递归法与迭代法相关推荐

  1. c语言折半查找递归程序,C语言数据结构中二分查找递归非递归实现并分析

    C语言数据结构中二分查找递归非递归实现并分析 前言: 二分查找在有序数列的查找过程中算法复杂度低,并且效率很高.因此较为受我们追捧.其实二分查找算法,是一个很经典的算法.但是呢,又容易写错.因为总是考 ...

  2. c语言递归方法排序,归并排序,递归法,C语言实现。

    利用归并排序法对序列排序的示意图(递归法): 一.算法分析:利用递归的分治方法:1.将原序列细分,直到成为单个元素:2.在将分割后的序列一层一层地按顺序合并,完成排序.细分通过不断深入递归完成,合并通 ...

  3. java零钱换整程序_贪心算法换零钱(java)

    贪心算法思想 贪心算法总是做出在当前看来做好的选择.也就是说贪心算法并不从整体最后考虑,他做出的选择只是局部最优选择.他所做出的仅是在某种意义上的局部最优解.贪心算法不是对所有问题都能得到整体最优解, ...

  4. 每日程序C语言41-带头结点的头插法创建链表

    题目: 创建单链表(3) 程序分析 带头结点的头插法创建链表 带有头结点的链表,头结点中的data中没有放置任何元素,头结点存储的地址就是链表首元素存储的地址.头插法顾名思义就是从链表的头部进行插入, ...

  5. 每日程序C语言39-不带头结点的头插法创建链表

    题目: 创建一个链表(1) 程序分析 不带头结点的头插法创建链表 首先创建的结点会出现在整个链表的最末端,所以数据的写入是逆序的 主要代码 main.c //创建结点类型 typedef struct ...

  6. 用查表法写c语言程序,C语言day07-15进制查表法

    #include void printfBinary(int value); void printfBunary2(int value); void printOct(int value); // 8 ...

  7. c语言上20级台阶递归法,c语言递归算法.pptx

    计算机语言与程序设计;第八章 递归算法;从前有座山,山上有座庙,庙里有一个老和尚和一个小和尚,老和尚正在给小和尚讲故事.讲的是什么故事呢?他说,从前--;Recursion- See "Re ...

  8. C语言:卖鸭子问题---递归法实现

    1.一个人赶着鸭子去每个村庄卖,每经过一个村子卖去所赶鸭子的一半又一只.这样他经过了七个村子后还剩两只鸭子,问他出发时共赶多少只鸭子?经过每个村子卖出多少只鸭子? 代码: #include<st ...

  9. c语言递归函数检测回文,递归法判断回文字符串,急用

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 /*--------用线性表存储字符串,结合堆栈判断回文(关键判断个数为奇或偶,奇跳过中间数据)---------*/ #include #include ...

最新文章

  1. 联机共享 Windows Live Writer 草稿
  2. linux C编程之makefile
  3. gzip、bzip2和tar
  4. Intel Realsense D435 在C/C++中表示的frame_set就是python中的frames?【wait_for_frames()】
  5. Java 算法 寂寞的数
  6. 复变函数:傅里叶变换
  7. DNS 的工作原理——域名系统
  8. win服务器系统无法切换输入法,Win10系统下中英文输入法无法切换的解决方案
  9. 推荐系统-协同过滤在Spark中的实现
  10. 关于python语言、下列说法不正确的是-模拟试卷C【单项选择题】
  11. stm32f4定时器时钟频率/选择
  12. java人工智能开源项目_人工生命——一个用代码模拟生命进化的人工智能开源项目...
  13. Ubuntu20.04连接校园网WiFi
  14. android 单元布局,在Android布局中挖洞
  15. MATLAB App Designer入门实战(一)
  16. 带有图片的Excel信息导入
  17. # 如何破429问题|x-kpsdk-ct|x-kpsdk-cd|snkrs bot ## nike 429| snkrs 429|nike 新盾 |snkrs 新盾|nike源码|snkrs 源码
  18. 淘宝无线端店铺权重提升方法技巧步骤
  19. 微信支付 生成付款二维码链接 并生成支付二维码图片 .net C#
  20. Oracle Linux 6 3下安装Oracle 11g R2 11 2 0 3

热门文章

  1. 小学老师工资多少一个月_当农村小学教师工资一年有多少,我给你们看一看
  2. Ubuntu 18下安装tensorflow(基于python3.6)
  3. 物联网毕业设计选题大全
  4. 集体备课模板_小学语文集体备课教案模板(共5篇)
  5. 教师计算机返岗实践方案,国培计划返岗实践方案
  6. ATM机、自助查询机无线联网方案
  7. ubuntu 18.04 新配主机 无wifi适配器 找不到wifi问题解决
  8. HCNP RoutingSwitching之RSTP保护
  9. 海投的简历暴露了你的什么问题?
  10. 阿里官方Java代码规范标准解析 - 基本数据类型与包装数据类型的使用标准