设计一个 ATM 机器(LeetCode 第2241题)
在本篇博客中,我们将深入探讨如何设计一个 ATM 机器,以满足存取钞票的需求。这个问题来源于 LeetCode 的第2241题,旨在考察如何高效地管理不同面额的钞票并处理用户的存取请求。
问题描述
背景
设计一个 ATM 机器,该机器存有 5 种面值的钞票:$20、$50、$100、$200 和 $500 美元。初始时,ATM 机是空的。用户可以通过该机器进行存款或取款操作。
取款规则
- 优先取出大面额钞票:在取款时,ATM 会优先取出较大面额的钞票,以减少取款的张数。
- 精确取款:如果无法通过可用的钞票组合精确取出所需金额,则取款请求将被拒绝。
具体要求
请实现一个 ATM
类,支持以下操作:
ATM()
: 初始化 ATM 对象。deposit(int[] banknotesCount)
: 存入 5 种面额的钞票数量,依次为 $20、$50、$100、$200 和 $500。withdraw(int amount)
: 尝试取出指定金额的钞票,返回一个长度为 5 的数组,表示每种面额钞票的数量。如果无法完成取款,则返回[-1]
。
示例
输入:
["ATM", "deposit", "withdraw", "deposit", "withdraw", "withdraw"]
[[], [[0,0,1,2,1]], [600], [[0,1,0,1,1]], [600], [550]]
输出:
[null, null, [0,0,1,0,1], null, [-1], [0,1,0,0,1]]
解释:
ATM atm = new ATM();
atm.deposit([0,0,1,2,1]); // 存入 1 张 $100,2 张 $200 和 1 张 $500 的钞票。
atm.withdraw(600); // 返回 [0,0,1,0,1]。机器取出 1 张 $100 和 1 张 $500 的钞票。
atm.deposit([0,1,0,1,1]); // 存入 1 张 $50,1 张 $200 和 1 张 $500 的钞票。
atm.withdraw(600); // 返回 [-1]。无法精确取出 600 美元。
atm.withdraw(550); // 返回 [0,1,0,0,1],取出 1 张 $50 和 1 张 $500 的钞票。
解决方案
数据结构选择
为了高效管理钞票,我们使用一个数组来表示不同面额钞票的数量。具体来说:
- 定义钞票面额数组
DENOMINATIONS = [20, 50, 100, 200, 500]
。 - 使用数组
banknotes
来存储每种面额的钞票数量,索引与DENOMINATIONS
对应。
操作实现
存款操作 (deposit
)
存款操作相对简单,只需将输入数组中的钞票数量累加到 banknotes
对应的位置。
取款操作 (withdraw
)
取款操作需要考虑以下几点:
- 优先取出大面额钞票:从高到低遍历
DENOMINATIONS
,尽可能多地取出当前面额的钞票。 - 确保取款金额精确:在取出钞票的过程中,需实时更新剩余金额,并确保最终能精确取出所需金额。
- 回滚机制:如果在取款过程中发现无法精确取出所需金额,应回滚所有取出的钞票,确保 ATM 状态不变。
代码实现
以下是基于上述思路的 Python 实现:
from typing import List# 定义钞票面额
DENOMINATIONS = [20, 50, 100, 200, 500]
KINDS = len(DENOMINATIONS)class ATM:def __init__(self):# 初始化每种面额的钞票数量为0self.banknotes = [0] * KINDSdef deposit(self, banknotesCount: List[int]) -> None:"""存入钞票,banknotesCount按面额顺序依次对应20, 50, 100, 200, 500"""for i, count in enumerate(banknotesCount):self.banknotes[i] += countprint(f"存款后钞票状态: {self.banknotes}")def withdraw(self, amount: int) -> List[int]:"""尝试取出指定金额的钞票,返回取出各面额的数量数组或[-1]表示失败"""ans = [0] * KINDSremaining = amount# 从大面额开始取for i in range(KINDS - 1, -1, -1):if remaining >= DENOMINATIONS[i]:# 计算当前面额最多可取出的钞票数量max_can_take = remaining // DENOMINATIONS[i]take = min(max_can_take, self.banknotes[i])ans[i] = takeremaining -= take * DENOMINATIONS[i]print(f"取出 {DENOMINATIONS[i]} 美元: {ans[i]} 张, 剩余金额: {remaining}")# 如果无法精确取出所需金额,返回 [-1]if remaining != 0:print(f"取款失败,无法取出 {amount} 美元")return [-1]# 更新钞票数量for i in range(KINDS):self.banknotes[i] -= ans[i]print(f"取款后钞票状态: {self.banknotes}")return ans
代码解析
-
初始化:
def __init__(self):self.banknotes = [0] * KINDS
初始化时,ATM 中所有面额的钞票数量均为0。
-
存款操作:
def deposit(self, banknotesCount: List[int]) -> None:for i, count in enumerate(banknotesCount):self.banknotes[i] += countprint(f"存款后钞票状态: {self.banknotes}")
遍历输入的
banknotesCount
数组,将每种面额的钞票数量累加到self.banknotes
中。 -
取款操作:
def withdraw(self, amount: int) -> List[int]:ans = [0] * KINDSremaining = amountfor i in range(KINDS - 1, -1, -1):if remaining >= DENOMINATIONS[i]:max_can_take = remaining // DENOMINATIONS[i]take = min(max_can_take, self.banknotes[i])ans[i] = takeremaining -= take * DENOMINATIONS[i]print(f"取出 {DENOMINATIONS[i]} 美元: {ans[i]} 张, 剩余金额: {remaining}")if remaining != 0:print(f"取款失败,无法取出 {amount} 美元")return [-1]for i in range(KINDS):self.banknotes[i] -= ans[i]print(f"取款后钞票状态: {self.banknotes}")return ans
- 初始化:
ans
用于记录每种面额取出的钞票数量,remaining
用于跟踪剩余金额。 - 取钞逻辑:从大到小遍历面额,尽可能多地取出当前面额的钞票,同时更新剩余金额。
- 失败检查:如果在遍历结束后,
remaining
不为0,表示无法精确取出所需金额,返回[-1]
。 - 更新钞票数量:如果取款成功,更新
self.banknotes
中的钞票数量。 - 返回结果:返回
ans
数组,表示取出的各面额钞票数量。
- 初始化:
运行示例
以下是一个运行示例,展示了如何使用 ATM
类:
# 初始化ATM
atm = ATM()# 存入1张$100,2张$200,1张$500
atm.deposit([0, 0, 1, 2, 1]) # 存款后钞票状态: [0, 0, 1, 2, 1]# 取出600美元
print(atm.withdraw(600)) # 输出: [0,0,1,0,1], 取款后钞票状态: [0,0,0,2,0]# 存入1张$50,1张$200,1张$500
atm.deposit([0, 1, 0, 1, 1]) # 存款后钞票状态: [0,1,0,3,1]# 尝试取出600美元
print(atm.withdraw(600)) # 输出: [-1], 取款失败,无法取出600美元# 取出550美元
print(atm.withdraw(550)) # 输出: [0,1,0,0,1], 取款后钞票状态: [0,0,0,3,0]
输出结果
存款后钞票状态: [0, 0, 1, 2, 1]
取出 500 美元: 1 张, 剩余金额: 100
取出 200 美元: 0 张, 剩余金额: 100
取出 100 美元: 1 张, 剩余金额: 0
取出 50 美元: 0 张, 剩余金额: 0
取出 20 美元: 0 张, 剩余金额: 0
取款后钞票状态: [0, 0, 0, 2, 0]
[0, 0, 1, 0, 1]
存款后钞票状态: [0, 1, 0, 3, 1]
取出 500 美元: 1 张, 剩余金额: 100
取出 200 美元: 0 张, 剩余金额: 100
取出 100 美元: 0 张, 剩余金额: 100
取出 50 美元: 0 张, 剩余金额: 100
取出 20 美元: 0 张, 剩余金额: 100
取款失败,无法取出 600 美元
[-1]
取出 500 美元: 1 张, 剩余金额: 50
取出 200 美元: 0 张, 剩余金额: 50
取出 100 美元: 0 张, 剩余金额: 50
取出 50 美元: 1 张, 剩余金额: 0
取出 20 美元: 0 张, 剩余金额: 0
取款后钞票状态: [0, 0, 0, 3, 0]
[0, 1, 0, 0, 1]
复杂度分析
-
时间复杂度:
deposit
操作的时间复杂度为 O(1),因为面额种类固定为5。withdraw
操作的时间复杂度同样为 O(1)。
-
空间复杂度:
- 使用了固定大小的数组
banknotes
和ans
,空间复杂度为 O(1)。
- 使用了固定大小的数组
总结
通过使用贪心算法,我们能够有效地管理 ATM 机器中的钞票,并满足用户的存取需求。然而,需要注意的是,贪心算法在某些情况下可能无法满足取款金额的精确要求。因此,在实现时,我们需要在取款后进行剩余金额的检查,以确保取款的合法性和准确性。