最近做了 Ramp 的 OA,题目跑在 CodeSignal 上,90 分钟四道题,分成四个 Level 逐级解锁。它不是那种一眼看穿套路的算法谜题,而是一道层层加码的系统设计实现题:你要把一个银行系统从最简单的存取转账,一步步扩展成支持排行、定时付款和取消的完整服务。这篇复盘记录整个流程、每个 Level 的要求和我的解法思路。如果你也在准备类似的 OA,希望这份笔记能帮你把节奏和数据模型提前想清楚。需要 OA辅助 或 OA代面的同学,也可以直接看文末的联系方式。
OA 环境与形式概览
值得先说清楚的一点:这套 90 分钟的 CodeSignal 银行系统格式取自一个共享题库,同样的风格在 Meta、Coinbase、TradeDesk 的 OA 里都出现过,目前市面上主要流传着三个大背景(场景),银行系统只是其中之一。所以提前熟悉这套「逐级构建」的出题方式,比临场硬啃更划算。
| 项目 | 详情 |
|---|---|
| 平台 | CodeSignal |
| 总时长 | 90 分钟 |
| 题量 | 4 道,对应 4 个 Level |
| 形式 | 逐级解锁,必须当前 Level 全部通过才能进入下一级 |
| 考点 | 数据结构建模、增量扩展、按时间戳有序处理、堆与哈希表 |
最关键的规则:你必须把当前 Level 全部测试点跑对,下一个 Level 才会解锁。所以第一级一定要稳,别为了赶进度留下隐藏 bug——一旦某个 Level 卡住,后面的分数就全部锁死了。另外,每个操作都带一个唯一且严格递增的时间戳,这是后面几个 Level 排序和调度的基础。
Level 1:简易银行系统
题目背景
系统初始没有任何账户。需要支持三个操作:
createAccount(timestamp, accountId):账户不存在则创建并返回True;已存在返回False。deposit(timestamp, accountId, amount):存款并返回更新后的余额;账户不存在返回空(None)。transfer(timestamp, sourceId, targetId, amount):在两个不同账户之间转账,成功返回转出方的剩余余额;若任一账户不存在、两个账户相同、或转出方余额不足,返回空(None)。
思路
用一个 HashMap 存 accountId → 余额 即可,三个操作全是 O(1)。这一级时间戳只用来给操作排序,不影响逻辑。
Python 解法
from typing import Optional
class BankingSystem:
def __init__(self) -> None:
# accountId -> 余额
self.accounts: dict[str, int] = {}
def create_account(self, timestamp: int, account_id: str) -> bool:
"""新建账户;已存在则返回 False。"""
if account_id in self.accounts:
return False
self.accounts[account_id] = 0
return True
def deposit(self, timestamp: int, account_id: str,
amount: int) -> Optional[int]:
"""存款并返回最新余额;账户不存在返回 None。"""
if account_id not in self.accounts:
return None
self.accounts[account_id] += amount
return self.accounts[account_id]
def transfer(self, timestamp: int, source_id: str, target_id: str,
amount: int) -> Optional[int]:
"""转账成功返回转出方剩余余额;否则返回 None。"""
if source_id not in self.accounts or target_id not in self.accounts:
return None
if source_id == target_id: # 不能转给自己
return None
if self.accounts[source_id] < amount: # 余额不足
return None
self.accounts[source_id] -= amount
self.accounts[target_id] += amount
return self.accounts[source_id]
时间复杂度:createAccount / deposit / transfer 均为 O(1)。 空间复杂度:O(k),k 为账户数量。
Level 2:Top Spenders 排行
题目背景
新增 topSpenders(timestamp, n),返回累计转出金额最高的前 n 个账户。转出金额包括成功的转出转账,以及后面 Level 里成功执行的定时付款 / 取款。按转出金额降序排列,金额相同则按 accountId 升序,格式化为 "accountId(totalOutgoing)";若账户总数不足 n,则返回全部。
思路
给每个账户维护一个 outgoing 字段(创建时初始化为 0),只在转出方成功转账时累加它——存款和收到的转入都不计入。topSpenders 遍历所有账户排序即可。
Python 解法
class BankingSystem:
def __init__(self) -> None:
self.accounts: dict[str, int] = {}
self.outgoing: dict[str, int] = {} # accountId -> 累计转出
def create_account(self, timestamp: int, account_id: str) -> bool:
if account_id in self.accounts:
return False
self.accounts[account_id] = 0
self.outgoing[account_id] = 0 # 转出额初始化为 0
return True
def transfer(self, timestamp: int, source_id: str, target_id: str,
amount: int) -> Optional[int]:
if source_id not in self.accounts or target_id not in self.accounts:
return None
if source_id == target_id or self.accounts[source_id] < amount:
return None
self.accounts[source_id] -= amount
self.accounts[target_id] += amount
self.outgoing[source_id] += amount # 只累加转出方
return self.accounts[source_id]
def top_spenders(self, timestamp: int, n: int) -> list[str]:
"""返回累计转出最高的前 n 个账户。"""
ranked = sorted(
self.accounts.keys(),
key=lambda acc: (-self.outgoing[acc], acc), # 转出降序,id 升序
)
return [f"{acc}({self.outgoing[acc]})" for acc in ranked[:n]]
时间复杂度:topSpenders 单次查询 O(m log m),m 为账户数;其余操作仍是 O(1)。 空间复杂度:O(m)。
Level 3:定时付款与取消
题目背景
新增两个操作:
schedulePayment(timestamp, accountId, amount, delay):创建一笔在timestamp + delay执行的付款,返回一个全局递增的 paymentId;账户不存在返回空(None)。执行时若余额不足则跳过这笔付款。成功执行的付款计入转出金额。cancelPayment(timestamp, accountId, paymentId):只能取消一笔尚未执行、尚未被取消、且属于该账户的付款;否则返回False。
两个关键时序规则:到期的定时付款必须先于该时间戳上的任何其他操作执行;多笔在同一时刻到期的,按创建顺序执行。
思路
用一个以 (executionTime, creationOrder) 为键的最小堆,配合一个 paymentId → 详情/状态 的 HashMap。在执行任何公开操作之前,先处理所有 executionTime <= 当前时间戳 的付款:跳过已取消的;余额够就扣款并累加转出,不够就标记失败。创建付款时生成递增 ID 并同时写入堆和 map;取消时按 ID 定位,校验归属和状态。
Python 解法
import heapq
class BankingSystem:
def __init__(self) -> None:
self.accounts: dict[str, int] = {}
self.outgoing: dict[str, int] = {}
self.pay_heap: list[tuple[int, int, str]] = [] # (执行时间, 顺序, id)
self.payments: dict[str, dict] = {} # paymentId -> 详情
self.payment_counter = 0 # 全局递增 paymentId
self.order_counter = 0 # 同刻到期的创建顺序
def _process_due(self, timestamp: int) -> None:
"""执行所有 executionTime <= timestamp 的到期付款。"""
while self.pay_heap and self.pay_heap[0][0] <= timestamp:
_, _, payment_id = heapq.heappop(self.pay_heap)
info = self.payments[payment_id]
if info["status"] != "pending":
continue # 已取消,跳过
acc, amount = info["account_id"], info["amount"]
if self.accounts.get(acc, 0) >= amount:
self.accounts[acc] -= amount
self.outgoing[acc] += amount # 成功付款计入转出
info["status"] = "done"
else:
info["status"] = "failed" # 余额不足,跳过
def schedule_payment(self, timestamp: int, account_id: str,
amount: int, delay: int) -> Optional[str]:
self._process_due(timestamp)
if account_id not in self.accounts:
return None
self.payment_counter += 1
payment_id = f"payment{self.payment_counter}"
self.order_counter += 1
self.payments[payment_id] = {
"account_id": account_id,
"amount": amount,
"status": "pending",
}
heapq.heappush(
self.pay_heap,
(timestamp + delay, self.order_counter, payment_id),
)
return payment_id
def cancel_payment(self, timestamp: int, account_id: str,
payment_id: str) -> bool:
self._process_due(timestamp)
info = self.payments.get(payment_id)
if info is None or info["account_id"] != account_id:
return False
if info["status"] != "pending": # 已执行或已取消
return False
info["status"] = "cancelled"
return True
时间复杂度:堆的插入 / 弹出 O(log n),n 为待执行付款数;取消按 ID 查表约 O(1)。每个公开操作前的 _process_due 总体摊还 O(log n)。
空间复杂度:O(n),用于堆和付款表。
Level 4:常见变体与应对策略
我这次抽到的 OA,Level 4 只给了标题就没能细看题面,所以这里不编造具体题目和代码,只诚实地说说方向。这套 CodeSignal 银行系统的 Level 4 通常是在前三级基础上继续扩展,常见的 Level 4 变体有:余额历史查询(回溯某账户在某时间戳的余额)、带过期规则的商户返现(cashback),以及账户合并(把一个账户的余额、转出记录和待执行付款并入另一个)。
应对未知的 Level 4,通用策略是:复用 Level 1-3 的数据模型(账户表、转出字段、付款堆),保持「按时间戳有序处理、公开操作前先结算到期事件」这条主线,然后仔细读题把新规则接到现有结构上。只要前三级的抽象搭得干净,第四级往往是自然延伸而非推倒重来。
备考建议
因为这套题来自共享题库,提前吃透题型比刷海量新题更有效:
- 稳住 Level 1:数据模型定好,后面三级都在它上面加字段。一开始就把账户抽象成「余额 + 转出 + 相关付款」会让后续扩展顺很多。
- 时间戳是主线:从 Level 3 起,任何公开操作前都要先结算到期付款,别把这步漏在某个入口。
- 增量而非重写:每上一级只加最小改动,保留已通过的逻辑,避免破坏前面的测试点。
- 熟悉三个大背景:银行系统只是其中之一,把另外两个场景的套路也过一遍,临场就能快速对号入座。
FAQ
Q1:Ramp 的 OA 一定要每个 Level 全对才能继续吗?
是的。CodeSignal 这套格式是逐级解锁,当前 Level 的全部测试点跑对后下一级才开放。所以宁可在低级别多花几分钟确认边界,也别留隐藏 bug 把后面的分数锁死。
Q2:90 分钟四道题,时间够用吗?
够,但前提是数据模型一开始就搭对。四道题是同一个系统的层层扩展,如果 Level 1 的抽象合理,后面基本是加字段和加方法;反之若前期结构混乱,到 Level 3 的堆和调度就会很吃力。
Q3:topSpenders 里的转出金额到底算哪些?
只算转出方的成功转账,加上后面成功执行的定时付款 / 取款。存款和收到的转入都不计入。相同金额按 accountId 升序排列,输出格式是 accountId(totalOutgoing)。
Q4:定时付款的执行时序为什么这么重要?
因为到期付款必须先于同一时间戳上的其他操作执行,多笔同刻到期的还要按创建顺序处理。用 (executionTime, creationOrder) 的最小堆天然满足这个顺序,并在每个公开操作入口先调用一次结算,就不会漏。
Q5:这套银行系统题在别的公司也会遇到吗?
会。这套 90 分钟 CodeSignal 格式取自共享题库,Meta、Coinbase、TradeDesk 都出现过类似风格,目前主要流传三个大背景。把出题套路提前过一遍,跨公司都能复用。
正在准备 Ramp 或其他公司的 CodeSignal OA? 我们熟悉这套 90 分钟逐级解锁的银行系统格式,从数据建模到定时付款调度都能帮你梳理清楚,提供全流程的 OA辅助 与 OA代面服务。
立即添加微信 Coding0201,获取一对一定制备考方案。
联系方式
- 微信:Coding0201
- Email:[email protected]
- Telegram:@OAVOProxy