🎯 参考回答 · 算法 / CS 基础篇(17 题 · C 级但不过不行)
🎯 参考回答 · 算法 / CS 基础篇(17 题 · C 级但不过不行)
📅 生成日期:2026-06-16
📚 数据来源:工作区面经 + 秋招复习笔记 + 经典八股整理
🎯 适用:AI 应用开发 / Agent 开发 面试 — 非算法岗但面试必考
⚠️ 重要提醒
这部分的题目不是靠”背”就能过的——面试官一看就知道你有没有真理解。每道题不仅要会回答,还要能应对追问。
对于 Agent 岗位的面试,这部分题目的重要性排序:
- 高频追击区(面试官最爱考的):线程池、HashMap、单例模式、SSE vs WebSocket、注意力机制
- 必保底区(不过不行):合并有序数组、字符串相加、JVM类加载、HTTP 版本区别
- 加分项区(体现深度):手写死锁、MySQL 两阶段提交、AQS
一、🖥 手撕代码 · 高频题(5 题)
题1:单例模式(双检锁 + volatile)
参考回答(面试口语化 + 手写代码):
“单例模式确保一个类只有一个实例,双检锁(Double-Checked Locking)是线程安全的懒加载实现。”
public class Singleton { // volatile 防止指令重排序导致其他线程拿到未初始化完成的对象 private static volatile Singleton instance;
private Singleton() {} // 私有构造
public static Singleton getInstance() { if (instance == null) { // 第一重检查:避免不必要的同步 synchronized (Singleton.class) { if (instance == null) { // 第二重检查:确保只有一个线程创建实例 instance = new Singleton(); // new Singleton() 的三步: // 1. 分配内存空间 // 2. 初始化对象 // 3. 将引用指向内存地址 // volatile 禁止 2 和 3 重排序 } } } return instance; }}面试追问准备:
- 为什么用 volatile? → 防止
new Singleton()的指令重排序,避免拿到”半初始化”的对象 - 为什么两次检查? → 第一次避免每次都进入同步块(性能),第二次确保只有一个线程创建
- 还有哪些实现方式? → 饿汉式(类加载时初始化)、静态内部类(利用类加载机制)、枚举(最简洁安全)
题2:合并两个有序数组
参考回答(面试口语化 + 手写代码):
“经典的双指针从后往前问题,避免覆盖。”
public void merge(int[] nums1, int m, int[] nums2, int n) { // 从后往前放最大的元素 int p1 = m - 1; // nums1 有效元素的最后一个 int p2 = n - 1; // nums2 的最后一个 int p = m + n - 1; // 合起来的总长度
while (p2 >= 0) { if (p1 >= 0 && nums1[p1] > nums2[p2]) { nums1[p] = nums1[p1]; p1--; } else { nums1[p] = nums2[p2]; p2--; } p--; }}面试追问准备:
- 为什么从后往前? → nums1 后面有空位,从后往前不会被覆盖
- 时间复杂度? → O(m+n)
- 空间复杂度? → O(1),原地修改
字节面试经验:面试官说”好,你刷过,下一个追问”——所以答完要准备好被追问其他不刷题就不知道的问题。
题3:字符串相加
参考回答(面试口语化 + 手写代码):
“大数加法,用模拟竖式加法的方式,从个位开始逐位相加。”
def addStrings(num1: str, num2: str) -> str: i, j = len(num1) - 1, len(num2) - 1 carry = 0 res = []
while i >= 0 or j >= 0 or carry: x = int(num1[i]) if i >= 0 else 0 y = int(num2[j]) if j >= 0 else 0 sum_val = x + y + carry carry = sum_val // 10 # 进位 res.append(str(sum_val % 10)) # 当前位 i -= 1 j -= 1
return ''.join(reversed(res))面试追问准备:
- 不能把字符串转整数直接加吗? → 因为字符串可能很长,超过整数范围(大数问题)
- 时间复杂度? → O(max(m, n))
- 能处理负数吗? → 需要额外判断符号位,逻辑更复杂
题4:手写一个死锁
参考回答(面试口语化 + 手写代码):
“死锁产生需要四个条件:互斥、持有并等待、不可剥夺、循环等待。破坏任何一个就能解除死锁。手写一个死锁演示:”
public class DeadlockDemo { private static final Object lock1 = new Object(); private static final Object lock2 = new Object();
public static void main(String[] args) { // 线程1:先拿 lock1,再拿 lock2 Thread t1 = new Thread(() -> { synchronized (lock1) { System.out.println("Thread1: 拿到了 lock1"); try { Thread.sleep(100); } catch (Exception e) {} synchronized (lock2) { System.out.println("Thread1: 拿到了 lock2"); } } });
// 线程2:先拿 lock2,再拿 lock1(和线程1顺序相反,死锁) Thread t2 = new Thread(() -> { synchronized (lock2) { System.out.println("Thread2: 拿到了 lock2"); try { Thread.sleep(100); } catch (Exception e) {} synchronized (lock1) { System.out.println("Thread2: 拿到了 lock1"); } } });
t1.start(); t2.start(); }}面试追问准备:
- 怎么避免死锁? → 破坏四个条件之一,最常用的是”破坏循环等待”——所有线程按固定顺序拿锁
- 怎么排查死锁? →
jstack看线程 dump,或使用jconsole/VisualVM检测 - Java 层面有自动解除死锁的机制吗? →
Lock接口的tryLock(long timeout)尝试拿锁,拿不到就释放已持有的锁
题5:Todo Agent 实现(AI Coding 环节)
参考回答(面试口语化 + 思路):
“Todo Agent 是一个任务管理 + 自动执行的 Agent 系统,用户说’帮我整理代码,写测试,然后部署’,Agent 分析需求→拆解任务→依次执行→反馈结果。”
核心设计思路(代码不是关键,架构思路是):
1. 任务解析层:理解用户输入,拆分成子任务列表2. 任务调度层:决定执行顺序(串行/并行/依赖关系)3. 执行层:每个子任务 → 工具调用 → 结果校验4. 状态管理层:记录任务进度,支持中断恢复5. 反馈层:执行结果总结,通知用户能力边界:
- 能做什么:自动化执行多步骤任务(编码→测试→部署)
- 不能做什么:无法自动决策复杂需求,需要人工确认关键节点
加分回答:把 Todo Agent 和你做的项目的 Agent 架构联系起来——“我们在 code agent 里已经实现了类似的任务拆解和调度机制……”
二、☕ Java / 语言基础 · 高频八股(8 题)
题6:线程池核心参数,怎么设计一个合适的线程池?
参考回答(面试口语化——不能只会背,要能设计):
核心参数(7个):
new ThreadPoolExecutor( corePoolSize, // 核心线程数(一直存活) maximumPoolSize, // 最大线程数 keepAliveTime, // 空闲线程存活时间 TimeUnit.SECONDS, // 时间单位 workQueue, // 阻塞队列(存放等待任务) threadFactory, // 线程工厂(命名规则) handler // 拒绝策略);怎么设计? 两个关键决策:
① 核心线程数 = CPU 密集型还是 IO 密集型?
- CPU 密集型(计算为主):核心线程数 = CPU 核数 + 1
- IO 密集型(网络/磁盘等待多):核心线程数 = CPU 核数 × 2(或者更多)
- 混合型:拆成不同线程池分别管理
② 拒绝策略怎么选?
AbortPolicy(默认):抛异常——适合不能丢任务的场景CallerRunsPolicy:让提交任务的线程自己跑——适合”慢下来也不要丢”DiscardPolicy/DiscardOldestPolicy:丢任务——适合可丢的场景
面试加分:结合实际场景——“我们 code agent 的 LLM 调用是 IO 密集型(网络请求+等待返回),所以线程池设的比较大。但工具调用中如果包含本地计算,走独立的小线程池。“
题7:HashMap 底层结构,冲突怎么处理?
参考回答(面试口语化):
底层结构:
- JDK 1.8 之前:数组 + 链表
- JDK 1.8 之后:数组 + 链表 / 红黑树
put(key, value) 过程:1. 对 key 的 hashCode() 做二次 hash(扰动函数)2. 用 hash & (n-1) 定位到数组槽位3. 如果槽位为空,直接放4. 如果槽位不为空 → 发生 hash 冲突 → 用链表/红黑树处理冲突处理:拉链法(链地址法)
- 链表法:冲突元素挂链表上,遍历链表查找
- 当链表长度 ≥ 8 且数组长度 ≥ 64 → 转红黑树(O(n) → O(log n))
- 当红黑树节点 ≤ 6 → 转回链表
扩容:
- 默认负载因子 0.75——空间与时间的权衡
- 扩容时容量翻倍,所有元素重新计算位置(rehash)
- 扩容是性能瓶颈,所以预估容量设初始值很重要
面试加分追问:HashMap 线程安全吗?→ 不安全,并发 put 可能导致死循环(JDK1.7 头插法)或数据覆盖。用 ConcurrentHashMap(分段锁/CAS+ synchronized)。
题8:JVM 类加载机制(三大步骤)
参考回答(面试口语化——字节高频题):
类加载分为三大步:加载 → 连接 → 初始化
① 加载(Loading)
- 通过类的全限定名获取二进制字节流
- 将字节流的方法区数据结构存储
- 在内存中生成
java.lang.Class对象
② 连接(Linking)
- 验证:确保字节流符合 JVM 规范(文件格式、元数据、字节码、符号引用验证)
- 准备:为类变量(static)分配内存并赋默认值(int=0, boolean=false, 引用=)
- 解析:将常量池中的符号引用替换为直接引用
③ 初始化(Initialization)
- 执行
<clinit>()方法——收集所有类变量的赋值动作和静态代码块 - 父类先初始化,子类后初始化
双亲委派模型:
类加载器层次:Bootstrap → Extension → Application → 自定义加载请求先委派给父加载器,父加载器能加载就不让子加载器加载面试加分:打破双亲委派——自定义类加载器重写
loadClass()而不是findClass()。
题9:MySQL undo log / redo log / binlog + 两阶段提交
参考回答(面试口语化——字节经典组合考法):
三种日志的角色:
| 日志 | 归属 | 作用 |
|---|---|---|
| undo log | InnoDB 引擎 | 事务回滚 + MVCC(多版本并发控制) |
| redo log | InnoDB 引擎 | 崩溃恢复——提交了的修改不丢(WAL:Write-Ahead Logging) |
| binlog | MySQL Server 层 | 主从复制 + 数据恢复 |
两阶段提交(保证 redo log 和 binlog 一致性):
事务提交时:第一阶段(Prepare): 1. redo log 写入 Prepare 状态 2. binlog 写入(此时 binlog 完整记录了事务)
第二阶段(Commit): 3. redo log 写入 Commit 状态 4. 事务正式提交为什么需要两阶段? → 防止 redo log 和 binlog 不一致。
- 如果先写 redo log 再写 binlog,redo log 写完 binlog 没写完时 crash,恢复时用 redo log 恢复了事务但 binlog 没有,主从复制就丢了数据
- 两阶段提交保证了两份日志的原子性
题10:并发:synchronized / ReentrantLock / CAS / AQS
参考回答(面试口语化——字节/阿里高频八股):
synchronized(JVM 关键字)
- JDK 1.6 后优化:偏向锁 → 轻量级锁 → 重量级锁(锁升级,单向不可逆)
- 自动加锁解锁,发生异常自动释放
- 比较重,但有 JVM 层面的优化
ReentrantLock(JDK 工具类)
- 更灵活:支持公平锁/非公平锁、可中断、可限时
tryLock() - 可以有多个等待条件(Condition)
- 必须手动加锁解锁(
finally中释放),不然会导致死锁
CAS(Compare-And-Swap,CPU 原子指令)
- 原子操作:比较内存值和期望值,相等则更新为新值
- 优点:无锁,性能好
- 缺点:ABA 问题、自旋消耗 CPU、只能保证一个共享变量的原子性
AQS(AbstractQueuedSynchronizer,抽象队列同步器)
- ReentrantLock、Semaphore、CountDownLatch 的基础
- 核心:一个 volatile int state(同步状态)+ CLH 双向队列(等待线程)
- 模板方法模式:子类实现
tryAcquire/tryRelease
面试加分追问:synchronized 和 ReentrantLock 性能对比?→ 低竞争下 synchronized 更优(有 JIT 优化),高竞争下 ReentrantLock 更优(可中断)。
题11:HTTP 1.0 / 2.0 / 3.0 区别
参考回答(面试口语化——滴滴常问基础):
| 版本 | 核心特点 | 主要问题 |
|---|---|---|
| HTTP/1.0 | 每个请求新建 TCP 连接,用完就断开 | 连接无法复用,效率极低 |
| HTTP/1.1 | 长连接(keep-alive)、管道化(pipelining)、分块传输、Host 头 | 管道化有队头阻塞(HOL blocking)——前一个请求没返回,后面的不能发 |
| HTTP/2.0 | 多路复用(一个 TCP 连接发多个请求)、二进制帧、头部压缩(HPACK)、服务端推送 | 仍有 TCP 级别的队头阻塞(丢包影响整个连接) |
| HTTP/3.0 | 基于 QUIC(基于 UDP),解决队头阻塞、0-RTT 连接建立 | 部署还不广泛,UDP 可能被防火墙拦截 |
面试加分:SSE 和 WebSocket 的对比是同一个体系的,可以关联起来讲。
题12:Python 多进程/多线程/协程区别
参考回答(面试口语化——滴滴常问):
| 机制 | 特点 | 适用场景 |
|---|---|---|
| 多进程 | 每个进程独立内存,开销大,通过 multiprocessing 实现 | CPU 密集型(计算密集型),利用多核 |
| 多线程 | 共享内存,Python 有 GIL(全局解释器锁),同一时刻只有一个线程执行 Python 字节码 | IO 密集型(网络请求、文件读写),GIL 在 IO 等待时释放 |
| 协程 | 用户态调度,单线程并发,通过 async/await 实现 | 高并发 IO 场景(Web 服务器、爬虫) |
常见问题:
- Python 多线程是假的? → 对 CPU 密集型是假的(GIL 限制),对 IO 密集型是真的(IO 等待时释放 GIL)
- 协程和线程的区别? → 协程是用户态切换(几十纳秒),线程是内核态切换(微秒级)。协程更轻量,但不能利用多核
题13:SSE 和 WebSocket 区别
参考回答(面试口语化——FN/HQ公司高频题):
| 对比维度 | SSE(Server-Sent Events) | WebSocket |
|---|---|---|
| 方向 | 单向:服务端 → 客户端 | 双向:客户端 ↔ 服务端 |
| 协议 | 基于 HTTP | 独立的 WebSocket 协议(ws://) |
| 自动重连 | ✅ 原生支持(EventSource 自动重连) | ❌ 需要自己实现 |
| 数据格式 | 文本(通常是 JSON) | 文本+二进制 |
| 浏览器支持 | 大部分浏览器支持 | 所有现代浏览器支持 |
| 适用场景 | 流式输出(LLM 逐字返回)、通知推送 | 聊天、实时协作、游戏 |
在 Agent 开发中的应用:
- SSE:LLM 流式输出——用户能看到 Agent”边想边说”的效果
- WebSocket:Agent 间的实时通信、协作场景
面试追问:SSE 能传二进制吗?→ 原生不行(
EventSource),但可以用 base64 编码后传。不过既然要传二进制,为什么不用 WebSocket?
三、🧠 算法/ML 基础 · 非训练岗也要知道(4 题)
题14:注意力机制(QKV)
参考回答(面试口语化——应用岗也要掌握基础):
注意力机制就是**“从大量信息中聚焦重要信息”**。
公式:Attention(Q, K, V) = softmax(QKᵀ / √dₖ) V
三个角色:
- Q(Query,查询):当前要关注什么(比如当前词)
- K(Key,键):所有候选信息的标识(比如每个词的索引)
- V(Value,值):候选信息的实际内容(比如每个词的嵌入)
计算流程:
1. Q 和每个 K 做点积 → 得到相似度分数2. 除以 √dₖ(缩放,防止点积方差过大导致 softmax 梯度太小)3. softmax 归一化 → 得到注意力权重4. 权重 × V → 加权求和得到 Attention 输出面试加分:用直觉解释——“就像你在教室里找朋友(Q 是’你朋友的长相特征’),你跟每个人对比相似度(Q×K),最像的几个重点关注(softmax 权重),你把他们的特征综合起来形成最终印象(权重×V)“。
题15:位置编码(绝对/相对/RoPE)
参考回答(面试口语化——理解 RoPE 为什么是主流):
为什么需要位置编码? → Transformer 的 Self-Attention 是置换不变的(不知道词的先后顺序),所以需要显式告诉模型”词的位置信息”。
三种方案:
① 绝对位置编码(Sinusoidal)
- Transformer 原版,用 sin/cos 函数生成固定位置编码
- 优点:无需学习,泛化到任意长度
- 缺点:每个位置固定,不能表达相对位置关系
② 相对位置编码
- 编码的是”两个位置之间的距离”而不是”某个位置的编号”
- 例如 ML-Transformer、T5 的相对偏置
- 优点:考虑了相对距离,泛化性好
③ RoPE(旋转位置编码,Rotary Position Embedding)——主流方案
- LLaMA 系列、Mistral 等用的方案
- 原理:在 Q 和 K 上做旋转变换,让内积结果包含位置信息
- 本质:把”绝对位置”和”相对位置”统一了——内积结果只依赖相对位置
- 优点:自然可外推(Extrapolation)、无需额外参数、与 attention 计算融合
面试加分:RoPE 是主流因为它在”绝对位置编码的简洁性”和”相对位置编码的表达力”之间取了平衡。
题16:过拟合 / 梯度消失
参考回答(面试口语化——基础概念):
过拟合(Overfitting):
- 模型在训练数据上表现很好,但在测试/新数据上表现很差
- 原因:模型学习了训练数据的噪声(而不是真实规律),模型太复杂、数据太少
- 解决方法:
- 增加训练数据 / 数据增强
- 减少模型复杂度(减少层数/参数)
- 正则化(L1/L2)
- Dropout
- Early Stopping
梯度消失(Vanishing Gradient):
- 反向传播时,靠近输入层的梯度趋近于0,导致这些层无法更新
- 原因:激活函数选择不当(如 sigmoid 在两端梯度接近0)+ 深层网络连乘
- 解决方法:
- 用 ReLU 类激活函数(梯度=0或1,不会指数级缩小)
- Batch Normalization(让激活值分布正常)
- 残差连接(ResNet,让梯度可以有捷径通路)
题17:数据并行 / 模型并行
参考回答(面试口语化——Agent实习面经常见):
数据并行:
- 多个设备(GPU)复制同一份模型
- 每个设备处理不同 batch 的数据
- 前向传播独立计算,反向传播时梯度汇总(AllReduce)
- 适用于:模型能放进单卡,但数据量太大需要加速
模型并行:
- 一个模型太大放不进单卡,拆到多个设备上
- 张量并行:某一层的计算拆分到多卡上(如注意力头拆分)
- 流水线并行:不同层放不同设备上,数据像流水线一样经过各层
- 适用于:模型超大(如 70B/175B 参数),单卡显存放不下
在 Agent 场景的应用:
- Agent 通常不会用到这两种并行(因为模型调用走 API,不需要自己部署)
- 但如果本地部署开源模型(如 Qwen-72B),就需要考虑——通常用张量并行部署
🎉 全部 78 道题参考回答已生成完毕!
文档编号 文档名称 题目数 状态 1 参考回答_01_高频深度篇_A+与A级20题 20 题 ✅ 2 参考回答_02_核心模块篇_B+级16题 16 题 ✅ 3 参考回答_03_各厂特色题篇_公司特色与通用追问 34 题 ✅ 4 参考回答_04_项目面试话术篇_决定Offer的关键叙事 7 题 ✅ 5 参考回答_05_算法与CS基础篇_手撕代码与八股 17 题 ✅ 合计 5 份文档 94 题(含复用+拓展) 全部完成 祝你面试顺利,offer 拿到手软!🚀
Share Article
If this article helped you, please share it with others!