前段時間投了 TikTok 的後端職缺,履歷過篩之後收到一封 CodeSignal 的測評邀請。這篇複盤記錄我在這場 OA 裡碰到的四道題、平台形式,以及每一題的完整解法。需要提一句:CodeSignal 最近把 General Coding Assessment 的介面刷新了一版,也輪換了一部分題目,但共享題庫裡的老題仍然會反覆出現——我這次四道裡就有三道是能在往期討論裡對上號的。如果你也在準備 TikTok 的 OA,或者想找 OA輔助 / OA代面 把節奏和思路一起打磨到位,希望這份筆記能幫到你。
面試環境與形式概覽
整場測評在 CodeSignal General Coding Assessment(共享題庫)上完成,全程限時 70 分鐘,一共四道題,難度由前往後遞進:前兩道是熱身性質的 Easy,第三題是矩陣/陣列模擬,第四題是偏資料結構的壓軸題。CodeSignal 會全程錄影、監控切換視窗,所以中途盡量別離開作答頁。
| 項目 | 詳情 |
|---|---|
| 平台 | CodeSignal General Coding Assessment(共享題庫) |
| 時長 | 70 分鐘 |
| 題量 | 4 道 |
| 難度 | 遞進(Easy → 模擬 → 資料結構) |
| 考察重點 | 基礎實作速度、邊界處理、模擬與查詢效率 |
節奏建議:前兩道 Easy 盡量在 20 分鐘內拿下,把時間留給第三、四題。CodeSignal 的題目是要真正通過全部測試用例的,所以寫完記得自己先在腦子裡 dry run 幾個邊界例子再提交。
Problem 1:數字積減和
題目背景
給定一個正整數 n,計算它十進位各位數字的乘積與各位數字之和,回傳「乘積 − 和」。例如 123456:乘積是 1·2·3·4·5·6 = 720,和是 1+2+3+4+5+6 = 21,結果 720 − 21 = 699。再看 1010:因為包含數字 0,乘積直接變成 0,和是 0+1+0+1 = 2,結果 0 − 2 = -2。
思路
把整數轉成字串再逐位轉回整數:list(map(int, str(n)))。一次走訪同時累乘和累加即可。乘積的初值設為 1,只要遇到任意一位是 0,乘積自然就變成 0,不需要單獨判斷。
Python 解法
def digit_product_minus_sum(n: int) -> int:
"""回傳 n 各位數字的乘積減去各位數字之和。
只要有任意一位是 0,乘積會自然變為 0,無需特判。
"""
digits = list(map(int, str(n))) # 拆出每一位
product = 1
total = 0
for d in digits:
product *= d # 累乘,遇到 0 會自然歸零
total += d # 累加
return product - total
Dry run 演示
用 n = 123456 走一遍:
| 步驟 | 當前位 | product | total | 說明 |
|---|---|---|---|---|
| 初始 | — | 1 | 0 | product 初值為 1 |
| 讀 1 | 1 | 1 | 1 | |
| 讀 2 | 2 | 2 | 3 | |
| 讀 3 | 3 | 6 | 6 | |
| 讀 4 | 4 | 24 | 10 | |
| 讀 5 | 5 | 120 | 15 | |
| 讀 6 | 6 | 720 | 21 |
最終 720 − 21 = 699。
再驗證含 0 的邊界 n = 1010:走訪到第二位 0 時 product 歸零,之後無論怎麼乘都還是 0,最終 total = 2,回傳 0 − 2 = -2,符合預期。
時間複雜度:O(log n),位數與 n 的對數成正比。
空間複雜度:O(log n),存放各位數字。
Problem 2:最長連續字元
題目背景
給定一個只含小寫字母的字串,找出被單個字元連續重複的最長一段;如果有多段並列最長,取最右邊的那一段;回傳「字元 + 長度」的拼接,例如連續三個 c 回傳 "c3"。
思路
線性掃描,維護當前段的字元 cur_char 和長度 cur_len:遇到和上一位相同就 cur_len += 1,不同就把 cur_len 重設為 1 並更新 cur_char。同時用 best_len / best_char 記錄全域最佳。並列時取最右的關鍵是比較用 >=:只要出現同樣長的段,就用後來的覆蓋前面的。
Python 解法
def longest_run(s: str) -> str:
"""回傳最長連續重複字元段,並列時取最右邊一段。
用 >= 保證相同長度時後出現的段覆蓋先出現的段。
"""
if not s:
return "" # 空字串直接回傳空
cur_char = s[0]
cur_len = 1
best_char = s[0]
best_len = 1
for ch in s[1:]:
if ch == cur_char:
cur_len += 1 # 延續當前段
else:
cur_char = ch # 開啟新段
cur_len = 1
if cur_len >= best_len: # >= 讓最右的並列段勝出
best_len = cur_len
best_char = cur_char
return f"{best_char}{best_len}"
Dry run 演示
用 s = "aabbbxxccc"(bbb 與 ccc 長度並列為 3)走一遍關鍵節點:
| 位置 | 字元 | cur_char / cur_len | best_char / best_len | 說明 |
|---|---|---|---|---|
| 0 | a | a / 1 | a / 1 | 初始 |
| 1 | a | a / 2 | a / 2 | 延續 |
| 4 | b | b / 3 | b / 3 | 第一段最長 3 |
| 7 | c | c / 1 | b / 3 | 新開 c 段 |
| 9 | c | c / 3 | c / 3 | 並列 3,>= 覆蓋為 c |
最終回傳 "c3":bbb 與 ccc 都是長度 3,取最右的 ccc。
時間複雜度:O(n),單次走訪。 空間複雜度:O(1),只用了常數個變數。
Problem 3:記憶體配置模擬
題目背景
模擬一段記憶體的配置與釋放。給定一系列操作:
alloc x:在記憶體裡找到最左邊一段連續x個空閒單元並佔用它,回傳這段的起始索引,同時給這次配置一個遞增的配置 ID;如果找不到就回傳-1。erase ID:釋放此前用該 ID 配置出去的整塊記憶體,回傳釋放的長度;如果該 ID 不存在或已經被釋放,回傳-1。
思路
用一個布林陣列表示每個單元是否被佔用。alloc 時從左到右掃描,統計當前連續空閒長度,遇到被佔用的單元就把計數歸零;一旦計數達到 x,就把這段區間標記為佔用,並在雜湊表裡存下 id -> (start, length)。erase 時用 ID 在雜湊表裡查到區間,把這些單元清空,然後刪除映射。
Python 解法
class MemoryRegion:
"""連續記憶體的配置 / 釋放模擬。
used[i] 表示單元 i 是否被佔用;blocks 記錄 id -> (start, length)。
"""
def __init__(self, size: int):
self.used = [False] * size
self.blocks = {} # id -> (start, length)
self.next_id = 0 # 遞增配置 ID
def alloc(self, x: int) -> int:
"""佔用最左邊 x 個連續空閒單元,回傳起始索引;失敗回傳 -1。"""
run = 0
for i in range(len(self.used)):
if self.used[i]:
run = 0 # 碰到佔用,連續長度歸零
else:
run += 1
if run == x: # 湊夠 x 個連續空閒
start = i - x + 1
for j in range(start, i + 1):
self.used[j] = True
self.blocks[self.next_id] = (start, x)
self.next_id += 1
return start
return -1 # 沒有足夠的連續空閒
def erase(self, block_id: int) -> int:
"""釋放該 ID 對應的整塊記憶體,回傳長度;無效 ID 回傳 -1。"""
if block_id not in self.blocks:
return -1
start, length = self.blocks.pop(block_id)
for j in range(start, start + length):
self.used[j] = False
return length
Dry run 演示
假設記憶體大小為 8,依次執行操作:
| 操作 | 記憶體狀態(1=佔用) | 回傳 | 說明 |
|---|---|---|---|
| 初始 | 00000000 |
— | 全空閒 |
alloc 3 |
11100000 |
0 |
最左 3 連空閒,配置 ID 0 |
alloc 2 |
11111000 |
3 |
接著往右,配置 ID 1 |
erase 0 |
00011000 |
3 |
釋放 ID 0 的 3 格 |
alloc 4 |
00011111 |
4 |
左側只有 3 空格不夠,落到索引 4 |
erase 5 |
00011111 |
-1 |
ID 5 不存在 |
時間複雜度:alloc 為 O(n)(最壞掃描整段記憶體),erase 為 O(len)(清空對應區間)。
空間複雜度:O(n),用於佔用陣列與配置映射。
Problem 4:障礙區間查詢
題目背景
這一題在共享題庫裡的題面被貼錯了——原文把 Q4 配成了 Q1 的描述,真正的 Q4 要靠它的解法來還原。它實際考的是一個障礙放置 / 區間查詢問題:你維護一個有序的障礙位置陣列,支援兩種操作:
ADD(pos):在pos處新增一個障礙,插入後仍保持有序。QUERY(x, size):判斷一個長度為size的物體能否恰好放在x之前結束,也就是區間[x - size, x - 1]內是否沒有任何障礙。
思路
用 bisect.insort 保持陣列有序,插入是 O(n)(陣列搬移導致)。查詢時用二分找到「位置小於等於 x - 1 的最靠右的那個障礙」,再看它是否落在 [x - size, x - 1] 內:落在裡面就說明放不下,回傳 False;否則回傳 True。查詢是 O(log n)。
提示:如果面試進一步要求插入也做到 O(log n),就把有序陣列換成平衡二元搜尋樹 / 有序集合(例如
sortedcontainers.SortedList),插入和查詢都能到 O(log n)。
Python 解法
import bisect
class ObstacleField:
"""維護有序障礙位置,支援新增與區間空閒查詢。"""
def __init__(self):
self.obstacles = [] # 始終保持升序
def add(self, pos: int) -> None:
"""插入一個障礙,保持陣列有序(O(n) 的陣列搬移)。"""
bisect.insort(self.obstacles, pos)
def query(self, x: int, size: int) -> bool:
"""判斷 [x - size, x - 1] 區間內是否沒有障礙。"""
left = x - size
right = x - 1
# 找到第一個 > right 的位置,它左邊一個就是 <= right 的最右障礙
idx = bisect.bisect_right(self.obstacles, right) - 1
if idx < 0:
return True # right 左側沒有任何障礙
nearest = self.obstacles[idx]
return nearest < left # 落在 [left, right] 內則放不下
Dry run 演示
依次 ADD 2、ADD 5、ADD 9,陣列變為 [2, 5, 9],再做幾次查詢:
| 操作 | 目標區間 [x-size, x-1] |
最近障礙 | 回傳 | 說明 |
|---|---|---|---|---|
QUERY(x=5, size=2) |
[3, 4] |
2(<=4 最右) |
True |
2 < 3,區間內無障礙 |
QUERY(x=6, size=3) |
[3, 5] |
5 | False |
5 落在 [3,5] 內 |
QUERY(x=2, size=1) |
[1, 1] |
無(idx<0) |
True |
1 左側無障礙 |
QUERY(x=10, size=1) |
[9, 9] |
9 | False |
9 落在 [9,9] 內 |
時間複雜度:query 為 O(log n)(二分);add 為 O(n)(陣列搬移)。若改用有序集合,add 也可降到 O(log n)。
空間複雜度:O(n),存放障礙位置。
備考策略
- 前兩道求快求穩:Q1、Q2 都是一遍掃描能解決的基礎題,練到看題即寫、順手把空字串 / 含 0 / 並列取最右這類邊界帶上,20 分鐘內清掉,給後面留時間。
- 模擬題重在把狀態維護清楚:Q3 這種 alloc/free 靠一個佔用陣列加一個 ID 映射就能覆蓋,寫之前先把「怎麼找連續空閒」和「怎麼按 ID 回收」兩條主線想明白,能少踩很多索引錯誤。
- 資料結構題先想清讀寫複雜度:Q4 的核心是「有序結構 + 二分查詢」,先預設用
bisect,再根據是否要求高頻插入決定要不要上SortedList。 - 提交前自測邊界:CodeSignal 要通過全部用例,寫完先手動餵幾個極端輸入(空、單元素、全佔用、越界查詢)再點提交。
FAQ
Q1:CodeSignal 的題目一定要通過全部測試用例嗎?
是的。和某些只看思路的現場輪不同,CodeSignal General Coding Assessment 是按通過的隱藏用例計分的,所以提交前一定要自己 dry run 幾個邊界例子,確認邏輯站得住再交。
Q2:共享題庫的老題現在還會出現嗎?
會。雖然介面刷新了一版、也輪換了部分題目,但共享題庫裡的經典題仍然高頻復現。我這次四道裡就有三道能在往期討論裡對上號,提前刷一遍高頻題很值。
Q3:Q3 記憶體模擬裡 alloc 找不到足夠空間怎麼處理?
掃描整段記憶體後連續空閒長度始終沒達到 x,就回傳 -1,且不改動任何狀態。注意每碰到一個被佔用的單元要立刻把連續計數歸零。
Q4:Q4 的題面被貼錯了,怎麼判斷真正要考什麼?
共享題庫偶爾會出現題面與測試用例不匹配的情況。遇到這種,以測試用例和函式簽章為準去反推真實邏輯——這一題的 ADD / QUERY 語意就是從用例還原出來的障礙區間查詢。
Q5:Q4 用有序陣列還是有序集合更好?
如果查詢遠多於插入,bisect + 有序陣列足夠,查詢 O(log n)、插入 O(n)。如果插入也很頻繁,換成 sortedcontainers.SortedList,插入和查詢都能到 O(log n)。
正在準備 TikTok 的 OA? 我們熟悉 CodeSignal General Coding Assessment 的題庫與節奏,能提供全程 OA輔助 / OA代面 服務,陪你把高頻題型和邊界處理一次打磨到位。
立即添加微信 Coding0201,取得一對一客製備考方案。
聯繫方式
- 微信:Coding0201
- Email:[email protected]
- Telegram:@OAVOProxy