news 2026/9/10 6:58:55

贪心算法入门:用找零问题与if语句设计编程教案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
贪心算法入门:用找零问题与if语句设计编程教案

1. 贪心算法到底是什么——先弄清楚找零问题的本质

很多朋友第一次听“贪心算法”这四个字,第一反应是“这又是哪个竞赛选手发明的高深玩意儿”。其实你把名字拆开就明白了:贪心,就是每一步都贪心地选当前最好的那个选项,不考虑后面会不会后悔。放在金额找零问题里,意思就是:要凑出某个金额时,每次先拿面值最大的硬币,能拿几张拿几张,然后换次大的面值继续拿,直到金额归零。

这里关键来了,为什么说“if 解题”?因为贪心算法的每一次抉择,本质上就是一个“条件判断”:当前剩余金额够不够我拿这张大面值?够就拿下,不够就跳到下一档。你不用实现什么复杂的搜索、回溯,也不用维护一张二维表,只要根据金额大小一层一层判断就行。这恰恰是新手最容易接受的理解方式。

有人可能会问:这么简单的策略,也能算一种“算法”?是的,它不但算,而且是算法竞赛和工程实践中非常基础的一种思想。找零、区间调度、哈夫曼编码、最小生成树,背后都有它的影子。而在教学场景里,“金额找零”是公认最好的贪心入门案例,因为需求直观、不需要数学前置知识,代码量又少,一节课就能让学生体会到“策略设计”的乐趣。

我需要提前说明一点:贪心算法不是万能的。它对问题的性质有要求,有些找零场景它给出的是最优解,有些场景它给出的只是“可行解但未必最优”。这个坑我会在第4章专门讲,因为这是新手最容易误解的地方。现在你先记住一句话:贪心的本质是“局部最优推导全局最优”,这个推导成立是有条件的。

这节内容适合谁?如果你正在学习算法但被动态规划劝退过,如果你准备带学生入门编程但找不到合适的教学案例,或者你只是好奇“if 还能这么玩”,这篇文章都能给你一个完整、可复制的方案。我会把教学台词、代码、习题、坑点全部摆出来,你拿来就能用。

2. 教案设计思路——为什么找零问题适合用“if 判断”做教学切入点

2.1 教学目标的拆解:不是教语法,是教思维

我在设计这节教案时,首要目标不是让学生背下来“贪心算法”的定义,而是让他们通过一次真实的编程练习,体会到“策略选择”是如何转化为“代码逻辑”的。如果一上来就抛概念、贴伪代码,学生很容易陷入“听懂了但不会写”的困境。

所以我把目标拆成了三个层次:

  • 第一层:能用 if 语句描述“当前金额是否足够使用某面额”的判断逻辑。
  • 第二层:能手动模拟贪心选择的每一步,理解“从大到小逐个尝试”的合理性。
  • 第三层:能识别贪心算法的适用边界,知道它在某些场景下会失效。

这样的目标设计有一个好处:即使学生基础薄弱,也能在第一个层次获得成就感;而基础好的学生,可以在第三层做延伸思考,避免“一节课下来啥也没学到”的感觉。我建议你在实际教学中,把这三点目标直接写在黑板或课件首页,让学生带着目标去听课,效果会好很多。

2.2 为什么用“if”而不是一上来就写 while 循环

我见过很多教材,讲找零问题时直接上 while 循环,配合取整和取余运算,代码确实简洁,但对新手并不友好。原因在于:循环体里的“反复执行”逻辑会掩盖掉贪心策略本身。学生看代码时满脑子都是“这个循环什么时候停”,反而忽略了“每次循环在做什么决策”。

用 if 判断的好处是透明的。你把每一种面额单独写一个判断块,代码虽然啰嗦,但每一行都对应着一句人话:“如果剩下的钱还够付一张100,就给一张100。”这种一一映射的关系,对学生来说是极大的认知减负。等他们把 if 版本跑通了、理解透了,再去封装成 while 循环,那就是水到渠成的事,甚至你自己不讲,他们也能猜出来。

2.3 教学节奏安排:一节课45分钟的全程拆解

我在实际教学中,把这一节课分成四个阶段,每个阶段都有明确的时间分配和产出物:

  1. 情境导入(5分钟):抛出“假设你是收银员,收到一张100元,需要找零83元,手头有50、20、10、5、1元面额,最少给几张?”这个问题。先让学生凭直觉回答,再追问“你为什么先拿50而不是先拿1元?”引导他们说出“先拿大的”这个朴素策略。

  2. 手动模拟(10分钟):在黑板上或白板上手动走一遍83元的找零过程。50元拿1张,剩余33元;20元拿1张,剩余13元;10元拿1张,剩余3元;1元拿3张,结束。每一步都在旁边标注“当前剩余金额”和“使用了哪个 if 判断”,让学生看到完整的推导链路。

  3. 代码实现(20分钟):让学生跟着敲代码。这一步我会要求他们先写 if 版本,运行通过后,再引导他们重构为循环版本。这两个版本我都会在这篇文章里给出完整代码和解释。

  4. 总结与挑战(10分钟):抛出两个思考题。第一题:把面额换成 [1, 5, 10, 20, 50, 100] 之外的其他组合,贪心还成立吗?第二题:如果新增一张 7 元面额,要找零 14 元,贪心会给出什么答案?这题留到下节课讲动态规划时用。

这个节奏我测试过多次,学生对前三个阶段的反馈都很好,只有最后一个思考题会让不少人卡壳。但卡壳是好事,它制造了认知冲突,为后面的动态规划学习埋下了伏笔。

3. 核心代码实现——先写 if 版本,再讲循环重构

3.1 最简单的 if 版代码:一行判断对应一个动作

下面这段代码是我上课用的第一版,目的是让学生把“算法策略”和“代码结构”一一对应起来。这里我用 Python 写,因为语法最接近自然语言,适合新人上手。

# 金额找零问题——if 版本(教学用) amount = 83 # 要找回的金额 coins = [100, 50, 20, 10, 5, 1] # 从大到小排列的面额列表 result = {} # 用一个字典记录每种面额使用了多少张 # 注意:这里故意不写循环,全部用 if 逐层判断 if amount >= 100: result[100] = amount // 100 amount = amount % 100 if amount >= 50: result[50] = amount // 50 amount = amount % 50 if amount >= 20: result[20] = amount // 20 amount = amount % 20 if amount >= 10: result[10] = amount // 10 amount = amount % 10 if amount >= 5: result[5] = amount // 5 amount = amount % 5 if amount >= 1: result[1] = amount // 1 amount = amount % 1 print("找零方案:", result)

运行结果:

找零方案: {50: 1, 20: 1, 10: 1, 1: 3}

注意这段代码里我用了//整除和%取余,这两个运算符是找零问题的灵魂。amount // 50的意思是“83 里面最多有几个 50”,结果是 1;amount % 50的意思是“拿走这些 50 之后还剩多少”,结果是 33。每一次 if 判断,其实都在回答一个问题:“当前这档面额,我能用几张?”能用几,就取几,剩下零头留给下一档处理。

为什么要强调 if 而不是 while?因为 if 是一次性的判断,判断完就往下走,不回头。这恰好模拟了贪心算法的“单次决策”特性。学生如果一上来就用 while,很容易在循环条件上纠结,反倒忽略了“每次循环其实就是一句 if”的本质。

3.2 重构为循环版本:让学生看到重复代码的“压缩”过程

当学生确认 if 版本的逻辑没有问题后,我会引导他们观察:这六个 if 块,除了面额数字不同,结构完全一样。这种“结构重复”是重构的绝佳信号,也是理解循环抽象的好机会。

# 金额找零问题——循环版本 amount = 83 coins = [100, 50, 20, 10, 5, 1] result = {} for coin in coins: if amount >= coin: result[coin] = amount // coin amount %= coin print("找零方案:", result)

这段代码一行循环,就把上面六个 if 块全部包揽了。引导学生一行行对比,他们会发现:循环版本的每一次迭代,其实就是在执行原来那个“if amount >= coin”的判断。coin这个变量依次取列表里的每个面额,代码的本质没有变,只是把重复的“手工展开”变成了“自动遍历”。

这一步教学的核心,不是让学生记住循环怎么写,而是帮助他们建立“对于重复结构,用循环去压缩”的直觉。这个直觉对后续学习数组遍历、函数封装乃至递归都很有价值。我通常在课堂上让学生自己动手把 if 版本改成循环版本,改完再对比运行结果是否一致,这种“重构验证”体验会让他们的理解更扎实。

3.3 代码讲解的“口播台词”参考:手把手教你怎么说

很多新老师在课堂上讲代码时,容易陷入逐行翻译的陷阱,比如“这一行是定义变量,那一行是打印输出”。这种讲法效果很差,因为学生记住的是语法碎片,而不是逻辑脉络。我建议用“策略式口播”的方式来讲,也就是每一行代码都和学生确认“这一步在落实什么策略”。

以循环版本为例,我的讲法是:

“同学们看这个 for 循环。for 每拿到一个面额,就进入循环体,循环体第一句问:现在剩下的钱够不够这张面额?注意,这个‘够不够’用的是 if 判断。如果够,我就算一算能拿几张,然后把对应的张数记到 result 里,同时把剩余金额更新为拿完之后剩下的零头。如果不够,就直接跳过,去试下一个更小的面额。大家想一下,这样一轮一轮下来,金额是不是一定在减小?减到零的时候,循环就处理完了,方案也就出来了。”

这段口播的重点是:让学生把“amount >= coin”理解成“当前钱还够不够做这个选择”,而不是“一个大于等于的比较表达式”。语言的力量在教学中比很多人想象的要大,换一种说法,学生的理解路径就会完全不一样。

4. 贪心算法的适用边界——什么时候“贪心”会失灵

4.1 一个反例:加入 7 元面额后的找零测试

贪心算法有一个天然的软肋:它只做局部最优决策,从不回头。这意味着它依赖一个前提——每一步的“大额优先”策略,必须保证不会把后续步骤推入死胡同。遗憾的是,这个前提在有些货币体系下并不成立。

我上课时最常用的反例是:假设一个国家发行了 1 元、5 元、7 元、10 元四种面额的硬币,现在要找零 14 元。按贪心思路,优先选 10 元,剩余 4 元,然后选 1 元 × 4,总张数是 5 张。但如果换成 7 元 × 2,只需要 2 张。贪心方案 5 张,最优方案 2 张,差距非常明显。

还有另一个经典反例:面额 [1, 3, 4],找零 6。贪心会先拿 4,剩余 2,然后拿两张 1,一共 3 张。但实际上用 3 元 + 3 元,只需 2 张。这个例子数值更小,口算就能算出来,特别适合课堂板书演示。

4.2 如何判断一个面额体系是否适用贪心:直觉与规律

看到这里你可能会问:那到底什么样的面额体系,贪心算法才能给出最优解?严格来讲,这个问题有数学上的充分条件判断标准,但新手阶段不需要掌握那么深的形式化方法。课堂上我会教学生一个直观判断法:观察大面额是不是小面额的整数倍关系。

比如人民币体系:50 是 20 的 2.5 倍,20 是 10 的 2 倍,10 是 5 的 2 倍,5 是 1 的 5 倍。整套体系虽然不完全成倍数,但整体看,任意大面额能覆盖若干个小面额的组合空间。这种情况下,贪心策略通常不会出错。而像 1、5、7、10 这种,7 和 10 之间没有整除关系,也不存在“7 能被若干个更小面额完全代替”的性质,贪心就容易翻车。

如果学生刨根问底,想知道为什么整数倍关系能保证贪心正确性,我给出的解释是:当小面额能整除大面额时,“换成若干张小面额”不会比“直接用一张大面额”更省张数;因此先拿大面额永远不会亏。一旦这个条件不成立,先拿大面额就可能错过更优解,就需要动态规划登场。

4.3 动态规划法是什么时候才需要的——给新手的差异说明

动态规划法和贪心算法的核心区别在于:贪心只维护一个决策路径,走完即结束,速度快但可能错过全局最优;动态规划会把“所有可能的决策路径”都记录下来,通过状态转移方程逐步推导,保证最终结果是全局最优,代价是时间和空间复杂度更高。

回到找零问题,如果要求“绝对最优”且面额体系不满足贪心条件,就该用动态规划。经典做法是定义dp[i]表示凑出金额 i 需要的最少硬币数,然后用两层循环:外层遍历金额,内层遍历面额列表,更新dp[i] = min(dp[i], dp[i - coin] + 1)

这段内容我在教案里只做科普性引入,不要求新手当堂掌握。我的建议是:先把贪心思路彻底吃透,对动态规划有一个印象就行了。等你面对更复杂的优化问题时,自然会有动力去系统学习动态规划。在入门阶段,贪心算法配合 if 判断,已经能解决掉大部分身边的实际问题。

5. 新鲜教案:课堂活动设计、练习与作业的完整闭环

5.1 课堂互动游戏:把“找零”变成计算思维训练

为了让学生对贪心算法产生“肌肉记忆”,我会在代码实操之外设计一个不碰电脑的互动环节。这个环节耗时短、道具简单,但对理解贪心策略非常有帮助。

游戏规则如下:准备一叠卡片,分别标记 100、50、20、10、5、1 面额,数量不限。随机抽一名学生扮演收银员,老师或另一位学生扮演顾客,由顾客随机说出一个 1 到 100 之间的金额,收银员需要在 10 秒内从卡片堆中挑出能凑出该金额的最小张数组合。计时完毕后,全班一起验证组合是否成立、张数是否最少。

这个游戏的巧妙之处在于:它把“算法执行”从代码层面拉回到了动作层面。学生每次抽卡,内心都在做一次“当前金额还够不够这张大面额”的判断,这与 if 语句的逻辑完全一致。玩过几轮之后,你会发现学生在写代码时明显更流畅,因为他们已经在大脑里“跑”过很多次贪心流程了。

5.2 分层作业设计:基础题、进阶题和挑战题

作业设计我坚持“分层”原则,目的是让不同水平的学生都能在自己的舒适区边缘练习。基础题面向全体学生,进阶题面向中等以上的学生,挑战题则是给学有余力的学生准备的。

基础题:给定 amount = 67,用循环版本代码求找零方案,并对比 if 版本和循环版本的输出是否一致。

进阶题:扩展面额列表,加入 2 元面额,得到 coins = [100, 50, 20, 10, 5, 2, 1],分别测试 amount 为 98、73、36 时的找零方案,并检查贪心策略是否依然合理。

挑战题:设计一组面额和金额,使贪心算法给出非最优解,并用自己的话解释为什么贪心会失败。这道题直接对接下节课的动态规划内容,能写出正确答案的同学,说明真正理解了贪心的边界。

5.3 验收标准:怎么判断学生真的学会了

判断学生是否掌握,不能只看代码能不能跑通。我总结了一套简单的验收标准,供你参考:

  • 能用自己的话解释“为什么先拿大面额”是合理的(理解策略动机)。
  • 能独立写出 if 版本和循环版本的代码(掌握实现)。
  • 能口算出常见金额的最优找零方案(建立数感)。
  • 能说出至少一个贪心算法失效的反例(理解边界)。

如果以上四条全部满足,这节课的核心目标就达成了。不要强求学生记住“贪心算法”这个名词,只要他们能做出这些事,名词迟早会内化。

6. 常见问题与排查技巧——新手写代码最容易踩的坑

6.1 面额列表没有“从大到小”排:最隐蔽的坑

我在课上见过最多的错误,就是把 coins 列表写成[1, 5, 10, 20, 50, 100],也就是从小到大排列。这样一来,代码会优先使用 1 元面额,跑出来的结果虽然也能凑出金额,但张数会多到离谱。比如找零 83 元,如果先从 1 元开始,那 result 里会有一大堆 1,完全看不出贪心策略的影子。

这个坑为什么隐蔽?因为程序不会报错,结果也能凑出正确金额,只有张数不对。新手很难发现问题,还以为自己写对了。我在课堂上会专门让学生做一次对比实验:把 coins 顺序反着写,跑同一个金额,观察结果差异。这个对比能非常直观地让学生理解“面额顺序本质上决定了决策优先级”。

6.2 整除和取余弄混://%的区别总被忽略

很多新手第一次接触取整和取余时,会混淆两者的含义。amount // coin求的是“商”,也就是能拿几张;amount % coin求的是“余数”,也就是拿完剩下的钱。如果你把两个运算符写反了,代码要么报错,要么输出完全不对的结果。

我推荐一个记忆技巧://像一把刀,把数切成整数份;%像一个漏勺,只留下漏下去的零头。上课时我会让学生分别打印83 // 2083 % 20,让他们亲眼看到一个是 4、一个是 3,比口头解释一百遍都管用。

6.3 金额最终不为零:遗漏了最小面额 1

另一种常见错误是:面额列表里写了各种大面额,唯独漏掉了 1 元。如果 amount 最后剩余一个无法处理的零头,代码就“卡住”了——不是报错,而是静默地少了几个硬币。这种情况下,调试技巧是:在代码运行结束后打印一下 amount,看它是否归零。只要金额不是零,就说明还有没覆盖到的面额。

这个问题的排查思路其实可以用到更多场景:任何“处理完数据后检查状态是否符合预期”的习惯,都是新手应该尽早养成的。我会建议学生在找零问题里显式添加一行print(amount)来确认结果,这个习惯以后排查复杂 bug 时会非常有用。

6.4 找零方案的记录方式:字典、列表还是直接打印

有些学生喜欢在 if 判断里直接print,这样做的问题在于“每次输出的结果是即时性的,不容易复核”。我建议把找零方案记录到一个字典里,最后统一打印。这样既能清晰地看到每种面额用了几张,也方便后续写测试代码来验证结果的正确性。

顺带提一句,如果用列表存储方案,也可以写成[50, 20, 10, 1, 1, 1]这种形式,每条记录就是一张真实选中的硬币面额。字典的好处是紧凑,列表的好处是贴近实际找零的“展开过程”,两种风格各有千秋,你选一种顺手的就行。

7. 教案的延伸价值——不只是找零,还能继续玩出什么花样

7.1 从固定面额到动态面额:体会“抽象”的力量

基础教案做完后,我喜欢额外布置一个开放性的小任务:把代码改造成“由用户输入面额列表和金额”的版本。也就是说,不把 coins 写死在代码里,而是用input()接收用户输入。

这是一个很好的抽象能力训练。学生会发现,只要保证面额列表从大到小,代码本身几乎不用改,就能适用于任意货币体系。这个体验对新手理解“函数”“参数”“输入输出”都有帮助。代码大致长这样:

# 动态面额版本 amount = int(input("请输入需要找零的金额:")) coins_input = input("请输入面额列表,用逗号分隔(必须从大到小):") coins = [int(x) for x in coins_input.split(",")] result = {} for coin in coins: if amount >= coin: result[coin] = amount // coin amount %= coin print("找零方案:", result) print("剩余未找零金额:", amount)

我特意在最后加了一行打印剩余金额,方便学生检查是否所有金额都被成功兑换。这行小小的打印语句,其实就是“程序自检”思维的雏形,对后续学习非常有价值。

7.2 与现实的连接:收银台场景、零钱分配和日常生活

有些学生会问:现在都用手机支付了,找零问题还有什么实际意义?这个问题问得好。我通常会回应两点:第一,移动支付普及不意味着现金场景消失,自动售货机、投币洗衣机、停车场缴费机仍然大量存在;第二,找零问题在本质上是一个“资源分配”问题,不止硬币可以用,纸钞、票券、时间片、带宽分配,全都可以套用同样的策略框架。

现实中最贴近的应用是:自动售货机设计硬币找零模块时,需要在“硬件可找零的硬币种类有限”和“顾客等待时间要短”之间做权衡。这和课程里“怎么样用最少张数找零”的目标有天壤之别,但底层逻辑依然是“在有限资源下做决策”。这种迁移视角,能让新手看到算法的通用性,而不仅仅把它当成一道考试题。

7.3 下一步学习路径:从贪心走向动态规划

如果学生做完了找零问题,并且对“最优方案”产生了兴趣,那下一站动态规划就是顺理成章的事。找零问题恰好是动态规划教材里的经典案例,和 Fibonacci 数列、背包问题并列。

我建议的后续学习路径是:先用找零问题搞懂“递归+记忆化搜索”的写法,再做“自底向上的递推写法”,最后把这个流程迁移到背包问题上。这样一路学下来,算法思维会非常扎实。这里给你一个动态规划的参考实现,供学有余力的学生课后钻研:

# 金额找零问题——动态规划版本(求最少硬币数) def min_coins_dp(coins, amount): dp = [float("inf")] * (amount + 1) dp[0] = 0 for i in range(1, amount + 1): for coin in coins: if i - coin >= 0: dp[i] = min(dp[i], dp[i - coin] + 1) return dp[amount] if dp[amount] != float("inf") else -1 coins = [1, 5, 7, 10] print(min_coins_dp(coins, 14)) # 输出 2,对应 7 + 7

这段代码和贪心版本最大的不同是:它不再“先选最大面额”,而是对所有可能的“最后一枚硬币”做比较,取最小。学生如果能亲手跑通这段代码,再回去看贪心版本,就能体会两种算法的哲学差异,这种对照学习的效果远比单学任何一个要好。

8. 我的教学复盘与个人经验

最后聊点我自己的体会。这节“贪心算法 if 解题”的教案,我前后调整过三四轮。第一轮的时候,我直接把循环版本甩给学生,结果代码跑通了,但让他们解释“为什么这么写”时,几乎没有学生能说明白。后来我改成“先 if 后 while”的两步走,理解率明显上来了,这也验证了我一直坚持的教学信念:新手学算法,顺序比速度重要。

找零问题最大的魅力在于:它足够简单,简单到可以用 if 一行一行写出来;但它又足够深刻,深刻到能牵出贪心算法的全部核心思想,以及和动态规划的第一次相遇。我建议每一位带新人的老师或者自学的新手,都认真走一遍“if 版 → 循环版 → 反例思考”的完整流程,不要跳过任何一个阶段。

如果你在实际授课或学习过程中遇到具体问题,比如代码跑不通、反例设计不出来、或者在重构环节学生卡壳,欢迎在评论区留言,我看到都会回复。毕竟这套教案没有完美版本,只有一次次和学生真实互动之后不断打磨出来的版本。教与学本来就是一场双向优化,希望这份教案能让你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:57:12

Java基础进阶:接口、内部类与常用API的底层原理与面试要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:55:01

5分钟上手Semgrep:面向新手的免费静态代码分析完整指南

5分钟上手Semgrep:面向新手的免费静态代码分析完整指南 【免费下载链接】semgrep Lightweight static analysis for many languages. Find bug variants with patterns that look like source code. 项目地址: https://gitcode.com/GitHub_Trending/se/semgrep …

作者头像 李华
网站建设 2026/9/10 6:53:54

AI全栈开发实战:从Demo到生产级应用的架构与稳定性设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:50:09

大模型为何越强越难管?从对齐失效到本地可控实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华