简介这是一份面向准备AI相关岗位笔试与面试的学习者整理的名企面试题精选PDF侧重计算机语言基础与常见工程问题覆盖C调用C函数时的链接规则、多线程与多任务区别、Java类初始化顺序、死锁构造、集合遍历删除、Linux CPU与进程排查、top内存指标、iptables访问控制以及Python列表去重、lambda、快排、回文判断等高频考点。资料包共1个PDF文件压缩包约49.22MB内容按题目加解析组织部分题目配有代码片段或命令示例目录从语言基础延伸到算法与大数据、机器学习、深度学习及应用方向便于按知识点集中刷题和面试前快速回顾。已有251人学习题解通常先点明原理再给出可操作的解决方式或排错思路适合校招、社招及转岗面试者用于自测与查漏补缺。1. 从4000到100AI面试题库的压缩为什么反而更有效AI面试的题库很容易给人一种错觉题越多覆盖越全刷完就稳了。但真正面过几轮的人会发现面试官反复问的往往集中在几十个根问题上剩下的都是这些根问题的变体。这份《名企AI面试100题》做的最反直觉的一件事是从近4000道题里砍到100道却仍然覆盖计算机语言基础、算法与大数据、机器学习、深度学习、应用方向五大块。压缩不是随机抽样而是沿着知识依赖链往上收一个extern C的问题背后是链接器符号匹配一个XGBoost泰勒展开的问题背后是目标函数二阶近似。把根节点搞懂比刷完4000道答案更耐用。下面从链接器开始逐层拆开这些题到底在考什么。2. extern C与链接符号C调用C函数时编译器到底改了什么2.1 名称修饰函数重载在符号表里留下的痕迹C支持函数重载编译器必须让同名但参数不同的函数在符号表里区分开这套机制叫名称修饰name mangling。C语言没有重载函数名编译后基本保持原名不同编译器可能加下划线前缀。链接器不认语法只认符号名所以C调用C函数时如果不管符号名链接阶段就会报未定义引用。常见的实现思路是GCC 的 Itanium ABI 把int add(int, int)修饰成_Z3addii把float add(float, float)修饰成_Z3addffMSVC 则生成?addYAHHHZ这类名字。名字里编码了函数名、参数个数、参数类型甚至命名空间。C编译器生成的符号通常就是add或_add。两边对不上链接器自然找不到。// C 侧声明两个重载函数 int add(int a, int b); float add(float a, float b); // 编译后符号GCC Itanium ABI 近似 // _Z3addii // _Z3addff上面这段代码没有加extern C所以两个add都被修饰。如果C文件里只提供了int add(int, int)C侧链接时找的是_Z3addii而C目标文件里只有add链接器就会报undefined reference to add(int, int)。extern C的作用就是告诉C编译器这段声明按C的规则生成符号不要修饰。2.2 链接阶段从目标文件到可执行文件符号表怎么对上编译单元先变成目标文件里面有一张符号表记录本文件定义了哪些符号、引用了哪些未定义符号。链接器的任务是把多个目标文件的符号表拼起来把未定义符号解析到定义符号的地址上。这个过程不看源代码只看符号名和重定位信息。所以混合编程的关键不在语法而在符号名一致。C侧用extern C包住C头文件C编译器就会为这些函数生成未修饰的符号链接时正好命中C目标文件里的同名符号。// cppFile.cppC 调用 C 函数 extern C { #include cExample.h // C 头文件里只有普通声明 } int main() { add(2, 3); // 链接器找的是未修饰的 add return 0; }/* cExample.hC 头文件不能写 extern C */ #ifndef C_EXAMPLE_H #define C_EXAMPLE_H extern int add(int x, int y); /* extern 只表示可见性不改变符号名 */ #endif这里有两个容易混淆的关键字extern管的是可见性告诉编译器这个函数或变量定义在别的模块extern C管的是链接方式告诉C编译器按C规则生成符号。C头文件里只能写extern因为C编译器不认识extern C写了会直接语法错误。C侧包含C头文件时才需要在外层包一层extern C。2.3 反向混编C调C时为什么头文件要加 extern C反过来C代码调用C函数时C头文件里必须把函数声明成extern C这样C编译器生成未修饰符号C编译器才能按同名符号去链接。但C文件里不能直接#include这个头文件因为C编译器看到extern C会报错。常见做法是C文件里手写一份extern int add(int, int);声明或者用条件编译把extern C包起来。/* cFile.cC 调用 C 函数 */ extern int add(int x, int y); /* 只能写 extern不能写 extern C */ int main(void) { add(2, 3); return 0; }// cppExample.hC 头文件 #ifndef CPP_EXAMPLE_H #define CPP_EXAMPLE_H extern C int add(int x, int y); /* 生成未修饰符号 */ #endif这套双向写法的动机很实在C和C的符号规则不同但链接器只认符号名。extern C不是让函数可以被外部调用的开关extern才是管可见性的它真正解决的是符号名不一致导致的链接失败。面试里问这道题考的是你有没有把编译期和链接期分开看。2.4 Java类初始化 与 的两阶段Java的初始化顺序题看起来是背结论底层其实是两个不同的阶段。类加载的最后一步是初始化JVM执行clinit方法里面按源代码文本顺序收集静态字段赋值和静态块对象创建时执行init方法先调父类构造再执行实例字段赋值和实例块最后执行自己的构造体。class Base { static { System.out.println(Base static block); } private static String v init(Base static field); { System.out.println(Base instance block); } private String name init(Base instance field); Base() { System.out.println(Base constructor); } static String init(String s) { System.out.println(s); return s; } }这段代码输出顺序是Base static block、Base static field、Base instance block、Base instance field、Base constructor。静态部分只在类第一次初始化时执行一次实例部分每次new都执行。如果子类继承Base父类的clinit先于子类父类的init也先于子类。很多题会在这个顺序上挖坑比如静态块里引用后面才声明的静态字段拿到的是默认值null因为字段声明会被提到前面但赋值按文本顺序执行。2.5 死锁与fail-fast两个运行时的资源竞争与一致性检查死锁题的核心不是背四个条件而是能写出一个稳定复现的死锁。两个线程以相反顺序拿两把锁各自持有第一把、等待第二把循环等待形成谁也走不下去。Object lockA new Object(); Object lockB new Object(); Thread t1 new Thread(() - { synchronized (lockA) { sleep(100); // 放大交错窗口 synchronized (lockB) { } // 等 t2 释放 lockB } }); Thread t2 new Thread(() - { synchronized (lockB) { sleep(100); synchronized (lockA) { } // 等 t1 释放 lockA } });死锁的四个必要条件是互斥、占有且等待、不可抢占、循环等待。这段代码四个条件全占锁互斥、每个线程持有一把等另一把、锁不能强行剥夺、A等B且B等A。线上排查时jstack能看到两个线程互相BLOCKED并且各自持有对方需要的锁。面试里如果只背条件写不出代码通常会被追问你怎么保证它每次都死。另一个运行时一致性检查是ArrayList的fail-fast。for-each底层走的是迭代器迭代器在创建时记录expectedModCount modCount每次next()检查两者是否相等。如果直接在循环里调list.remove()集合的modCount会加一迭代器不知情下一次next()就抛ConcurrentModificationException。ListInteger list new ArrayList(Arrays.asList(1, 2, 3, 4)); for (Integer x : list) { if (x 2) { list.remove(x); // modCount迭代器 expectedModCount 没变 } }正确写法是用iterator.remove()它会同步更新expectedModCount或者用list.removeIf(x - x 2)。注意删倒数第二个元素时可能不抛异常因为循环刚好结束没机会再调next()。这种偶发不报错最容易让人误以为代码没问题到线上数据量一大就翻车。3. 算法与大数据的底层套路快慢指针、B树分裂与分治Top K3.1 链表判环Floyd判圈为什么快慢指针一定会相遇单链表判环的经典解法是快慢指针慢指针走一步快指针走两步。如果存在环快指针先进入环慢指针后进入之后快指针每轮比慢指针多走一步两者在环上的距离每次缩小一最多经过环长那么多轮就会相遇。def has_cycle(head): slow fast head while fast and fast.next: slow slow.next # 慢指针一步 fast fast.next.next # 快指针两步 if slow is fast: # 相遇即有环 return True return False # 快指针走到 None无环这里的关键是步差为一保证了相遇而不是靠运气。如果快指针走三步步差为二在某些环长下可能跳过慢指针需要更复杂的分析。面试里常追问怎么找环的入口相遇后把一个指针放回头节点两个指针都每次走一步再次相遇的位置就是入口。推导依赖的是相遇点到入口的距离与头节点到入口的距离模环长同余。3.2 B树插入与删除分裂、借位、合并的触发条件B树是一种多路平衡搜索树每个节点最多有m个子节点、m-1个关键字。插入时先找到叶子节点放入关键字如果节点关键字数超过m-1就把中间关键字上移到父节点左右两半拆成两个节点。删除时如果节点关键字数少于下限先向兄弟借一个兄弟不够借就与兄弟合并合并可能继续向上触发分裂的逆过程。# B树插入的简化逻辑阶为 m def insert(node, key): if node.is_leaf: node.keys.append(key) node.keys.sort() if len(node.keys) m - 1: split(node) # 中间关键字上移节点一分为二 else: child find_child(node, key) insert(child, key) def split(node): mid len(node.keys) // 2 up_key node.keys[mid] left Node(node.keys[:mid]) right Node(node.keys[mid 1:]) parent node.parent parent.insert_child(up_key, left, right) if len(parent.keys) m - 1: split(parent) # 父节点溢出继续向上分裂B树的设计动机是减少磁盘I/O每个节点大小对齐一个磁盘页树高被压得很低一次查找只需要几次页读取。插入分裂和删除合并都是为了维持所有叶子在同一层这个平衡条件。面试里问B树插入删除重点不是背步骤而是说清为什么分裂是向上传的——因为节点溢出破坏了关键字数量约束必须把中间元素交给父节点让父节点重新分配左右子树的取值范围。3.3 海量日志Top IP分治哈希与小顶堆的两次收敛从海量日志里找访问次数最多的IP单机内存放不下全量数据常见思路是分治加堆。先对每条日志里的IP做哈希按hash(ip) % N把日志分散到N个小文件同一个IP一定会落到同一个文件然后每个小文件单独用哈希表统计词频最后用一个小顶堆在所有文件的统计结果里取Top K。import heapq def top_k(counter, k): heap [] for ip, cnt in counter.items(): if len(heap) k: heapq.heappush(heap, (cnt, ip)) # 小顶堆堆顶是当前第 k 大 elif cnt heap[0][0]: heapq.heapreplace(heap, (cnt, ip)) # 替换堆顶维持 k 个 return [ip for cnt, ip in sorted(heap, reverseTrue)]分治哈希解决的是内存上限小顶堆解决的是不全量排序。如果单个小文件仍然太大可以继续二次哈希拆分。面试里常追问为什么不用大顶堆大顶堆需要把所有数据都压进去内存扛不住小顶堆只维护K个元素空间是O(K)。另一个坑是哈希函数要足够均匀否则某个文件会倾斜分治就失去意义。4. 机器学习与深度学习的机制题从SVM对偶到Transformer注意力4.1 SVM为什么要把原问题转成对偶形式SVM的原问题是带约束的凸二次规划最小化1/2 ||w||^2约束每个样本满足y_i(w·x_i b) 1。直接解这个约束优化不方便于是引入拉格朗日乘子把约束吸收进目标函数再根据强对偶性转成对偶问题。对偶形式里样本只以内积x_i·x_j出现这才方便把内积替换成核函数把线性SVM扩展到非线性场景。对偶问题长这样最大化sum(alpha_i) - 1/2 sum sum(alpha_i alpha_j y_i y_j x_i·x_j)约束sum(alpha_i y_i) 0且alpha_i 0软间隔时上界为C。KKT条件给出一个重要结论只有alpha_i 0的样本才影响决策边界这些样本就是支持向量alpha_i 0的样本在间隔外对模型没有贡献。面试里问SVM为什么能处理非线性答案就落在核函数替换内积这一步而不是用了核技巧这种空话。4.2 XGBoost二阶泰勒展开目标函数近似与增益计算XGBoost的目标函数由损失项和正则项组成每轮加一棵新树去拟合当前残差。它没有直接优化原始损失而是对损失做二阶泰勒展开用一阶梯度g_i和二阶梯度h_i近似目标函数。这样做的好处是不管损失函数长什么样只要二阶可导就能统一成二次函数形式叶子权重有解析解。# 叶子权重解析解 w_j -G_j / (H_j lambda) # 分裂增益 gain 0.5 * ( G_L**2 / (H_L lambda) G_R**2 / (H_R lambda) - (G_L G_R)**2 / (H_L H_R lambda) ) - gamma其中G_j是叶子j上样本一阶梯度和H_j是二阶梯度和lambda控制叶子权重收缩gamma控制分裂门槛。一阶信息只能告诉你往哪走二阶信息告诉你走多远所以XGBoost比传统GBDT收敛更快、更稳。面试里追问泰勒展开的优势在哪要答到把损失函数的优化转化为叶子权重的解析解避免每轮做数值线搜索。4.3 树模型与归一化为什么分裂点不关心特征尺度树模型的分裂只看特征排序对某个特征排好序枚举切分点计算左右子集的 impurity 下降。对特征做单调变换比如归一化、标准化、取对数不改变样本之间的相对顺序所以分裂点不变树结构也不变。这就是树形结构不需要归一化的底层原因。线性模型和距离模型则不同。线性模型的系数大小和特征尺度直接相关尺度不统一会导致正则项惩罚不公平KNN、SVM、KMeans依赖距离量纲不同会让大数值特征主导距离计算。面试里常把归一化和标准化混在一起问归一化通常指缩放到[0,1]标准化指减去均值除以标准差。两者都依赖全局统计量推理时必须复用训练集的均值和方差不能在各批次里重新算。4.4 梯度消失与BN链式乘积与训练推理不一致梯度消失的根源是链式法则的连乘。以sigmoid为例导数最大只有0.25多层反向传播时梯度不断乘以小于1的数层数一多就趋近于零。ReLU的正区间导数为1负区间为0缓解了这个问题残差连接则给梯度提供了一条常数通路让深层网络至少能学到恒等映射。Batch Normalization的做法是在每个mini-batch上算均值和方差把激活值标准化再乘可学习的gamma、加beta。训练时用当前batch的统计量推理时用训练阶段累积的滑动平均均值和方差。这个差异是很多线上事故的来源训练和推理行为不一致如果推理时误用了batch统计单条样本的方差为零输出会直接崩掉。训练mu_B mean(x), sigma_B^2 var(x) x_hat (x - mu_B) / sqrt(sigma_B^2 eps) y gamma * x_hat beta 推理用 running_mean / running_var 代替 mu_B / sigma_B^2BN能加速收敛一个常见解释是它把每层输入拉回相对稳定的分布减少了内部协变量偏移另一个更严格的角度是它改变了损失曲面的平滑性允许更大的学习率。面试里能把训练和推理的差异讲清楚比背防止梯度消失更有说服力。4.5 Transformer与NMS注意力权重和后处理抑制Transformer的核心是自注意力每个位置产生Query、Key、Value三个向量用Query和所有Key做点积得到相关性分数除以sqrt(d_k)防止点积过大导致softmax饱和再对Value加权求和。多头的意义是让模型在不同子空间里学不同的相关性模式。NMS是目标检测的后处理按置信度从高到低排序取最高分框删掉与它IoU超过阈值的框重复直到没有框可删。它解决的是同一个物体被多个锚框重复检测的问题。def nms(boxes, scores, iou_thresh): order scores.argsort()[::-1] # 按置信度降序 keep [] while order.size 0: i order[0] keep.append(i) # 保留当前最高分框 ious compute_iou(boxes[i], boxes[order[1:]]) order order[1:][ious iou_thresh] # 抑制重叠过大的框 return keepNMS的取舍在于阈值阈值低会误删相邻物体阈值高会留下重复框。工业界后来出现Soft-NMS、可学习的NMS都是为了缓解这个硬阈值带来的问题。面试里问one-stage和two-stage的区别落到机制上就是two-stage先出候选区域再分类回归精度高但慢one-stage直接在特征图上密集预测速度快但正负样本极不均衡需要Focal Loss这类手段来缓解。5. 刷题与理解中的五个翻车现场从符号名到集合迭代器5.1 以为 extern C 是导出函数的开关现象在C头文件里给函数加了extern C以为这样别的模块就能调用结果C文件那边链接时报找不到符号。原因把可见性和链接方式混为一谈。extern才是声明这个符号定义在别处extern C只改变符号名的生成规则。如果C文件里没有对应的未修饰符号加多少extern C都没用。解决先确认C目标文件里到底生成了什么符号Linux下用nm看目标文件符号表确认是add还是_Z3addii再检查C侧是否用extern C包住了声明C侧是否只写了extern。5.2 Java静态初始化顺序背了结论写代码还是错现象静态块里打印一个静态字段得到null但字段明明在下面赋了值。原因静态字段和静态块按源代码文本顺序执行字段声明会被提到前面但赋值语句留在原位。静态块在赋值之前执行自然读到默认值。解决把依赖的静态初始化放到静态块里或者把静态块放到字段声明之后。更稳妥的做法是避免在静态块里依赖后面的静态字段用显式的初始化方法按顺序调用。5.3 死锁代码单线程跑永远测不出来现象本地跑死锁示例程序正常结束以为死锁不会发生。原因死锁需要两个线程并发且交错到特定顺序。单线程或者没有强制交错的调度两个线程可能一前一后跑完根本形不成循环等待。解决用两个线程加CountDownLatch强制交错确保两个线程都拿到第一把锁之后再请求第二把线上排查用jstack看线程状态和持有锁别靠猜。5.4 ArrayList在for循环里删除偶发异常现象删一个元素时没报错删多个元素时抛ConcurrentModificationException换台机器或换个数据量又好了。原因for-each走迭代器迭代器的expectedModCount和集合的modCount在next()时校验。删最后一个或倒数第二个元素时循环可能刚好结束没机会触发校验删中间元素则一定触发。解决用iterator.remove()或者removeIf()或者改用CopyOnWriteArrayList。不要用索引for循环边删边遍历索引会错位。5.5 归一化和标准化混用树模型上做归一化白费功夫现象给树模型做了归一化精度没提升反而多了预处理开销给线性模型只做了归一化没做标准化正则项效果不对。原因树模型只看特征排序单调变换不影响分裂点线性模型和距离模型对尺度敏感需要标准化或归一化但两者目标不同混用会破坏正则项对系数的公平惩罚。解决树模型可以跳过归一化线性模型、SVM、KNN、KMeans优先标准化需要把特征压到固定区间时用归一化。推理阶段必须复用训练集的统计量不能在线重算。5.6 海量Top K直接全量排序内存直接爆现象日志文件几个G读进内存排序找Top IP程序直接OOM。原因全量排序需要把所有数据放进内存空间复杂度O(N)数据量一上来就扛不住而且排序本身是O(N log N)找Top K没必要。解决先哈希分片保证同一IP落同一文件每个文件单独统计词频再用小顶堆维护Top K空间降到O(K)。分片数要足够让单文件能放进内存哈希函数要均匀避免数据倾斜。6. 题库组织的取舍为什么按依赖链排序比按难度排序更耐用这份手册的章节顺序是计算机语言基础、算法和大数据、机器学习、深度学习、应用方向题目顺序还和在线题库页面保持一致。这个安排看起来朴素背后有一个明确的取舍按依赖链排序而不是按难度排序。按难度排序的问题是难度是主观且不稳定的。一道SVM对偶题对做过凸优化的人是中等对没学过拉格朗日的人是困难把难题放前面读者会被卡住把简单题放前面读者又不知道后面要用到前面的哪个结论。按依赖链排序则不同语言基础里的链接符号、类加载顺序、集合迭代器是后面算法和工程题的地基算法里的分治和堆是海量数据处理的前提机器学习里的梯度、正则、过拟合是深度学习调参和BN理解的前置。读的时候可能觉得前面慢但到后面会发现少了很多这里为什么这样的卡顿。另一个取舍是题目顺序和在线题库页面一致。这牺牲了一部分按学习路径重新编排的自由换来的是可跳转、可讨论、可对照。读者在手册里看到某道题能顺着题号回到对应页面看更多讨论和勘误。手册只有100题但真实笔试面试不会局限在这100题所以它更像一个索引而不是一个封闭的题集。如果让我给一条复习路径我会先把语言基础和算法两块过一遍确保链接、初始化、集合、链表、树、堆这些机制能自己讲清楚再进机器学习和深度学习每道题都追问这个设计解决了什么问题、不这么设计会怎样最后用应用方向的题做串联比如目标检测里的NMS和IoU能同时串起CNN、后处理和评价指标。刷题时不要只看答案答案是对结果的描述机制才是可迁移的部分。从那以后我每次带人复习都会强制让他先讲一道题的机制再对答案讲不出来就回去补前置。这份手册的价值不在于100这个数字而在于它把近4000道题里最常被追问的根节点挑了出来剩下的靠读者自己往下挖。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?