美团 AI Infra 校招 一面
Q: 手撕:删除链表中的重复节点 II?
(编程题)
Q: HashMap 引入红黑树的目的?为什么不一开始就用红黑树?
引入红黑树的动机:
Java 8 的 HashMap 在桶内链表长度超过阈值(默认 8)时,将链表转为红黑树。核心目的是防止 hash 冲突攻击导致的性能退化。
问题场景分析:
1 | 正常情况: hash 分布均匀,每个桶 0-2 个元素 |
为什么不一开始就用红黑树:
内存开销差异巨大:
1
2链表节点: next 指针 (8 bytes) + key/value/hash
TreeNode: left + right + parent + color (32+ bytes) + 继承 Node红黑树节点内存是链表的 2-3 倍
短链表下性能对比:
- 链表长度 1-6 时,顺序遍历比树查找更快(CPU cache 友好,无分支预测开销)
- 红黑树查找需要比较 + 左右分支跳转(cache miss 概率高)
- 实测:长度 < 8 时链表平均查找时间 < 红黑树
维护成本:
- 红黑树的插入/删除需要旋转(rotation)和重染色(recoloring)
- 平均每次插入 0.6 次旋转,删除 0.8 次旋转
- 链表的插入/删除是 O(1) 的简单指针操作
实际统计:大多数 bucket 在正常负载因子(0.75)下只有 0-2 个元素,使用链表的桶 > 99%
阈值为什么是 8:
- 根据泊松分布,负载因子 0.75 时,单桶 ≥ 8 个元素的概率 < 0.00000006
- 这意味着正常使用中几乎不会触发树化
- 阈值 8 在”不浪费内存”和”防止退化”之间取得平衡
Q: 用二叉排序树或 AVL 代替红黑树可以吗?
BST(无平衡保证)——不可以:
- 极端情况退化为链表:如果 hash 值的自然序是有序的,BST 退化为 O(n)
- 根本没有解决链表的退化问题
- 唯一比链表好的点是可以二分,但前提是平衡
AVL 树——可以,但红黑树更优的理由:
| 维度 | AVL | 红黑树 |
|---|---|---|
| 平衡严格度 | 严格平衡(左右高度差 ≤ 1) | 近似平衡(最长路径 ≤ 2×最短) |
| 查找性能 | 略优(树高更矮) | 略差(树高最多 2×log(n)) |
| 插入旋转次数 | 最多 2 次 | 最多 2 次 |
| 删除旋转次数 | 最多 O(log n) 次 | 最多 3 次 |
| 适用场景 | 查找密集、修改少 | 增删频繁 |
关键差异在删除操作:
- HashMap 的使用模式:频繁 put/remove(增删密集)
- AVL 删除后可能需要从删除点一路旋转到根(O(log n) 次旋转)
- 红黑树删除最多 3 次旋转 + 若干次重染色(O(1) 结构调整)
- 在 HashMap 这种增删频繁的场景下,红黑树的综合效率更高
B+ 树呢?——过重:
- B+ 树适合磁盘 I/O 优化(减少访问次数),内存场景无此需求
- 节点结构复杂,空间开销大
- HashMap 的单桶元素量有限(通常 < 100),用 B+ 树大材小用
Q: 线程池的核心参数?
线程池(以 Java ThreadPoolExecutor 为例)通过参数化配置实现灵活的线程管理策略:
六大核心参数:
1 | new ThreadPoolExecutor( |
参数详解与设计哲学:
| 参数 | 含义 | 设计考量 |
|---|---|---|
| corePoolSize | 长期保持存活的线程数,即使空闲也不销毁 | CPU 密集型:= CPU 核心数;IO 密集型:= 2× CPU 核心数 |
| maximumPoolSize | 允许的最大线程数(含核心线程) | 防止线程无限创建导致 OOM |
| keepAliveTime | 非核心线程空闲超时后销毁 | 应对流量突刺后回收资源 |
| workQueue | 任务等待队列 | 有界队列防 OOM,无界队列注意堆积 |
| threadFactory | 创建线程的工厂 | 自定义命名(便于调试)、设置优先级、设置 daemon |
| handler | 队列和线程都满时的拒绝策略 | 保护系统不被压垮 |
任务提交流程:
1 | 新任务提交 |
四种内置拒绝策略:
- AbortPolicy(默认):抛出 RejectedExecutionException
- CallerRunsPolicy:由提交任务的线程自己执行(天然限流)
- DiscardPolicy:静默丢弃
- DiscardOldestPolicy:丢弃队列头部最旧的任务
常见配置模板:
1 | CPU 密集型(如矩阵计算): |
Q: synchronized 和基于 AQS 的锁有哪些区别?
两者都是 Java 中实现互斥的机制,但设计层次和能力有本质区别:
| 维度 | synchronized | AQS (ReentrantLock 等) |
|---|---|---|
| 实现层面 | JVM 内置(monitorenter/monitorexit 字节码) | Java 代码层面(CAS + CLH 队列) |
| 锁的获取 | 隐式(进入代码块自动获取) | 显式(手动 lock()/unlock()) |
| 公平性 | 非公平(无法配置) | 支持公平/非公平模式 |
| 可中断性 | 不可中断(线程在等待锁时无法被中断) | lockInterruptibly() 支持中断 |
| 超时获取 | 不支持 | tryLock(timeout) 支持 |
| 条件变量 | 只有一个隐含的 wait set | 支持多个 Condition(精细唤醒) |
| 锁状态查询 | 不支持 | isLocked()、getQueueLength() |
| 性能 | JDK 6+ 优化后(偏向锁→轻量锁→重量锁)轻度竞争接近无锁 | 竞争激烈时队列管理更高效 |
| 死锁检测 | JVM 内置支持 | 需手动实现 |
synchronized 的锁升级路径(JDK 6+):
1 | 无锁 → 偏向锁(单线程反复进入) → 轻量锁(CAS 竞争) → 重量锁(OS mutex) |
AQS 的内部原理:
1 | 核心: volatile int state + CLH 双向队列 |
选择建议:
- 简单互斥 + 不需要高级特性 → synchronized(代码简洁、不会忘记释放)
- 需要超时、中断、公平性、多 Condition → ReentrantLock
- 读多写少场景 → ReentrantReadWriteLock
- 高并发计数器 → 不用锁,用 AtomicLong 或 LongAdder
Q: 如何理解最左前缀原则?联合索引 (a,b,c) 下 a>1 and c=1 的索引使用情况?
最左前缀原则的本质:
联合索引 (a,b,c) 在 B+ 树中按 (a, b, c) 的字典序 组织数据:
1 | B+ 树中数据排列顺序: |
关键推论:
- 知道 a 的值/范围 → 可以定位 B+ 树上 a 对应的区域
- 在 a 确定的情况下,b 是有序的 → 可以继续二分
- 但如果 a 是范围查询,则不同 a 值下 b 的全局顺序不确定 → b 无法使用索引
**分析 a>1 AND c=1**:
1 | 索引 (a,b,c) 的 B+ 树: |
执行过程:
- 利用索引中 a 字段做范围扫描(type = range)
- 对于每条索引记录,c=1 的过滤有两种处理:
- 回表过滤:读取完整行数据后在 server 层过滤
- ICP(Index Condition Pushdown):MySQL 5.6+ 的优化,在存储引擎层直接用索引中 c 的值过滤(减少回表次数)
ICP 优化的效果:
1 | 无 ICP: 索引扫描 a>1 的所有记录 → 全部回表 → server 层过滤 c=1 |
ICP 不改变索引的使用方式(仍然只用 a 字段定位),但减少了不必要的回表 I/O。
各种查询条件的索引利用情况:
| 查询条件 | 利用索引字段 | 原因 |
|---|---|---|
| a=1 AND b=2 AND c=3 | a, b, c(全部) | 等值逐级匹配 |
| a=1 AND b>2 AND c=3 | a, b | b 是范围,c 无序 |
| a>1 AND b=2 | a | a 是范围,b 无序 |
| b=2 AND c=3 | 无(全表扫描) | 缺少最左字段 a |
| a=1 AND c=3 | a(跳过 b) | b 不确定时 c 无序,但 ICP 可优化 |
Q: 线上出现慢查询如何定位和排查?
系统化排查流程:
1 | Step 1: 发现 → 慢查询日志 / 监控告警 |
Step 1:开启慢查询日志并发现问题:
1 | -- 开启慢查询日志 |
Step 2:EXPLAIN 关键指标:
| EXPLAIN 字段 | 关注点 | 问题信号 |
|---|---|---|
| type | 访问类型 | ALL(全表扫描)、index(全索引扫描) |
| key | 实际使用的索引 | NULL(未使用索引) |
| rows | 预估扫描行数 | 远大于结果集行数 |
| Extra | 额外信息 | Using filesort、Using temporary |
| filtered | 过滤比例 | 过低说明扫描了大量无用行 |
Step 3:常见根因分析:
| 根因类别 | 具体原因 | 诊断方法 |
|---|---|---|
| 索引缺失/失效 | 没建索引、函数运算导致索引失效、类型转换 | EXPLAIN type=ALL, key=NULL |
| 大表全扫描 | 数据量大且无合适索引 | rows >> 实际需要的行数 |
| 锁等待 | 行锁/表锁/MDL 锁 | SHOW PROCESSLIST 看 State |
| 复杂 JOIN | 多表 JOIN 无合适索引 | EXPLAIN 中多个 ALL |
| 子查询 | 相关子查询每行执行一次 | Extra: Dependent subquery |
| 排序/分组 | filesort 处理大数据量 | Extra: Using filesort |
| 深分页 | LIMIT 1000000, 10 | 扫描前 100 万行后丢弃 |
Step 4:优化方案:
1 | -- 1. 添加合适索引 |
Q: 手撕:二维矩阵行有序列有序,查找某个值?
(编程题)