名企AI面试100题:考点地图与结论—机制—边界答题框架
简介:《名企AI面试100题1》面向准备名企AI岗位笔试与面试的计算机专业学生及转行求职者,聚焦高频基础考点与答题思路的集中梳理。内容节选整理自大型题库,覆盖计算机语言基础、算法和大数据、机器学习、深度学习及应用方向等章节,并围绕C++调用C函数时为何使用extern "C"、Java类初始化顺序、死锁构造、ArrayList遍历删除、Linux CPU与进程排查、iptables规则及Python列表去重等典型问题给出解析与代码示例,便于按章节系统复习和查漏补缺。资源包为1个PDF文件,约49.22MB,适合在电脑或平板上阅读标注。已有251人学习,可用于面试前快速过题、理解常见陷阱并积累规范表达,提升现场作答的条理与准确度。
1. 这场AI面试考什么:五章目录就是一张分值地图
很多考生拿到这份「名企AI面试100题」之后,第一反应是从第 1 题开始按顺序刷。这是最低效的姿势。这份题库的章节顺序——计算机语言基础、算法和大数据、机器学习、深度学习、应用方向——本身就是一张面试考点地图:越靠前的章节越像用来筛人的基础题,越靠后的章节越像决定薪资档位的区分题。面试官不会平均用力,语言基础两三轮就过,机器学习那几十道题才是真正吃掉四十分钟的地方。所以拿分的关键不是刷完 100 道,而是先弄清楚哪 30 道决定你是及格还是优秀,剩下的题在什么情况下才会被追问到。
2. 计算机语言基础:extern "C"、死锁和 Linux 命令的六个必答细节
这一章在面试里出现频率最高、单题分值最低。它的作用不是拉开差距,而是快速判断你的基础是不是靠背题堆出来的。答题时间通常只有两三分钟,所以答法必须短、准、有机制:先给结论,再说一层原理,最后补一句边界或者追问的落点。下面四组题,基本覆盖了这一章的高频区。
2.1 extern "C":从符号修饰讲到链接失败
这道题的提问方式几乎固定:"C++ 程序里调用 C 编译器编译出来的函数,为什么要加 extern "C"?" 背答案的人会说"为了兼容 C",这句话只能拿到一半的分,因为它没有解释清楚"链接期到底发生了什么"。
真正的机制是符号名。C++ 支持函数重载,同一个名字可以有多个不同参数列表的版本,编译器必须把参数信息编进符号名里,才能让链接器区分它们,这个过程就是名称修饰(name mangling)。C 语言不支持重载,因此编译器不做修饰。于是同一个void foo(int, int),C 编译器产出的符号是foo,C++ 编译器产出的可能是_Z3fooii。链接器只认符号名,名字对不上就报"undefined reference"。extern "C"的作用只有一个:告诉 C++ 编译器,被它修饰的这部分声明按 C 语言的规则生成和查找符号,不要做名称修饰。
// calc.h —— 一个同时被 C 和 C++ 源文件包含的头文件 #ifdef __cplusplus extern "C" { #endif int add(int x, int y); #ifdef __cplusplus } #endif这段代码的关键在__cplusplus宏:它只在 C++ 编译器下被定义,所以用 C 编译器编译时,extern "C"这两行会被预处理器整块删掉,头文件退化成一条普通声明。很多考生在答题时漏掉这个宏,直接用extern "C" { ... }包住头文件,结果这份头文件再也无法被 C 源文件包含,一包含就报语法错误。面试官如果追问"为什么不直接把 C 源文件也加上 extern "C"",答案就在这儿:C 编译器根本不认识这个语法。
想在答题时显得有实感,可以补一句验证方法:
# C 侧:符号未经修饰 gcc -c calc.c -o calc.o nm calc.o | grep add # 输出形如 T add # C++ 侧:未加 extern "C" 时,符号已被修饰,链接必然失败 g++ -c main.cpp -o main.o nm main.o | grep add # 输出形如 U _Z3addiiT表示该符号在本目标文件里定义,U表示未定义、需要链接期从别处找。_Z3addii里的3是函数名长度,ii是两个 int 参数,这套编码规则在 Linux 上很常见。答题顺序就是:现象(undefined reference)→ 机制(名称修饰)→ 解法(用 extern "C" 声明,配合__cplusplus宏)→ 边界(只影响声明处可见的代码,不影响已有目标文件)。
2.2 死锁与 ArrayList:两道写代码题的评分点
"写一个死锁"这类题,白板上写不出来就是直接掉档。它的评分点不在代码多漂亮,而在你能不能说清楚"我构造的是哪种破坏条件的缺失"。死锁有四个必要条件:互斥、请求与保持、不可剥夺、循环等待。最省事的构造方式就是让两个线程以相反顺序获取同一对锁,制造循环等待。
public class DeadLockDemo { private static final Object LOCK_A = new Object(); private static final Object LOCK_B = new Object(); static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public static void main(String[] args) { new Thread(() -> { // t1:先 A 后 B synchronized (LOCK_A) { sleep(100); // 留出时间窗,让 t2 先把 B 抢走 synchronized (LOCK_B) { System.out.println("t1 finished"); } } }, "t1").start(); new Thread(() -> { // t2:先 B 后 A,加锁顺序与 t1 相反 synchronized (LOCK_B) { sleep(100); synchronized (LOCK_A) { System.out.println("t2 finished"); } } }, "t2").start(); } }sleep(100)不是为了"玄学复现",而是为了让两个线程各自拿到第一把锁之后再互相请求,把时间窗撑开;如果没有这个间隔,很可能一个线程跑完两个锁,程序正常退出,看起来像没写对。跑起来之后用jstack <pid>能看到 "Found one Java-level deadlock" 以及两个线程互相持有的锁对象,这一段如果能主动说出来,比代码本身更加分。
配套的另一道题是"能不能用 for 循环直接删除 ArrayList 里的特定元素"。标准错法有两种:用下标遍历时删掉一个元素,后面的元素整体前移,i++会跳过紧接着的那个;用增强 for 循环删,则会抛ConcurrentModificationException。原因在于增强 for 底层是迭代器,迭代器在next()里比较modCount和expectedModCount,集合被结构性修改而迭代器不知情时就快速失败(fail-fast)。
// 反例:下标删除会漏删连续重复元素 for (int i = 0; i < list.size(); i++) { if ("b".equals(list.get(i))) list.remove(i); // 删完后 i 自增,跳过了下一个元素 } // 正解一:用迭代器自己的 remove,modCount 会被同步更新 Iterator<String> it = list.iterator(); while (it.hasNext()) { if ("b".equals(it.next())) it.remove(); } // 正解二:一行搞定,内部仍然是迭代器删除 list.removeIf("b"::equals);答题时把"错在哪、为什么错、怎么改、什么场景改用 CopyOnWriteArrayList"四句话说完就够。最后一句是加分项:CopyOnWriteArrayList每次写都复制整个数组,只适合读多写极少的场景,写多的时候性能会崩,不要当成万能药。
2.3 链表与数组:三类模板题的最小可用代码
链表判环是这一章最经典的题,答法分两层:能不能判出有环,能不能找到入口。快慢指针同时从头出发,慢的走一步、快的走两步,有环必然相遇。
def has_cycle(head): slow = fast = head while fast and fast.next: # fast 走两步,所以两个判空都要写 slow = slow.next fast = fast.next.next if slow is fast: return True return False找入口的做法是相遇之后把其中一个指针放回头部,两个指针都改成每次走一步,再次相遇的位置就是环的入口。why 的解释要能说出来:设头到入口距离为 a,入口到相遇点为 b,环长为 L,相遇时快指针比慢指针多走了一圈,即 a + b + nL = 2(a + b),化简得 a = nL - b,也就是说从头出发走 a 步、从相遇点出发走 nL - b 步会同时到达入口。
def detect_cycle(head): slow = fast = head while fast and fast.next: slow, fast = slow.next, fast.next.next if slow is fast: # 相遇:一个指针回头部,同速再走 p = head while p is not slow: p, slow = p.next, slow.next return p # 环的入口 return None数组类题里,"严格单调递增的整数数组中找 a[x] == x 的位置"最容易答成线性扫描。观察到数组严格递增,则 a[i] - i 单调不减,可以二分。
def find_fixed_point(a): lo, hi = 0, len(a) - 1 while lo <= hi: mid = (lo + hi) // 2 if a[mid] == mid: return mid if a[mid] < mid: lo = mid + 1 # a[i]-i 单调不减,左半区间不可能再有解 else: hi = mid - 1 return -1再看最大连续子数组和,状态定义只有一句话:以当前元素结尾的最大和,要么接着前面延续,要么从当前元素重新开始。
def max_subarray(nums): best = cur = nums[0] for x in nums[1:]: cur = max(x, cur + x) # 续上 或 重开,二选一 best = max(best, cur) return best这三道题放在一起练的价值在于:它们代表了链表双指针、有序性二分、线性 DP 三个不同的思考起点。面试官在白板上通常只给一道,但会连着追问两个变体,比如"如果数组不是严格递增而是非递减,二分还成立吗"——答案是不成立,因为 a[i] - i 不再单调,得退化成线性扫描或者其他思路。能主动说出这个边界,说明你是在推导而不是在背题。
2.4 Linux 命令与海量数据:命令题答"看哪一列",数据题答"分几步"
命令类题看着零碎,其实有固定答法:命令 + 看哪一列 + 一句追问。下面这张表把这一章最高频的几条压在一起,面试前扫一遍就够。
| 问题 | 命令 | 要看的关键列或参数 | 常见追问 |
|---|---|---|---|
| 几颗物理 CPU、每颗几核 | lscpu | Socket(s)是物理 CPU 数,Core(s) per socket是单颗核数 | 逻辑核数 = 物理核数 × 每核核数 × 超线程倍数 |
| 内存占用看哪列 | top | %MEM是内存占比,RES是驻留内存,SHR是共享内存 | VIRT 虚高不代表真占内存 |
| 当前有哪些进程 | ps aux/ps -ef | STAT 列看进程状态,R 运行、S 睡眠、Z 僵尸 | 僵尸进程怎么清理 |
| 网卡状态 | ip addr show/ifconfig | 收发包计数、错误计数、丢包计数 | 虚拟网卡为什么看不到物理链路 |
| 硬盘健康 | smartctl -a /dev/sdX | SMART 属性里的重映射扇区、待映射扇区 | 为什么不能只看df -h |
iptables 那道题的标准写法是把来源 IP、协议、端口、动作四要素写全:
# 拒绝 192.168.1.101 访问本机 80 端口 iptables -A INPUT -s 192.168.1.101 -p tcp --dport 80 -j DROP-A INPUT追加到入站链末尾,-s指定源地址,-p tcp必须写,因为端口只对 TCP/UDP 有意义,--dport 80是目的端口,-j DROP是静默丢弃。这里有一个高频追问:DROP和REJECT有什么区别。DROP不回包,客户端只能等到超时;REJECT会返回一个拒绝报文,客户端立刻失败。线上做封禁一般用DROP,做可观测的拒绝用REJECT。
海量数据题的代表是"从日志里找出访问次数最多的那个 IP"或者"找出出现次数最多的前 100 个关键字"。答题时先给框架,再给代码,框架是四步:分片、统计、归并、取 Top K。单机内存装不下时,先按hash(key) % N把数据切成 N 份,同一个 key 一定会落到同一份,然后每份单独统计,最后合并。
import heapq, collections def top_k_ips(lines, k=100): counter = collections.Counter(line.split()[0] for line in lines) # 统计每个 key 的次数 # 小顶堆维护 Top K:堆顶是当前第 K 名,新元素比堆顶大才替换 return heapq.nlargest(k, counter.items(), key=lambda kv: kv[1])用heapq.nlargest的好处是复杂度为 O(n log k) 而不是 O(n log n),k 远小于 n 时优势明显。答题时补一句容量估计更加分:10 亿条日志按每行 100 字节算约 100 GB,切成 100 份每份 1 GB,单机就能跑;每份取出的 Top 100 合并后再取一次 Top 100,内存里只需要放 100 × 100 个 (key, count) 对。
3. 算法与大数据:链表判环、Top K 和海量数据题的通用框架
这一章的题量在题库里排第二,但它真正考的并不是背了多少道题,而是面对一个没见过的场景时能不能拆出步骤。面试官在这里最常做的一件事,是给一个约束变化——数据量从万级加到十亿级、数组从有序变成无序——然后看你的解法是跟着变还是原地卡住。所以准备策略是把题按"考点类型"而不是按"题目编号"归档。
3.1 B 树与索引:插入删除答到哪一层
B 树这类结构题,面试官不会真让你手写完整实现,他要的是三件事:为什么要有它、它的平衡是怎么维持的、它和实际系统里的什么东西对应。B 树的动机是磁盘 IO:二叉树在内存里很快,但每下降一层就是一次随机 IO,几百万节点的树深度几十层,代价无法接受。B 树把一个节点撑大到一个磁盘页(常见 4 KB 或 16 KB),让一次 IO 带回几百上千个键,深度压到三四层。
插入的答法是:所有插入都发生在叶子节点,插入后如果节点键数超过上限就分裂,把中间键上推到父节点,父节点也可能因此再分裂,最坏一路传到根,根分裂时树高加一。删除更绕:如果删完节点键数低于下限(通常取 m/2 上取整减一),先看兄弟节点是否富余,富余就借一个键上来、把父节点的分隔键拿下来;兄弟也紧张就合并,合并可能导致父节点继续下溢,最坏一路传到根,根合并时树高减一。这两句话能说顺,比默写伪代码有效得多。
追问通常落在"B 树和 B+ 树的区别"上:B+ 树只有叶子节点存数据、内部节点只存索引键,叶子之间用链表串起来,所以范围查询可以顺着链表扫,而 B 树做范围查询要反复中序回溯。这就是为什么存储引擎几乎都用 B+ 树。
3.2 数组类题:从"能不能二分"开始判断
数组题的通用判断链是:数组有没有序、有没有单调性、要求的是连续还是子序列、能不能用双指针。有序就优先想二分,无序但求连续区间就想滑动窗口或者 Kadane 那一套,求子序列就想 DP 或者贪心。
"数组中的子序列个数"这类题容易翻车在计数重复上。如果只求不同子序列的个数,DP 定义是"以第 i 个元素结尾的不同子序列个数",转移时要减去上一次同一个字符贡献的重复部分,否则同样的字符出现在不同位置会被重复计数。答题时先把"子序列不要求连续"这句话说出来,再问清"是否要求不同",因为两者的解法完全不同——这一步主动澄清,面试官通常会给正面反馈。
"金字塔最小路径之和"是另一种典型:给一个二维三角,从顶走到底,每次只能走到下一行的相邻位置,求路径和最小。自底向上比自顶向下好写,因为不用处理边界分支。
def minimum_total(triangle): dp = triangle[-1][:] # 从最后一行起步 for i in range(len(triangle) - 2, -1, -1): for j in range(len(triangle[i])): dp[j] = triangle[i][j] + min(dp[j], dp[j + 1]) # 只依赖下一行相邻两个 return dp[0]这个写法把空间压成了一维,dp[j]在更新时代表"从下一行第 j 个位置到底部的最小和",所以min(dp[j], dp[j+1])就是两条合法路径。面试官如果追问空间能不能再省,答案是没法省到 O(1),但可以只保留当前行;如果追问路径本身怎么还原,就要额外开一个二维数组记录选择。
"交替字符串"考的是双序列 DP:判断第三个字符串是否由前两个字符串交错构成。状态是dp[i][j]表示第一个串取前 i 个、第二个串取前 j 个能否拼出第三个串的前 i+j 个,转移看第三个串的当前字符能不能从第一个串或者第二个串来。这道题的价值在于它同时考了状态定义、边界初始化和滚动数组优化,一道题能聊十分钟。
3.3 最长连续递增序列:别把"连续"读成"递增子序列"
这道题是坑点密集区。最长连续递增序列要求元素在原数组里相邻,一次扫描就够;最长递增子序列不要求相邻,要 DP 加二分做到 O(n log n)。读题读错,整道题的解法全错,而且代码跑出来的结果看着"也对",这是最难受的一类翻车。
def find_length_of_lcis(nums): if not nums: return 0 best = cur = 1 # 单个元素本身就是长度 1 的连续递增序列 for i in range(1, len(nums)): if nums[i] > nums[i - 1]: cur += 1 # 接着上一段继续 else: cur = 1 # 断了,从当前元素重开 best = max(best, cur) return best注意cur重置成 1 而不是 0,因为当前元素自己至少构成长度 1 的序列。这类边界值就是评分点,写错一个初始化,空数组或者全降序的用例上就露馅。
如果面试官改口说"不要求连续",答题结构要立刻切换:定义dp[i]为以第 i 个元素结尾的最长递增子序列长度,转移是找前面所有比它小的元素里的最大值加一,也就是 O(n²);再优化就是用tails数组维护每个长度对应的最小结尾值,因为tails单调,可以二分,复杂度降到 O(n log n)。能把这两步的演进讲清楚,比直接写出最优解更有说服力。
3.4 海量数据题的通用四步框架
"有序链表转换二叉搜索树"这类结构题和"海量日志找 Top IP"这类数据题,看起来毫不相干,但面试时的答法可以套同一个框架:先确定输入输出的规模,再确定能不能一次装进内存,然后选分治或流式,最后给复杂度和失败兜底。
有序链表转平衡二叉搜索树的关键是"找中点",链表不能随机访问,所以用快慢指针找中点,递归建树。时间复杂度 O(n log n),如果把链表先转成数组可以降到 O(n),代价是 O(n) 额外空间。面试官通常追问"能不能不用额外空间",答案是可以在递归时同时推进链表指针做到 O(n),但代码可读性会明显下降,这个取舍要能说出来。
hadoop 和 spark 的对比题属于"看起来是八股,其实在考你有没有工程判断"。答法不要罗列组件名,而是抓住三个维度:计算模型(一个偏批处理的磁盘迭代,一个偏内存的 DAG 执行)、中间结果(一个落盘,一个尽量留在内存)、适用场景(一个适合超大规模稳定批任务,一个适合迭代型算法和交互式分析)。能补一句"迭代次数多的机器学习任务在磁盘模型下会被 IO 拖死",这道题就答到位了。
4. 机器学习:SVM、树模型与特征工程的答题骨架
这一章是整份题库的重心,题量最大,也最容易把时间讲超。面试官在这里的提问逻辑是"你知不知道它为什么这么设计",而不是"你记不记得公式"。所以每道题都应该准备一个三层结构:一句话定义、一层机制、一个边界。下面按高频聚类拆开。
4.1 SVM 与 EM:推导题答到什么深度
SVM 的完整链条很长:几何间隔、间隔最大化、拉格朗日乘子、对偶问题、KKT 条件、核技巧、软间隔。全推一遍要二十分钟,实际面试只给五到八分钟,所以必须会裁剪。裁剪的原则是:保住三个关键节点,其余用一句话带过。
第一个节点是"为什么最大化间隔"。直观答案是间隔越大,模型对噪声的容忍度越高,泛化误差上界越小;形式化答案是 SVM 的优化目标就是最小化结构风险,间隔出现在正则项的位置。第二个节点是"为什么转对偶"。两个原因:一是原问题的约束是线性的,转成对偶后约束变成简单的上下界,求解更省事;二是对偶形式里样本只以内积出现,把内积换成核函数就能处理非线性,不用显式映射到高维。第三个节点是"支持向量是什么"。答案是对偶问题里乘子非零的样本,它们恰好落在间隔边界上,决定了最终的分类面,其余样本删掉不影响结果——这个稀疏性也是 SVM 相对其他模型的优势之一。
软间隔要能说清 C 的作用:C 越大,对误分类的惩罚越重,间隔越窄,越容易过拟合;C 越小,容忍更多误分类,间隔越宽,偏差上升。这个"参数往哪边调会怎样"的表述,比背公式有用得多。
EM 算法的答法类似。核心矛盾是有隐变量时对数似然里出现"log 求和",直接求导解不出来。解法是先用 Jensen 不等式构造一个下界(E 步求隐变量的后验期望),再最大化这个下界(M 步更新参数),交替进行保证似然单调不降,最终收敛到局部最优而不是全局最优——这句话一定要说,因为它是高频追问的答案。
# EM 的骨架,以混合模型为例 theta = init_params() for step in range(max_iter): # E 步:固定参数,算每个样本属于每个成分的后验概率(responsibility) resp = e_step(data, theta) # M 步:固定后验,最大化期望对数似然,更新参数 theta = m_step(data, resp, theta) if converged(log_likelihood(data, theta)): breakE 步和 M 步的先后不能反,先有后验才能加权求期望。收敛判据一般用对数似然的变化量小于阈值,或者参数变化量小于阈值。如果面试官问"EM 一定能收敛到全局最优吗",答案是不能,它对初值敏感,实践中常用的做法是多组随机初值取最优,或者用 K-Means 的结果做初始化。
4.2 过拟合、正则化与归一化:一套逻辑答遍五道题
过拟合、欠拟合、正则化、归一化、特征离散化这几道题,本质上是同一个知识网络的不同切面。准备的正确方式不是给每道题背一个答案,而是准备一条能走通的因果链。
起点是偏差方差分解:模型误差 = 偏差² + 方差 + 不可约噪声。欠拟合是偏差大,表现是训练集和验证集都差;过拟合是方差大,表现是训练集很好、验证集明显差。判断依据是学习曲线,不是感觉。
中间是解决手段的四个层级:数据层(加样本、数据增强、去噪)、模型层(降复杂度、加正则、早停)、训练层(交叉验证、集成、dropout)、问题层(重新审视特征和标签是否有泄漏)。答这道题时按层级说,比零散罗列十条有效,因为面试官能听出你有体系。
终点是正则化。L1 产生稀疏解,因为它对接近零的参数惩罚是常数斜率,容易把参数压到恰好为零;L2 把参数压向零但不为零,因为它对大参数的惩罚随数值增长。几何解释是约束区域一个是菱形一个是圆形,菱形更容易和目标函数的等高线在坐标轴上相切。这句几何解释是很多人漏掉的加分点。
归一化的因果链是另一条:不同特征的量纲差距大 → 梯度方向被量纲大的特征主导 → 优化路径呈锯齿状 → 收敛慢甚至震荡。归一化之后各特征尺度一致,等高线接近圆形,梯度下降走的是近似直线。同时,基于距离的模型(KNN、K-Means、SVM 的核)对尺度敏感,不归一化等于给某个特征偷偷加了权重;带正则的模型也要求尺度一致,否则惩罚力度在不同特征上不公平。
树模型为什么不需要归一化,这道题的答案只有一个核心:树的分裂点只依赖特征内部的相对顺序,而单调变换不改变顺序,所以归一化对分裂结果没有任何影响。能补一句"但如果是带正则的线性模型做 stacking,或者用树做特征编码后再进线性层,尺度又变得重要",说明你知道边界在哪。
4.3 树模型家族:RF、GBDT、XGBoost 的对比与追问
这三者的关系是面试里最常被连着问的一组。答法用一张对比表打底,再补两个追问。
| 维度 | 随机森林 | GBDT | XGBoost |
|---|---|---|---|
| 集成方式 | Bagging,并行训练多棵树 | Boosting,串行拟合残差 | Boosting,串行但内部特征级并行 |
| 目标 | 降低方差 | 降低偏差 | 降低偏差并加正则控制方差 |
| 目标函数 | 无显式正则 | 损失函数本身 | 损失函数 + 树复杂度正则项 |
| 分裂依据 | 基尼或信息增益 | 负梯度(残差) | 二阶泰勒展开后的增益 |
| 缺失值 | 需要预处理 | 需要预处理 | 分裂时自动学默认方向 |
第一个追问是"XGBoost 为什么要用二阶泰勒展开"。表层答案是二阶信息比一阶更准,收敛更快;深层答案是把损失函数抽象掉了——只要损失二阶可导,就能代进同一个增益公式,于是平方损失、逻辑损失、自定义损失都能用同一套框架;同时展开后的目标函数里,叶子权重有闭式最优解,增益可以直接算出来比较,分裂搜索变得很干净。
第二个追问是"RF 和 GBDT 的区别与联系"。区别在并行性和偏差方差:RF 的树之间独立,靠平均降方差,树越深单棵越强但整体靠投票兜底;GBDT 的树之间有依赖,每棵拟合前面所有的残差,靠累加降偏差。联系是两者都是树集成,都靠"多个弱模型组合成强模型",只是组合方式一个是并行平均、一个是串行累加。
还有一个高频的细节题:"为什么 XGBoost 要对特征预排序,或者说它的并行到底并的是什么"。答案是它并行的是特征维度的分裂点搜索,不是树与树之间——树必须串行,因为后一棵依赖前一棵的残差。这个区分说清楚,能挡掉一大半"以为 XGBoost 是并行训练树"的错误理解。
4.4 特征工程与评估指标:离散化、缺失值、AUC
特征离散化那道题的答案分四块。第一是稳定性:连续值容易受异常值影响,分箱之后单个异常点只影响一个箱。第二是非线性:离散化相当于给模型加了分段常数的表达能力,线性模型也能学到非线性关系。第三是稀疏与效率:one-hot 之后稀疏,很多实现有稀疏优化,计算更快。第四是业务可解释性:分箱的边界往往对应业务上的阈值,风控和营销场景尤其看重这一点。逻辑回归这类线性模型尤其需要离散化,因为它自身学不到特征之间的交互,离散化之后再交叉就补上了这块。
AUC 的物理意义要一句话说准:随机抽一个正样本和一个负样本,模型给正样本打分高于负样本的概率。它等价于 ROC 曲线下的面积,对排序敏感、对阈值不敏感、对类别不平衡相对不敏感——最后这一点经常被答错,很多人以为不平衡会让 AUC 失真,实际上失真的是准确率,AUC 只关心相对顺序。追问通常问"GAUC 是什么、为什么推荐场景更常用",答案是按用户分组算 AUC 再加权平均,因为全局 AUC 会被活跃用户主导,评估的其实是"同一个用户内部排序对不对"。
缺失值处理按模型分:树模型可以直接把缺失当单独分支,或者填一个明显偏离正常范围的常数;线性模型和神经网络一般要填充,常用的有均值、中位数、众数、模型预测填充、以及把"是否缺失"单独作为一个指示特征。最后一点经常被忽略,但缺失本身往往带信息,比如某个字段缺失意味着用户没有完成某一步操作。
特征选择的三类方法要能分清楚:过滤式(方差、卡方、互信息,不看模型)、包裹式(递归特征消除,看模型但计算贵)、嵌入式(L1 正则、树模型的特征重要性,训练过程中完成)。特征重要性的局限也要知道:基于不纯度的重要性会偏向取值多的连续特征,用置换重要性更稳。
5. 易错点:五类一开口就被追问的失分答法
这一章不是新知识,是把前面几章里最容易答偏的地方单独拎出来。它们的共同特征是:听上去像答对了,但一追问就露馅。每条按"错在哪、为什么错、怎么答对"走一遍。
5.1 归一化类题:把标准化和归一化混着说
错在哪:一上来就说"归一化就是把数据缩放到 0 到 1",然后把 Z-score 标准化也叫归一化,接着又给树模型做归一化。
为什么错:归一化(min-max)和标准化(Z-score)是两种不同的线性变换,前者依赖最小最大值、对异常值极敏感,后者依赖均值和标准差、相对稳一些。在中文语境里"归一化"常被当统称用,但面试时如果不区分,后面所有讨论都站不住。至于树模型,分裂只看相对顺序,单调变换不改变顺序,做归一化纯属白费。
怎么答对:先给一句"我理解您说的归一化是指统一量纲,具体实现有 min-max 和 Z-score 两种,区别在于对异常值的敏感度",把口径统一掉;再说"这套处理对距离类和梯度类模型必要,对树模型无效",一句话把适用边界划出来。这个开场比直接讲公式稳得多。
5.2 过拟合类题:只会说"加数据"
错在哪:被问"过拟合怎么解决",答"多加点数据"就停了,或者把"欠拟合"和"过拟合"的表现说反。
为什么错:加数据是有效但最不可控的一招,面试官问这道题想听的是你对模型复杂度的理解,不是数据够不够。表现说反更致命,因为后面所有的解决手段都会用错方向——把过拟合当成欠拟合去加模型复杂度,等于火上浇油。
怎么答对:先给判断标准(训练误差小、验证误差大,学习曲线上两条线明显张开),再按数据、模型、训练、问题四个层级给手段,最后补一句"具体到这道题我会优先看验证集和训练集的差距有多大,差距小说明不是过拟合,加正则反而会更差"。有取舍的答案永远比清单式的答案得分高。
5.3 评估指标类题:把 AUC 当成准确率的同义词
错在哪:说"AUC 越高模型越准",或者说"AUC 对样本不平衡敏感"。
为什么错:AUC 衡量的是排序能力,不是分类准确率;它和阈值无关,因此天然不受类别先验的影响,真正受不平衡影响的是准确率、精确率这些依赖阈值的指标。把这两个概念混在一起,后面解释为什么用 AUC 而不用准确率就讲不通了。
怎么答对:一句话给物理意义(随机正负样本对,正样本得分更高的概率),再补一句"所以它对阈值不敏感、对排序敏感,类别不平衡时比准确率可靠"。如果面试官继续问"AUC 高但业务效果差是什么情况",答"线上用的是固定阈值,AUC 只保证整体排序,具体阈值下的精确率和召回率才决定业务效果",这道题就闭环了。
5.4 树模型对比题:只说"XGBoost 用了二阶导"
错在哪:被问 XGBoost 和 GBDT 的区别,只答"用了二阶泰勒展开",然后卡住。
为什么错:二阶展开只是手段,不是目的。它带来的三个实际差异——目标函数里显式加了树复杂度正则、损失函数被抽象成可替换的接口、分裂增益可以直接解析计算——才是面试官想听的。只说二阶导,等于只说了实现细节,没说清设计动机。
怎么答对:抓住四点说:目标函数加了正则项(叶子数和叶子权重平方和),控制模型复杂度;用二阶展开把损失抽象掉,任何二阶可导的损失都能套;分裂增益有闭式解,搜索更快;缺失值在分裂时自动学默认方向。最后补一句"它并行的是特征维度的分裂点搜索,树与树之间仍然是串行的",把最常见的误解提前挡掉。
5.5 检测类题:NMS 的阈值方向说反
错在哪:解释非极大值抑制时说"两个框 IoU 小于阈值就抑制掉",或者说"阈值越大保留的框越多,所以阈值越大越宽松"。
为什么错:NMS 的逻辑是保留得分最高的框,把与它重叠过多的框压掉,所以抑制条件是 IoU 大于阈值。阈值越大,抑制条件越难满足,保留的框反而越多,这个方向一说反,追问"阈值调大对小目标检测有什么影响"就必然答错。
怎么答对:按步骤讲:按得分排序、取最高分入保留列表、算它与剩余框的 IoU、IoU 超过阈值的丢弃、重复直到队列空。再补一句参数影响:阈值调低会压掉更多邻近框,密集场景容易漏检;阈值调高保留更多框,但重复框会变多。能顺带提一句 soft-NMS 用分数衰减替代直接删除,说明你知道这条路的演化方向。
6. 答题技巧:把 100 题压成「结论—机制—边界」三层
刷到一定量之后会发现,题目是无限的,答法是可以复用的。我自己后来固定用三层结构答所有技术题,第一层一句话给结论,第二层讲清机制,第三层主动划边界。三层各自有明确的时间预算:结论十秒,机制一分钟,边界二十秒。
| 题型 | 第一句话怎么说 | 收尾补什么 |
|---|---|---|
| 概念题(什么是 BN) | 一句话定义加它解决什么问题 | 训练和推理的行为不一致,这点最容易被追问 |
| 对比题(RF 与 GBDT) | 先给最本质的一条差异(并行 vs 串行、降方差 vs 降偏差) | 说清"什么场景我会选哪个" |
| 推导题(SVM、EM) | 先说目标函数和核心矛盾 | 说清"哪一步是近似、哪一步会损失最优性" |
| 代码题(判环、Top K) | 先说思路和复杂度,再动手写 | 说清"边界用例我怎么测" |
| 场景题(海量数据、冷启动) | 先问清约束(数据量、时延、冷热) | 说清"这个方案哪里会先崩" |
第三层是最容易被忽略、也最容易拉开差距的地方。面试官判断一个人是背题还是真懂,看的往往不是你答得多完整,而是你能不能说清"这个方法什么时候不适用"。比如讲完 XGBoost 的缺失值自动分裂,补一句"这是训练时学的默认方向,如果训练集里某特征完全没缺失,推理时出现缺失就只能是默认走一边,效果没有保证";讲完 AUC,补一句"它评估的是排序,线上阈值还得单独调"。这类话不用多,一道题一句就够。
还有一个习惯值得强制养成:写完代码先说边界,再等面试官问。链表题先说空链表和单节点,数组题先说空数组和全相同元素,DP 题先说初始化的含义。我吃过一次亏,白板上写快排写完自己觉得挺顺,面试官问"如果数组里全是相同元素呢",我才意识到自己的分区把等于基准的元素全放一边,复杂度直接退化到 O(n²)。从那以后我每次写完代码,都强制在心里过一遍"全相同、已有序、逆序、单元素、空输入"这五个用例,能过再说下一句。这个动作看起来笨,但它救回来的分比多刷二十道题多。
题目本身不是目的,能把这 100 道题背后的考点分布、答题骨架和边界意识迁移到没见过的题上,才是这份题库真正的用法。题库里每道题都配了完整解析,章节顺序与在线题库的题号一一对应,方便你按模块对着刷、对着改自己的答法。希望帮到你。
本文还有配套的精品资源,点击获取