腾讯算法岗面经合集:感知哈希、RAG 与 Go 调度
- 轮次
- 多轮面试合集
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- (面经 01)算法:给定数组 a[1..n],可反复执行「所有 a[i] 减 i」或「所有 a[i] 减 n−i+1」,判断能否使数组全部变为 0;追问:给出的条件是否同时满足必要性和充分性,是否存在边界情况?
- (面经 01)算法:n 个 32 位整数中,一个数出现 1 次,其余数都出现 3 次,要求 O(n) 时间、O(1) 额外空间找出只出现一次的数;追问:若其余数出现 2 次应如何处理,是否有更优方法?
- (面经 01)算法:已知 rand5() 等概率返回 0~4,使用 rand5() 实现 rand3() 和 rand7();追问:调用 rand5() 的期望次数是多少?
- (面经 01)智力题:64 匹马、8 条跑道,每匹马能力固定,最少比赛多少轮可以决出前 4 名?
- (面经 02)请做一下自我介绍,并介绍当前实习岗位的主要工作
- (面经 02)算法:给定天数、目标距离和每天可走步数,求达到目标距离的方案数
- (面经 02)为什么在项目中引入感知哈希进行图片相似识别?
- (面经 02)汉明距离在图片相似识别中如何使用?
- (面经 02)感知哈希的阈值如何确定?
- (面经 02)感知哈希的大致原理是什么?
- (面经 02)RAG 采用什么方式进行召回?
- (面经 02)能力证据单元如何抽取?
- (面经 02)为什么不直接对整份简历做 Embedding?
- (面经 02)Redis 为什么使用单线程仍能获得较高吞吐?
- (面经 02)如果 Redis 命令改成多线程执行,会增加哪些开销?
- (面经 02)Go 协程如何实现?Goroutine 在什么情况下会发生调度切换?
- (面经 03)介绍你完成得最好的项目以及个人负责的部分
- (面经 03)项目效果具体提升了多少?
- (面经 03)为什么通用检索方案不能解决项目问题?
- (面经 03)你的方案与语义切块有什么区别?
- (面经 03)已有方案也能用模型补充上下文,你的方案有何不同?
- (面经 03)如何保证模型抽取结果的准确性?
- (面经 03)为什么不使用规则处理?
- (面经 03)项目中的排序指标表示什么?
- (面经 03)介绍一次处理线上故障的经历
- (面经 03)Go 的 Goroutine 底层如何调度?
- (面经 03)Goroutine 的初始栈有多大,如何扩容?
- (面经 03)Go GC 如何尽量降低 STW 时间?
- (面经 03)网络状况足够好时,TCP 和 UDP 传输同一个文件哪个更快,为什么?
- (面经 03)为什么已有 HTTP 仍需要 HTTPS?
《参考解析》
1. 其余数出现 3 次时找唯一数。不能只靠一次异或。主流做法有两种:①按位统计——把每个数的 32 个二进制位分别累加,每一位对 3 取模,剩下的位拼起来就是答案,时间 O(32n)、空间 O(1),最好写也最好讲;②用两个掩码模拟三进制计数:ones = (ones ^ x) & ~twos; twos = (twos ^ x) & ~ones;,遍历完 ones 就是答案,本质是把每个位的出现次数 mod 3 编码在两个 bit 里。追问「若其余数出现 2 次」就简单了——全部异或,成对的互相抵消,剩下的就是唯一数。由此可以总结出通用套路:「其余出现 k 次」这类题就是把每一位按模 k 计数。
2. rand5 实现 rand3 与 rand7 的期望调用次数。核心是拒绝采样。用两次 rand5() 拼出一个 0~24 的均匀随机数 r = 5 * rand5() + rand5()。实现 rand7:r < 21 时返回 r % 7(21 是 7 的倍数,保证均匀),否则重来,接受率 21/25;每次尝试要调 2 次 rand5,所以期望调用次数是 2 × 25/21 = 50/21 ≈ 2.38 次。实现 rand3:r < 24 时返回 r % 3,接受率 24/25,期望次数是 2 × 25/24 = 25/12 ≈ 2.08 次。面试时再补两句推广会更完整:k 次 rand5 能生成 5^k 个等概率值,所以调用次数越多可用的「余数空间」越大;绝对不能写成 (5 * rand5() + rand5()) % 7 这种直接取模,那会破坏均匀性。
3. 感知哈希做图片相似。pHash 的流程是「缩放到 32×32 灰度 → DCT 变换 → 取左上角 8×8 低频系数 → 去掉直流分量后与中位数比较 → 得到 64 位指纹」;aHash 是缩到 8×8 灰度后与均值比较;dHash 是比较相邻像素的梯度方向。相似度用汉明距离衡量(异或后数 1 的个数),阈值必须用业务样本标定而不是拍脑袋:收集「同一张图的不同版本」(压缩、缩放、加水印)和「不同图」两组样本,画出汉明距离分布,取两类分布的分界点,工程上 64 位指纹常用 5~10 这个区间,阈值越小越严(漏判多),越大越松(误判多)。感知哈希的优点是快、对缩放压缩和轻微改色鲁棒;缺点是对大幅裁剪、旋转和强滤镜不鲁棒,也不表达语义相似——内容不同但构图相似的图会被判为相似,要做语义级相似得上 CLIP 这类 embedding 模型。
4. 为什么不直接对整份简历做 Embedding。整份简历是多主题长文本——个人信息、教育、技能、多段实习、项目、获奖混在一起,直接 embedding 会把这些语义平均成一个向量,检索粒度太粗:一句「用过 PyTorch」和「主导过推荐系统」被压进同一个向量,相似度就失去了区分度。做法是先抽「能力证据单元」,把简历切成「技能 + 行为 + 量化结果 + 时间/角色」的原子条目(例如「用 XGBoost 把流失预测 AUC 从 0.72 提到 0.81」),每条单独 embedding,检索时按 query 召回具体证据再拼装上下文——好处是召回可解释、可加权、可去重,更新也只影响单条。抽取准确性靠「模型抽 + 规则校验 + 人工抽检」:用 JSON Schema 一类结构化输出约束格式,对数字和时间做规则校验,低置信度样本走二次模型复核或人工。召回方式上,简历里全是框架名、公司名、指标名这类专有名词,纯向量召回不敏感,标准组合是「向量检索 + BM25 关键词检索 + 重排」。
5. Redis 单线程为什么快、改多线程会增加什么开销。快的原因有五个:纯内存操作没有磁盘 IO;单线程没有锁竞争和上下文切换;IO 多路复用加事件驱动,把「等网络」和「执行命令」解耦,网络慢不会阻塞命令执行;数据结构和编码针对性强(listpack、intset、跳表);命令本身都是 O(1) 或 O(log n) 的小操作,单次延迟在微秒级。把命令执行改成多线程要付的代价是:①必须加锁保护共享数据结构(键空间、过期字典、统计),锁竞争会吃掉并行收益;②原子性语义变复杂——MULTI、Lua 脚本、INCR、WATCH 这些依赖单线程串行的语义都要重新定义;③CPU 缓存局部性变差、伪共享增多;④上下文切换与调度开销;⑤并发 bug 的复现和调试成本。所以 Redis 6 的多线程只用在网络 IO 读写和协议解析上,命令执行仍是单线程;真正的横向扩展手段是分片(Cluster)和读写分离。
6. Goroutine 调度、栈扩容与 Go GC 降 STW。调度是 GMP 模型:G 是 goroutine(初始栈 2KB,按需以约 2 倍扩容——旧栈内容复制到新栈并修正指向栈的指针,栈收缩放在 GC 时做),M 是 OS 线程,P 是处理器上下文(数量默认 GOMAXPROCS,持有长度 256 的本地运行队列)。M 必须绑定 P 才能执行 G;本地队列空了会从全局队列取,或从别的 P 偷一半(work stealing);G 做阻塞系统调用时 M 与 P 解绑,P 交给其他 M 继续跑,避免线程被 IO 拖死。抢占在 1.14 之前是协作式的(只在函数调用检查点让出),长循环会霸占 P,1.14 起用基于信号的异步抢占,sysmon 发现某个 G 运行过久就发信号。GC 是三色标记加混合写屏障的并发标记清除:标记与用户代码并发,靠写屏障保证不漏标;STW 只留在「标记开始前的准备」和「标记结束」两个极短阶段,把主要工作放到并发阶段和后台 mark worker。降低 STW 的手段还有:把大对象清扫和栈扫描分散到多个周期、用 GOGC 调整触发频率、用 GOMEMLIMIT(1.19+)限制内存上限、减少大对象和长生命周期对象的创建以缩小标记工作量;调优要靠 GODEBUG=gctrace=1 和 pprof 看 GC 次数与暂停分布,而不是凭感觉改 GOGC。