大厂真题 / OPPO
OPPO 8.8 笔试真题 - 数据分析岗
本场考试概述
考试时间:2026年8月8日
考试岗位:数据分析岗
考试方向:编程题、SQL、AI Coding
难度评级:简单偏中等
本页收录考点:
- 第一题:字符串模拟、转义扫描(难度简单)
- 第二题:分组聚合、
COUNT(DISTINCT)、HAVING与多字段排序(难度中等)
建议策略:
- 编程题先明确“反斜杠只作用于紧随其后的一个字符”,再做线性扫描,避免只看下划线前一个字符
- SQL 题先拆分单行过滤、分组统计、组级过滤和结果排序,再分别放入
WHERE、聚合函数、HAVING和ORDER BY - 活跃设备数与行为记录数是两种统计口径:前者需要去重,后者不能去重
本页仅整理题目信息完整、能够独立验证的两道题。
第 1 题:下划线转义
题目描述
在一般的 LaTeX 渲染中,下划线 _ 若要被正常显示,需要在它前面放置一个有效的反斜杠 \。
反斜杠本身也可以被转义。从左到右解析字符串时,一个反斜杠会作用于紧随其后的一个字符;被作用过的字符不会继续转义后面的字符。
给定一个由可见 ASCII 字符组成的字符串,请计算至少还需要补多少个反斜杠,才能让其中所有下划线都被正常显示。
输入描述
第一行输入一个整数 $n$,表示字符串长度,其中 $1 \le n \le 2\times 10^5$。
第二行输入一个长度为 $n$ 的字符串 $s$。
输出描述
输出一个整数,表示至少还需要补充的反斜杠数量。
样例
输入
4
\___
输出
2
样例解释
第一个反斜杠作用于紧随其后的第一个下划线,因此这个下划线已经被转义。后面两个下划线都是裸露的,各需要补一个反斜杠。
另一个容易误判的例子是字符串 \\_:第一根反斜杠转义第二根反斜杠,第二根反斜杠不再作用于下划线,因此仍需补一个反斜杠。
思路分析
把指针放在下一个尚未解析的字符上,并从左到右扫描:
- 当前字符是反斜杠时,它会与后面的一个字符共同构成一个解析单元。无论后一个字符是什么,都将指针向后移动两位。
- 当前字符是下划线时,说明它没有被前面的反斜杠消费,因此必须补一个反斜杠,答案加一,再向后移动一位。
- 当前字符是其他字符时,直接向后移动一位。
为什么可以直接统计扫描过程中遇到的裸下划线?因为在裸下划线前补一根反斜杠,只会让新反斜杠作用于当前下划线,不会改变左侧已经完成的解析,也不会影响右侧尚未解析的部分。因此每个裸下划线恰好需要补一根,局部最优可以组成全局最优。
正确性证明
扫描指针始终指向尚未被任何有效反斜杠作用的第一个字符。
- 如果当前字符是反斜杠,按照题意,它必然作用于下一个字符,所以同时跳过这两个字符不会遗漏任何需要处理的下划线。
- 如果当前字符是下划线,由于它仍位于扫描指针处,说明它没有被此前的反斜杠作用。任何合法方案都至少要在它前面补一根反斜杠;补一根已经足够,因此答案增加一是必要且最优的。
- 其他字符既不需要转义,也不会转义后续字符,跳过它不会改变答案。
算法逐个解析所有字符,并对每个裸下划线执行一次必要且充分的补充,所以最终计数就是最少需要补充的反斜杠数量。
题解代码
import sys
def solve() -> None:
input_stream = sys.stdin
length_line = input_stream.readline()
if not length_line:
return
n = int(length_line)
s = input_stream.readline().rstrip("\n")
if s.endswith("\r"):
s = s[:-1]
missing = 0
i = 0
while i < n:
if s[i] == "\\":
i += 2
elif s[i] == "_":
missing += 1
i += 1
else:
i += 1
print(missing)
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:$O(n)$,指针始终向右移动,每个字符至多被处理一次。
空间复杂度:$O(n)$,用于存储输入字符串;除输入外只使用常数个变量。
易错点
- 不能只判断下划线左边是否为反斜杠;连续两根反斜杠会互相形成转义单元
- 读入字符串时不要使用
strip(),否则可能误删题目允许的首尾空格 - 字符串末尾只有一根反斜杠时,
i += 2只会让指针越过末尾,不会发生越界访问 - 反斜杠在 Python 字符串字面量中要写成
"\\"
第 2 题:各渠道活跃设备数与行为记录数
题目描述
某智能终端业务使用设备行为日志表 c21_oppo_device_event_log 记录各渠道的每日设备行为。
请统计 2026-09-01 各渠道的活跃设备数和行为记录数,并满足以下要求:
- 只统计
ACTIVE、APP_OPEN、PAY三种行为,其他行为不计入 - 活跃设备数为不同设备的数量
- 行为记录数为符合条件的日志总条数
- 只输出活跃设备数不少于 2 的渠道
- 返回字段依次为
channel、active_device_count、event_count - 先按
active_device_count降序,再按event_count降序,若仍相同则按channel升序
表结构
c21_oppo_device_event_log:
event_id INT:行为记录 ID,主键event_date DATE:行为日期channel VARCHAR(20):渠道名称device_id INT:设备 IDevent_type VARCHAR(20):行为类型,可能取ACTIVE、APP_OPEN、PAY、HEARTBEAT
样例
输入
CREATE TABLE c21_oppo_device_event_log (
event_id INT PRIMARY KEY,
event_date DATE NOT NULL,
channel VARCHAR(20) NOT NULL,
device_id INT NOT NULL,
event_type VARCHAR(20) NOT NULL
);
INSERT INTO c21_oppo_device_event_log VALUES
(1, '2026-09-01', 'official_web', 401, 'ACTIVE'),
(2, '2026-09-01', 'official_web', 401, 'PAY'),
(3, '2026-09-01', 'official_web', 402, 'APP_OPEN'),
(4, '2026-09-01', 'oem', 301, 'ACTIVE'),
(5, '2026-09-01', 'oem', 301, 'APP_OPEN'),
(6, '2026-09-01', 'oem', 302, 'ACTIVE'),
(7, '2026-09-01', 'ad_feed', 201, 'ACTIVE'),
(8, '2026-09-01', 'ad_feed', 201, 'APP_OPEN'),
(9, '2026-09-01', 'ad_feed', 202, 'ACTIVE'),
(10, '2026-09-01', 'ad_feed', 202, 'PAY'),
(11, '2026-09-01', 'app_store', 101, 'ACTIVE'),
(12, '2026-09-01', 'app_store', 101, 'APP_OPEN'),
(13, '2026-09-01', 'app_store', 101, 'PAY'),
(14, '2026-09-01', 'app_store', 102, 'ACTIVE'),
(15, '2026-09-01', 'app_store', 102, 'APP_OPEN'),
(16, '2026-09-01', 'app_store', 103, 'ACTIVE'),
(17, '2026-09-01', 'app_store', 103, 'HEARTBEAT'),
(18, '2026-09-01', 'preinstall', 501, 'ACTIVE'),
(19, '2026-09-01', 'preinstall', 501, 'APP_OPEN'),
(20, '2026-09-01', 'preinstall', 502, 'HEARTBEAT'),
(21, '2026-09-02', 'app_store', 104, 'ACTIVE'),
(22, '2026-08-31', 'ad_feed', 203, 'ACTIVE');
输出
channel | active_device_count | event_count
app_store | 3 | 6
ad_feed | 2 | 4
oem | 2 | 3
official_web | 2 | 3
思路分析
这道题需要把四类操作放到正确的 SQL 阶段。
第一步:用 WHERE 做单行过滤
日期和行为类型都能针对单条日志判断,因此先保留 2026-09-01 且行为类型属于指定集合的记录。HEARTBEAT 以及其他日期的日志都应在聚合前移除。
第二步:按渠道分组并计算两种口径
COUNT(DISTINCT device_id)统计不同设备数量COUNT(*)统计过滤后保留下来的日志数量
同一台设备可能产生多条日志,因此这两个指标不能混用。
第三步:用 HAVING 做组级过滤
“活跃设备数不少于 2”只有完成分组后才能判断,应写在 HAVING 中,不能写在 WHERE 中。
第四步:完整实现三级排序
依次按设备数降序、记录数降序、渠道名升序排序。样例中的 oem 与 official_web 前两个指标完全相同,最后一级排序决定它们的稳定顺序。
题解代码
SELECT
channel,
COUNT(DISTINCT device_id) AS active_device_count,
COUNT(*) AS event_count
FROM c21_oppo_device_event_log
WHERE event_date = '2026-09-01'
AND event_type IN ('ACTIVE', 'APP_OPEN', 'PAY')
GROUP BY channel
HAVING COUNT(DISTINCT device_id) >= 2
ORDER BY
active_device_count DESC,
event_count DESC,
channel ASC;
复杂度分析
时间复杂度:在没有可用索引时为 $O(n)$ 的日志扫描,分组与去重还会产生与各渠道设备集合规模相关的哈希或排序开销。
空间复杂度:最坏为 $O(n)$,用于维护分组结果和各渠道的去重设备集合。
易错点
- 活跃设备数必须使用
COUNT(DISTINCT device_id),不能写成COUNT(*) - 行为记录数不能去重,同一设备的多次有效行为都应计入
HEARTBEAT必须在聚合前过滤,否则只产生心跳的设备会被错误计为活跃设备- “不少于 2”是
>= 2,不是> 2 - 组级条件应使用
HAVING,不能在WHERE中引用聚合结果 - 三级排序必须全部写出,否则指标相同时结果顺序不稳定
知识点总结
| 题目 | 核心考点 | 关键结论 |
|---|---|---|
| 下划线转义 | 字符串模拟、转义扫描 | 有效反斜杠与后一个字符组成解析单元,裸下划线各补一根 |
| 渠道行为统计 | 分组聚合、去重计数、HAVING | 单行条件进 WHERE,组级条件进 HAVING,两种统计口径分开处理 |
数据分析岗笔试不一定追求高难度算法,但很重视边界条件和统计口径。字符串题要把解析规则转成稳定的扫描状态,SQL 题则要明确每个条件属于查询执行过程的哪一层。