1. 问题背景与需求分析
今天想和大家分享一道非常经典的贪心算法题目——LeetCode 860题"柠檬水找零"。这道题看似简单,但实际编码时会遇到不少细节问题。我在AC这道题时耗时100ms,排名前10%,下面就把我的解题思路和优化过程完整分享给大家。
题目场景设定非常生活化:你开了一家柠檬水摊,每杯柠檬水售价5美元。顾客排队购买,每次只买一杯,并可能用5美元、10美元或20美元支付。你需要正确找零(注意一开始你手头没有零钱)。如果能给每位顾客正确找零则返回true,否则返回false。
2. 解题思路与算法选择
2.1 问题抽象化
首先我们需要将实际问题抽象为算法问题:
- 输入:一个整数数组bills,表示顾客的支付顺序
- 输出:布尔值,表示是否能正确找零
关键点在于:
- 5美元不需要找零
- 10美元需要找一张5美元
- 20美元优先找10+5的组合(贪心思想),其次考虑5*3
2.2 贪心算法适用性分析
为什么选择贪心算法?因为:
- 局部最优:每次找零时尽可能保留更多5美元(因为5美元更灵活)
- 全局最优:所有局部最优解能保证全局最优
- 无后效性:当前决策不影响后续决策
3. 代码实现与优化
3.1 基础版本实现
python复制def lemonadeChange(bills):
five = ten = 0
for bill in bills:
if bill == 5:
five += 1
elif bill == 10:
if not five:
return False
five -= 1
ten += 1
else: # 20
if ten and five:
ten -= 1
five -= 1
elif five >= 3:
five -= 3
else:
return False
return True
3.2 优化思路
- 提前终止:一旦发现无法找零立即返回,避免无效计算
- 变量简化:仅需记录5和10的数量,20不需要记录(因为不会用于找零)
- 条件判断顺序:优先处理20美元的情况,减少分支判断
3.3 优化后版本(100ms)
python复制def lemonadeChange(bills):
five = ten = 0
for bill in bills:
if bill == 5:
five += 1
elif bill == 10:
five -= 1
ten += 1
elif ten > 0: # 优先使用10+5
ten -= 1
five -= 1
else: # 只能用5*3
five -= 3
if five < 0: # 统一检查
return False
return True
4. 复杂度分析与边界条件
4.1 时间复杂度
- 时间复杂度:O(n),只需遍历一次数组
- 空间复杂度:O(1),只用了两个额外变量
4.2 边界条件测试
必须测试的特殊情况:
- 第一个顾客给20美元
- 连续多个10美元支付
- 大量5美元后突然来20美元
- 空输入数组
- 极端情况:[5,5,5,10,20]
5. 常见错误与调试技巧
5.1 典型错误模式
- 忘记处理20美元时的两种找零方式
- 没有及时检查five是否为负
- 错误计算找零组合(如用5*3而不用10+5)
5.2 调试建议
- 打印每次交易后的five和ten值
- 对特殊测试用例单步调试
- 使用assert检查中间状态
6. 算法扩展思考
虽然这道题用贪心算法最优,但可以思考:
- 如果面额更多(如加入50美元)如何处理?
- 如果要求找零张数最少如何修改?
- 动态规划解法是否可行?(虽然不必要但可以练习)
提示:在实际面试中,可能会被要求证明贪心选择的正确性。关键点在于20美元找零时,优先使用10+5比5*3能保留更多5美元,为后续找零提供更大灵活性。
7. 个人优化心得
在实现过程中,我发现几个优化点:
- 将条件判断合并后代码更简洁
- 统一在最后检查five<0比在每个分支检查更高效
- 提前终止确实能提升运行速度(实测从120ms降到100ms)
对于这类问题,我的经验是:
- 先理清所有可能的支付场景
- 明确找零的优先级(贪心策略)
- 用简单变量代替复杂数据结构
- 边界条件要单独测试