最近在整理算法面试资料时,看到一份流传很广的2023年58同城算法工程师面试题(第二版)。我把整份题单过了一遍,最大的感受是:考察面比想象中宽了不少。KMP的next数组、拉普拉斯锐化、音频重采样、PID、Drools的Rete算法、国密SM系列……从底层数据结构到业务算法,再到工程落地,基本一个没落下。
这份题单对准备算法岗位面试的人很有参考价值。它既不是清一色的LeetCode刷题,也不是纯理论八股,而是围绕“算法工程师在真实业务里要解决什么问题”来出题。如果你正在准备算法岗面试,或者想系统性查漏补缺,这篇文章可以帮你把涉及的考点全部过一遍,顺带把每道题背后的考察意图和答题思路讲清楚。
1. 面试题整体拆解:这份2023年算法工程师面试题考了什么
1.1 从考点分布反推岗位诉求
我把题单里涉及的算法方向做了个归类,大致能分成六块:
| 方向 | 典型考点 | 对应热词 |
|---|---|---|
| 数据结构与基础算法 | KMP、堆排序、快速幂、Dijkstra、贪心、二分图HK、DC3 | 数据结构排序算法、kmp算法、贪心算法 |
| 机器学习 | KNN、聚类、XGBoost、强化学习、变分推断 | 机器学习算法、knn算法的应用能力、kl elbo算法 |
| 图像与信号处理 | Sobel、拉普拉斯锐化、图像分类、工业异常检测、音频重采样 | sobel算法、图像锐化的拉普拉斯算法、音频重采样算法 |
| 控制与状态估计 | PID、卡尔曼滤波、粒子群、模拟退火 | pid算法、卡尔曼滤波算法、粒子群算法原理 |
| 业务与工程化 | BM25排序、Drools规则引擎、国密算法、SSL弱哈希 | bm25算法、规则引擎drools的rete算法、sm2 sm3 sm4 zuc |
| 深度学习与视觉前沿 | EVA-02、图像分类算法、异常检测 | eva-02分类算法、图像分类算法、工业异常检测算法 |
这个分布其实已经能看出来,这轮面试不是单纯刷题,而是“基础能力+业务理解+工程素养”的综合考察。尤其是把音频重采样、PID、Rete算法这类偏工程的方向放进来,说明面试官很在意候选人是否真的处理过生产环境问题,而不仅仅是会写算法题。
1.2 为什么58同城的算法岗会考这些
58同城的业务场景比较特殊:信息分类平台,覆盖招聘、房产、二手车、本地生活服务等领域。这类业务的核心算法需求集中在几个地方——搜索排序(用户搜“Java开发”怎么把最匹配的职位排前面)、推荐召回(给用户推哪些二手商品/房源)、风控识别(判断虚假信息、异常行为),以及大量文本图像内容的处理。
所以你会发现,面试题里出现BM25、KNN、聚类、图像锐化这些内容,背后都有业务影子。搜索排序需要相关性和BM25这类检索模型,推荐需要聚类和向量召回,图片真实性审核需要Sobel边缘检测、图像分类、异常检测这些视觉手段。出题方向其实是跟着业务走的,这在面试复盘时尤其值得注意。
2. 数据结构与基础算法:手撕代码前先理清这三类题
2.1 KMP算法的next数组怎么算:以p=“abacaba”为例
题单里有一道很经典的KMP题:“对于模式串p='abacaba',其next数组为多少”。这题看着简单,但能完整算对的人比例其实不高,因为不同教材对next数组的定义有差异。
先说最常用的“前缀函数”定义:next[i]表示p[0..i]子串的最长相等真前后缀长度。按这个定义,p="abacaba"的推导过程是:
- next[0] = 0,单字符没有真前后缀
- next[1],子串"ab",前缀a后缀b不相等,next[1]=0
- next[2],子串"aba",最长相等前后缀是"a",长度为1
- next[3],子串"abac",没有相等前后缀,next[3]=0
- next[4],子串"abaca",最长相等前后缀是"a",next[4]=1
- next[5],子串"abacab",最长相等前后缀是"ab",next[5]=2
- next[6],子串"abacaba",最长相等前后缀是"aba",next[6]=3
所以经典前缀函数结果是[0, 0, 1, 0, 1, 2, 3]。有一部分教材把next数组定义为“失配时模式串跳转的位置”,也就是next[0]=-1,后面的值等于前缀函数前一项,算出来是[-1, 0, 0, 1, 0, 1, 2]。面试时建议先把定义跟面试官确认清楚,再开始算,这个小细节反而能加印象分。
next数组的手写代码也给一版,标准前缀函数写法:
vector<int> getNext(const string& p) { int n = p.size(); vector<int> next(n, 0); for (int i = 1; i < n; i++) { int j = next[i - 1]; while (j > 0 && p[i] != p[j]) j = next[j - 1]; if (p[i] == p[j]) j++; next[i] = j; } return next; }KMP的匹配过程我就不再贴完整代码了,关键是记住:文本串指针不回退,失配时通过next数组确定模式串跳转位置,时间复杂度O(m+n)。面试时如果直接能写出这段代码,再把两种next定义的差异说清,基本就稳了。
2.2 排序算法:堆排序的建堆和下沉容易写错
热词里堆排序、冒泡排序C++、快速幂算法C++都出现了。排序算法是面试手撕代码的重灾区,但大家容易忽略的是,面试官考排序不是为了看你背不背得出来,而是看你对复杂度、稳定性、工程应用的理解。
堆排序尤其值得单独练。很多人能说清“大顶堆”“小顶堆”的概念,但写代码时容易在heapify这一步忘记递归或者边界判断出错。我贴一版简洁的堆排序核心代码:
void heapify(vector<int>& nums, int n, int i) { int largest = i; int l = 2 * i + 1, r = 2 * i + 2; if (l < n && nums[l] > nums[largest]) largest = l; if (r < n && nums[r] > nums[largest]) largest = r; if (largest != i) { swap(nums[i], nums[largest]); heapify(nums, n, largest); } } void heapSort(vector<int>& nums) { int n = nums.size(); // 从最后一个非叶子节点开始建堆 for (int i = n / 2 - 1; i >= 0; i--) heapify(nums, n, i); // 依次将堆顶放到末尾 for (int i = n - 1; i > 0; i--) { swap(nums[0], nums[i]); heapify(nums, i, 0); } }这里最容易踩的两个坑:一是建堆时必须从最后一个非叶子节点(n/2-1)开始,而不是从0开始;二是每次交换后堆的大小减1,heapify的n参数必须传当前有效长度,否则已排好的数据会被重新打乱。另外建议多嘴说一句“堆排序是不稳定排序,最好别用在需要稳定性的场景”,这能体现出你对工程细节的敏感度。
冒泡排序如果被问到,直接用“带标志位的优化版”写,一旦某轮没有发生交换就提前结束,复杂度最好情况能到O(n)。
2.3 快速幂、Dijkstra、贪心、二分图HK与DC3
这几个考点放在一起说,是因为它们分别代表了算法面试中不同类型的题目:数值计算、图论、组合优化、字符串处理。
快速幂的核心就一句话:把指数二进制拆开,底数不断平方,遇到二进制位为1就乘进结果。手写代码很短:
long long quickPow(long long a, long long b, long long mod) { long long res = 1; while (b > 0) { if (b & 1) res = res * a % mod; a = a * a % mod; b >>= 1; } return res; }Dijkstra考的是优先队列优化版本,重点说清楚为什么不能用普通队列:因为每个节点的最短距离会动态更新,必须用优先队列每次取当前距离最小的节点,复杂度O((V+E)logV)。被问负权边时直接回答“Dijkstra不能处理负权边,有负权边用Bellman-Ford或SPFA”。
贪心算法在面试里更多是作为一种“先想想”的思路,出现最多的题型是区间调度(按结束时间排序)和哈夫曼编码。答题时先说“greedy choice property”和“optimal substructure”,再给具体策略,比直接扔代码显得更有章法。
二分图HK(Hopcroft-Karp)和DC3后缀数组属于加分项,考得不多但一旦考到就是区分度。HK算法核心是用BFS为每个未匹配点找增广路径的层次图,再用DFS做多路增广,能把二分图最大匹配复杂度从O(VE)降到O(E√V)。DC3是线性时间构造后缀数组的算法,结合了基数排序和分治思想,如果能把这两个算法的思路说清楚,基本能给面试官留下“算法功底扎实”的印象。
3. 机器学习与深度学习:从经典模型到变分推断
3.1 KNN的三种应用能力与聚类算法选型
热词里有一条“KNN算法的应用能力包括哪三个方面”,这题看起来基础,但很多人答不全。KNN(K近邻)不是只能做分类,它有三种典型应用能力:分类(投票决定类别)、回归(取K个近邻的均值作为预测值)、密度估计与离群点检测(距离分布异常的点)。
举几个业务里的例子:分类对应垃圾信息识别,回归对应二手房价格预测,离群点检测对应刷单异常账号识别。一个算法在不同任务里换着用,这比单纯背概念更能打动人。
聚类算法则要会做选型对比。面试官如果问“K-Means和DBSCAN你怎么选”,不要只回答“K-Means快”,要把适用场景说清楚:
| 维度 | K-Means | DBSCAN |
|---|---|---|
| 聚类形状 | 凸形簇 | 任意形状 |
| 类别数 | 需要指定K | 不需要指定 |
| 噪声处理 | 敏感,噪声会影响质心 | 自动识别噪声点 |
| 复杂度 | O(nkt) | O(n²),可用索引优化 |
| 适用场景 | 大规模规整数据 | 密度不均、有大量噪声的数据 |
3.2 XGBoost与梯度提升树的高频追问
XGBoost在算法面试里属于“必被追着问”的模型。面试官通常不会满足于你回答“XGBoost是GBDT的优化版本”,他们想听的是目标函数和分裂增益。
XGBoost目标函数的核心是:
[ Obj = \sum_{i=1}^{n} L(y_i, \hat{y}i) + \sum{k=1}^{K} \Omega(f_k) ]
其中正则项 (\Omega(f_k) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T}w_j^2),T是叶子节点数,w是叶子权重。XGBoost在每轮迭代时对损失函数做二阶泰勒展开,利用一阶导g和二阶导h,分裂时的增益计算为:
[ Gain = \frac{1}{2}[\frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{(G_L+G_R)^2}{H_L+H_R+\lambda}] - \gamma ]
其中G和H分别是叶子节点上样本的一阶导累加和二阶导累加。能把这个公式讲清楚,再补充“XGBoost相比GBDT的优势在于二阶导数、正则项、列抽样、并行化”,这题的分数基本就拿到了。
3.3 强化学习与KL-ELBO变分推断
强化学习在这份题单里出现的频率不低。面试官想知道的是“你是否理解智能体与环境交互的学习范式”,而不是让你背DQN的架构。回答时抓住三个核心要素:策略(Policy)、奖励(Reward)、状态转移(Transition)。如果被追问深度Q网络,就重点说“目标网络+经验回放”两个技巧解决样本相关性和训练不稳定的问题。
KL-ELBO是变分推断里的核心概念,公式推导要注意逻辑链。在变分推断中,我们要用分布q(z)近似后验p(z|x),直接优化KL散度KL(q||p)不可行,因为里面包含log p(x)不好算。于是转化成优化ELBO(Evidence Lower Bound):
[ \log p(x) = ELBO(q) + KL(q(z)||p(z|x)) ]
因为KL散度非负,所以log p(x) ≥ ELBO(q)。最大化ELBO等价于最小化KL(q||p),而ELBO可以写成:
[ ELBO(q) = E_{q(z)}[\log p(x, z)] - E_{q(z)}[\log q(z)] ]
面试时把这条链路讲顺了,面试官就知道你是真的理解变分推断而不是背公式。
3.4 图像分类算法与工业异常检测的前沿方向
EVA-02这类视觉Transformer模型在热词里出现,说明面试题对前沿模型也有涉及。EVA系列模型的核心思路是用大规模CLIP模型做知识蒸馏,把冻结的CLIP视觉编码器的特征迁移到纯Transformer模型中,训练效率远高于从零训练。回答时不需要背模型参数,把“蒸馏+冻结CLIP+多模态对齐”这个思路说清楚就行。
工业异常检测是另一个值得准备的方向。传统方法用重构误差(AutoEncoder重建正常样本,异常样本重建误差大),现在主流方法是PatchCore这类基于特征存储库的方法:用预训练网络提取正常样本的patch特征存入内存库,测试时对比特征距离判断异常。面试官如果追问“为什么不直接用分类模型”,就回答“异常类型未知且样本极少,单分类/距离度量更实用”。
4. 图像与信号处理:看似偏门其实是业务刚需
4.1 图像锐化的拉普拉斯算法与Sobel的区别
热词里同时出现“图像锐化的拉普拉斯算法”和“sobel算法”,这俩确实经常被拿来做对比。核心区别是一阶微分和二阶微分:Sobel算子是边缘检测,提取的是梯度幅值;拉普拉斯算子是二阶微分,反应的是灰度突变率的变化,常用于锐化。
拉普拉斯算子的3×3模板常见有两种:
[ \begin{bmatrix} 0 & 1 & 0 \ 1 & -4 & 1 \ 0 & 1 & 0 \end{bmatrix} \quad \begin{bmatrix} 1 & 1 & 1 \ 1 & -8 & 1 \ 1 & 1 & 1 \end{bmatrix} ]
锐化公式一般写作 g = f - c * ∇²f,注意这里的∇²f用的是“中心为负”的模板(比如中心-4),所以是减去拉普拉斯结果;如果模板中心为正,则公式变成 g = f + c * ∇²f。这个符号问题我在面试里看很多人栽过,回答时最好主动把这两种对应关系讲一下。
Sobel算子的两个模板:
[ G_x = \begin{bmatrix} -1 & 0 & 1 \ -2 & 0 & 2 \ -1 & 0 & 1 \end{bmatrix} \quad G_y = \begin{bmatrix} -1 & -2 & -1 \ 0 & 0 & 0 \ 1 & 2 & 1 \end{bmatrix} ]
梯度幅值 ( G = \sqrt{G_x^2 + G_y^2} ),工程上常用 ( |G_x| + |G_y| ) 近似。
实操上最大的坑是:拉普拉斯算子对噪声极其敏感,直接用原图算会放大噪声,所以正确流程是先高斯模糊降噪,再用拉普拉斯锐化。用OpenCV实现的代码也很简单:
import cv2 img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE) blur = cv2.GaussianBlur(img, (3, 3), 0) lap = cv2.Laplacian(blur, cv2.CV_16S, ksize=3) lap = cv2.convertScaleAbs(lap) sharpened = cv2.subtract(img, lap) # 中心为负的模板注意用CV_16S而不是CV_8U,因为卷积结果会出现负值,直接存成8位会截断信息。
4.2 音频重采样算法:从线性插值到多相滤波
音频重采样在热词里出现,这道题更容易出现在偏音视频或语音的团队。重采样的本质是改变采样率,比如把44.1kHz转成48kHz,做不做得好直接决定音频质量。
最简单的重采样算法是线性插值,对相邻两个采样点做线性插值得到新采样点。优点是实现简单,缺点是对高频成分的衰减明显,容易产生频谱混叠。工程上使用最多的是多相滤波器(Polyphase Filter):预先算好一个低通滤波器系数,按重采样比例拆成多个子滤波器组,每个输出样本只需和其中一组系数做卷积,计算量远小于直接做一次完整低通卷积。
面试官如果问“重采样过程中最关键的一步是什么”,答案不是插值本身,而是抗混叠滤波。降采样前必须经过低通滤波器,截止频率要低于新的奈奎斯特频率,否则高频分量折叠到低频,声音会变糊。
4.3 PID、卡尔曼滤波、粒子群与模拟退火
这几个属于跨领域的“元算法”,面试官可能会把它放在业务问题里考。PID算法在工业控制里无处不在,热词里还有一条“pid算法在crps psu power的作用”,其实就是电源/控制系统里的反馈调节应用。
PID的三个参数作用要能讲明白:P(比例)根据当前误差调整输出,误差越大调整越大,但会产生稳态误差;I(积分)累积历史误差,消除稳态偏差;D(微分)预测误差变化趋势,抑制超调。调参口诀也记一下:先调P让系统基本稳定,再加I消除稳态误差,最后加D抑制超调,每加一项都要回到第一步重新验证。
卡尔曼滤波考的更多是“懂不懂状态估计”。它由预测和更新两步组成,预测是利用状态方程(比如匀速运动模型)预测下一步位置和误差协方差,更新是利用观测值(比如GPS读数)加权修正,权重就是卡尔曼增益K。面试时能把五个核心公式写出来就说明真懂:
[ \hat{x}^- = A\hat{x} + Bu, \quad P^- = APA^T + Q ] [ K = P^-H^T(HP^-H^T + R)^{-1} ] [ \hat{x} = \hat{x}^- + K(z - H\hat{x}^-), \quad P = (I - KH)P^- ]
粒子群和模拟退火都属于启发式优化算法。粒子群的核心是每个粒子在搜索空间里同时向个体最优pbest和全局最优gbest方向移动,速度和位置更新公式:
[ v_{i}(t+1) = w v_i(t) + c_1 r_1 (pbest_i - x_i) + c_2 r_2 (gbest - x_i) ] [ x_i(t+1) = x_i(t) + v_i(t+1) ]
w是惯性权重,c1是自我认知系数,c2是社会学习系数。模拟退火则要抓住Metropolis接受准则:温度越高越容易接受差解,随着温度降低,接受差解的概率越来越小,从而避免陷入局部最优。
5. 业务落地与算法工程化:决定offer高度的加分项
5.1 规则引擎Drools的Rete算法原理与事实匹配过程
这道题是工程向的硬核问题。Drools是Java生态最常用的规则引擎,底层用Rete算法做规则匹配。Rete算法的核心思想是把规则编译成一个有向无环网络,事实进入网络后逐层传播,避免每条规则都重新匹配一遍所有事实。
可以这么理解:假设有100条规则,每条规则里有多个条件,如果每条规则都从头匹配所有事实,复杂度是规则的重复劳动。Rete算法把相同的前缀条件提取出来共享,把条件拆分成Alpha节点(单条件匹配)和Beta节点(条件之间的Join,也就是跨事实的关联匹配),最终匹配结果汇聚到Terminal节点触发规则。
事实匹配的流程大致是:事实对象被插入Work Memory后,首先进入Alpha网络做单条件过滤,符合条件的对象进入Alpha内存;然后到Beta网络和之前已经匹配的部分结果做Join,产生新的部分匹配存入Beta内存;当某个规则的所有条件都被满足,激活进入Agenda,由冲突解决策略决定执行顺序。
面试时建议画一个简单的图:规则“年龄>30且城市=北京”在网络里如何传播。能把这三个节点的关系讲清楚,已经比大多数只背概念的人强了。
5.2 国密算法与SSL弱哈希修复
热词里有“sm2、sm3、sm4和zuc算法”,这在国内互联网公司的面试里越来越常见,因为很多项目需要满足信创和合规要求。这四种算法分工要记住:SM2是非对称加密(基于椭圆曲线,替代RSA),SM3是密码哈希(摘要256位,用于数字签名和完整性校验),SM4是对称分组加密(128位分组、128位密钥,类似AES),ZUC是序列密码(主要用于移动通信加密)。
如果面试官让你比较SM4和AES,从安全性、效率、国内合规三个方面说。如果问签名验签流程,就重点讲SM2 + SM3的组合使用。
“SSL证书使用了弱HASH算法(CVE-2005-4900)怎么修复”这道题属于安全运维类。CVE-2005-4900指的是证书签名使用SHA-1算法的风险。修复思路分几步:先检查现有证书的签名算法,用OpenSSL命令查看;
openssl x509 -in cert.pem -text -noout | grep "Signature Algorithm"如果输出SHA1WithRSAEncryption,就说明证书用了弱哈希。解决方法是重新生成密钥和证书签名请求,用SHA-256算法签名,然后把新证书配置到Nginx/Apache并重载服务。不用改业务代码,但要注意证书链里所有中间证书的签名算法也要检查。
5.3 BM25排序算法与搜索业务场景
BM25是搜索排序里的经典相关性模型,也是很多算法工程师面试的必问考点。它的核心思想是:一条文档和查询的相关性,由查询中每个词在文档中的词频决定,同时也受词在全局文档中的稀有程度(IDF)影响。
BM25公式:
[ score(D,Q) = \sum_{i=1}^{n} IDF(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{avgdl})} ]
k1控制词频饱和度,b控制文档长度归一化的强度。对于58同城这类信息分类平台,同一职位或房源的文本篇幅差异很大,BM25的文档长度惩罚就很重要,避免长文本天然得分更高。
面试时如果被追问“BM25和向量检索怎么选择”,回答思路是:BM25适合关键词精确匹配为主的短查询,可解释性强;向量检索适合语义匹配和长尾改写;生产环境往往混合使用,BM25作为召回候选,向量做语义补充,最后用learning to rank融合排序。
6. 面试复盘:答题节奏、表达方式与避坑建议
6.1 面试官想听的“思路”是什么
面试和考试最大的区别在于:面试官在乎的不是你会不会背这道题的答案,而是你能不能把一个未知问题拆解成已知方法。建议答题时固定用“暴力解→优化解→边界解”的三层结构。
举个例子,如果面试官问“如何在海量日志中统计TopK的IP”,别上来就写堆排序。先说暴力做法是统计所有IP频率再排序,然后用哈希表统计频率、用大小为K的小顶堆维护TopK,复杂度O(nlogK);再补充如果内存放不下,可以哈希分片处理。这样做的好处是你给面试官提供了追问的抓手,他也能看到你的思维层次。
手撕代码时务必先确认输入输出的边界:输入为空怎么办、全是重复值怎么办、数组长度有没有限制。这些细节往往比代码本身更影响面试评价。
6.2 七个高频翻车现场
这些年我在面试别人和复盘自己面试的过程中,总结了一些高频翻车点,这里整理成表格供大家自查:
| 挂点 | 典型错误 | 正确姿势 |
|---|---|---|
| KMP、next数组定义搞混 | 用了另一种定义却不说明 | 先确认“使用前缀函数定义”再计算 |
| 堆排序建堆起点错误 | 从0开始heapify | 从n/2-1开始,即最后一个非叶子节点 |
| 拉普拉斯锐化符号反 | 中心为正却用g=f-∇²f | 先确认模板中心符号再选公式 |
| Sobel与拉普拉斯混为一谈 | 把Sobel说成锐化算子 | Sobel是边缘检测一阶微分,拉普拉斯可锐化 |
| XGBoost和GBDT区别说不全 | 只答“加了正则” | 二阶泰勒展开、正则项、列抽样、并行化逐条说 |
| Dijkstra用普通队列 | 复杂度变成O(V²)甚至出错 | 必须用优先队列取最小距离点 |
| BM25和TF-IDF区别模糊 | 只说“BM25更高级” | 词频饱和度k1和文档长度惩罚b是核心 |
6.3 一周冲刺复习计划
如果离面试还有一周,不建议再盲目刷题。我的建议是按“基础算法40% + 机器学习25% + 工程与业务25% + 前沿与项目10%”的比例分配时间:
- 第1-2天:主攻KMP、堆排序、快速幂、二分图HK、贪心题,每天手写2-3遍核心代码,直到不看任何提示能独立写对;
- 第3天:机器学习里KNN、聚类、XGBoost、强化学习,重点练习“用业务场景说算法”,比如用KNN做异常登录检测;
- 第4天:把Sobel、拉普拉斯、音频重采样、卡尔曼滤波过一遍,用Python/OpenCV跑一遍图像和音频的处理流程;
- 第5天:工程向内容集中突击,Rete算法画图讲清流程、SM2/SM3/SM4/ZUC用途整理成表、BM25公式手推一遍;
- 第6天:模拟面试,找人给你随机抽题,要求5分钟内讲思路、10分钟内写完代码,训练自己的表达能力;
- 第7天:复盘所有翻车点,把自己最容易忘的公式和易错点写在一张A4纸上,进面试前看一遍。
我个人在带候选人时经常发现,很多人基础算法题能AC,但被问到“这个算法在你的项目里怎么用”就卡壳。从这份58同城的面试题来看,面试官明显是想把“会刷题”和“会干活”区分开的。所以你在复习时,每学一个算法都多问自己一句:这个东西在真实业务里能解决什么问题?想清楚这个问题,你就不再是背答案,而是真正在建立自己的算法知识体系了。