大厂真题 / OPPO

OPPO 8.8 笔试真题 - 数据分析岗

本场考试概述

考试时间:2026年8月8日

考试岗位:数据分析岗

考试方向:编程题、SQL、AI Coding

难度评级:简单偏中等

本页收录考点

  • 第一题:字符串模拟、转义扫描(难度简单)
  • 第二题:分组聚合、COUNT(DISTINCT)HAVING 与多字段排序(难度中等)

建议策略

  • 编程题先明确“反斜杠只作用于紧随其后的一个字符”,再做线性扫描,避免只看下划线前一个字符
  • SQL 题先拆分单行过滤、分组统计、组级过滤和结果排序,再分别放入 WHERE、聚合函数、HAVINGORDER BY
  • 活跃设备数与行为记录数是两种统计口径:前者需要去重,后者不能去重

本页仅整理题目信息完整、能够独立验证的两道题。


第 1 题:下划线转义

题目描述

在一般的 LaTeX 渲染中,下划线 _ 若要被正常显示,需要在它前面放置一个有效的反斜杠 \

反斜杠本身也可以被转义。从左到右解析字符串时,一个反斜杠会作用于紧随其后的一个字符;被作用过的字符不会继续转义后面的字符。

给定一个由可见 ASCII 字符组成的字符串,请计算至少还需要补多少个反斜杠,才能让其中所有下划线都被正常显示。

输入描述

第一行输入一个整数 $n$,表示字符串长度,其中 $1 \le n \le 2\times 10^5$。

第二行输入一个长度为 $n$ 的字符串 $s$。

输出描述

输出一个整数,表示至少还需要补充的反斜杠数量。

样例

输入

4
\___

输出

2

样例解释

第一个反斜杠作用于紧随其后的第一个下划线,因此这个下划线已经被转义。后面两个下划线都是裸露的,各需要补一个反斜杠。

另一个容易误判的例子是字符串 \\_:第一根反斜杠转义第二根反斜杠,第二根反斜杠不再作用于下划线,因此仍需补一个反斜杠。

思路分析

把指针放在下一个尚未解析的字符上,并从左到右扫描:

  1. 当前字符是反斜杠时,它会与后面的一个字符共同构成一个解析单元。无论后一个字符是什么,都将指针向后移动两位。
  2. 当前字符是下划线时,说明它没有被前面的反斜杠消费,因此必须补一个反斜杠,答案加一,再向后移动一位。
  3. 当前字符是其他字符时,直接向后移动一位。

为什么可以直接统计扫描过程中遇到的裸下划线?因为在裸下划线前补一根反斜杠,只会让新反斜杠作用于当前下划线,不会改变左侧已经完成的解析,也不会影响右侧尚未解析的部分。因此每个裸下划线恰好需要补一根,局部最优可以组成全局最优。

正确性证明

扫描指针始终指向尚未被任何有效反斜杠作用的第一个字符。

  • 如果当前字符是反斜杠,按照题意,它必然作用于下一个字符,所以同时跳过这两个字符不会遗漏任何需要处理的下划线。
  • 如果当前字符是下划线,由于它仍位于扫描指针处,说明它没有被此前的反斜杠作用。任何合法方案都至少要在它前面补一根反斜杠;补一根已经足够,因此答案增加一是必要且最优的。
  • 其他字符既不需要转义,也不会转义后续字符,跳过它不会改变答案。

算法逐个解析所有字符,并对每个裸下划线执行一次必要且充分的补充,所以最终计数就是最少需要补充的反斜杠数量。

题解代码

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 各渠道的活跃设备数和行为记录数,并满足以下要求:

  1. 只统计 ACTIVEAPP_OPENPAY 三种行为,其他行为不计入
  2. 活跃设备数为不同设备的数量
  3. 行为记录数为符合条件的日志总条数
  4. 只输出活跃设备数不少于 2 的渠道
  5. 返回字段依次为 channelactive_device_countevent_count
  6. 先按 active_device_count 降序,再按 event_count 降序,若仍相同则按 channel 升序

表结构

c21_oppo_device_event_log

  • event_id INT:行为记录 ID,主键
  • event_date DATE:行为日期
  • channel VARCHAR(20):渠道名称
  • device_id INT:设备 ID
  • event_type VARCHAR(20):行为类型,可能取 ACTIVEAPP_OPENPAYHEARTBEAT

样例

输入

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 中。

第四步:完整实现三级排序

依次按设备数降序、记录数降序、渠道名升序排序。样例中的 oemofficial_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 题则要明确每个条件属于查询执行过程的哪一层。