最近做了 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