← 返回部落格列表 Ramp OA 復盤:CodeSignal 銀行系統四個 Level
Ramp

Ramp OA 復盤:CodeSignal 銀行系統四個 Level

2026-08-08

最近做了 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:簡易銀行系統

題目背景

系統初始沒有任何帳戶。需要支援三個操作:

思路

用一個 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:定時付款與取消

題目背景

新增兩個操作:

兩個關鍵時序規則:到期的定時付款必須先於該時間戳上的任何其他操作執行;多筆在同一時刻到期的,按建立順序執行。

思路

用一個以 (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 的資料模型(帳戶表、轉出欄位、付款堆積),保持「按時間戳有序處理、公開操作前先結算到期事件」這條主線,然後仔細讀題把新規則接到現有結構上。只要前三級的抽象搭得乾淨,第四級往往是自然延伸而非推倒重來。


備考建議

因為這套題來自共享題庫,提前吃透題型比刷海量新題更有效


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獲取一對一定制備考方案

聯絡方式