前段时间投了 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