十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

奇安信秋招算法笔试复盘:从KMP到粒子群,硬核考点全拆解

奇安信秋招算法笔试复盘:从KMP到粒子群,硬核考点全拆解 说实话看到这套标题我第一反应是“2020年”这四个字——奇安信那年的秋招算法卷在圈子里流传度不算低很多准备网安方向算法岗的同学都拿它当过练习。奇安信的笔试风格和纯互联网大厂不完全一样它更偏向“算法基本功工程落地感”的组合偶尔还会掺一点机器学习的题整体难度中等偏上但坑点不少。这篇文章我打算按考后复盘的方式来写把这份卷子里牵涉到的核心考点、经典题型的解题思路、当时的踩坑记录以及我后来复盘时觉得“如果当时知道这些就好了”的经验一次性捋清楚。不管你是准备网络安全方向的技术岗还是泛算法岗的海投选手这份试卷的拆解都有参考价值。先说个总体的印象这套卷子并不是那种“上来就给你一道Hard动态规划让你怀疑人生”的风格。它更重视基础算法的是否真正理解数据结构掌握得牢不牢以及你能不能把常见的算法模板在笔试环境里快速、准确地写出来。但同时它又喜欢在基础题上做一点点变形考察你是不是“背模板型选手”——这一点我是到了复盘阶段才真正想明白的。1. 试卷整体复盘这份卷子到底考了什么1.1 先说说题型分布从回忆版的题目来看整张卷子大致分成了两个半场。前半场是数据结构与基础算法主要涉及数组、字符串、链表、栈、队列、二叉树这些核心数据结构算法层面则覆盖了排序、二分、贪心、动态规划、KMP、拓扑排序、最短路径等。后半场则偏向机器学习与深度学习考察一些基础概念和原理比如常见的损失函数、优化器、模型评估指标等而让很多人意外的是这份卷子里出现了类似粒子群算法、模拟退火这类智能优化算法的题目——这在互联网大厂的算法岗笔试题里是比较少见的倒是和奇安信的“安全算法”定位有关。我印象最深的是字符串相关题目比例不低。KMP的next数组是明确考了原题的这也符合奇安信历年笔试风格——不回避经典题甚至会有意考察那些“你觉得不会考但真的会考”的基础内容。从题目难度梯度来看卷子还是比较友好的。前面几道题基本是“热身级”比如简单的数组遍历、排序后取中位数、链表反转这种。做到中段难度明显上来开始出现需要思维转弯的贪心和DP变形题。真正能拉开差距的其实是两个地方一是KMP这类“看过就会没看过真不会”的经典算法题二是机器学习部分的简答题考察的不是会不会调库而是对算法本质的理解。1.2 奇安信的算法题偏好我后来复盘时整理了近几年的奇安信笔试回忆发现它的选题逻辑其实很清晰安全公司做算法核心目的是用算法去解决安全问题比如流量检测、恶意样本分类、日志异常识别。这就决定了它的考题偏好——数据结构必须扎实经典的字符串和图算法必考智能优化算法和机器学习基础概念也会涉及。举个具体的例子volatile、KMP、Dijkstra、拓扑排序这几位“老朋友”在这套卷子里都出现了。如果你只是单纯刷LeetCode热题100可能对KMP的next数组推导并不熟练但奇安信的卷子偏偏就喜欢这种“基础但需要完整掌握原理”的题。所以我后来给准备网安算法岗的同学一个建议别只盯着“热题”刷把《大话数据结构》或者《算法导论》里的经典章节吃透比刷三百道题有用。这一点尤其适用于那些目标公司是安全厂商的同学——奇安信、深信服这类公司的笔试风格都很“教科书”。2. 基础算法题精讲从一道模拟题说起2.1 一道典型的模拟题这份卷子前面部分有类题很值得讲——模拟题。它不是那种高深莫测的算法题而是给你一个具体场景让你老老实实按步骤把过程模拟出来。比如要求实现一个“LRU缓存淘汰策略”虽然题目描述可能换成“最近最少使用的计算资源被释放”之类的安全场景但核心还是LRU。我记得LRU这道题有个细节坑如果面试题要求在O(1)时间内完成get和put操作这就必须要用“双向链表哈希表”的组合。但是笔试环境里很多人习惯用Java的LinkedHashMap直接实现代码写起来很短问题是笔试判题系统不一定允许你依赖语言库的“漏题”式实现有时候题目会明确限制“不能使用内置容器实现”。所以我的建议是LRU的手写版本双向链表节点定义、哈希表维护、头尾哨兵节点一定要背熟这是安全公司笔试的高频题我在不止一份卷子里见到过它。再比如模拟题中常见的“括号匹配”变形——给一段日志字符串里面有大中小三种括号要求判断是否合法闭合。这道题看着简单但它的变形在于如果括号中间夹着引号引号里的括号不算闭合标记那就要额外维护一个“是否处于引号内”的状态位。这种题考的不是算法思维而是细心程度和对边界条件的覆盖。我自己当年就吃过这种亏。第一版代码只考虑了三对括号的匹配忘了处理转义字符结果在“引号内的括号”这个用例上挂了。后来复盘时总结了一条规律凡是在试卷上遇到模拟题先在草稿纸上把所有边界情况列出来——空字符串、单个左括号、单个右括号、嵌套、乱序、带干扰字符——然后再动手写代码比上来就写然后反复调试要快得多。2.2 贪心算法在笔试中的考法贪心算法也在这份卷子里出现了。这类题当场你未必能证明贪心策略的正确性但你需要能“猜”出来并且能写出能跑的代码。奇安信考过一道“会议室安排”的变体本质上就是“给定n个活动的起止时间求最多能安排多少个不冲突的活动”——经典的活动选择问题按结束时间排序然后从头到尾贪心选择。这类题的代码不难难的是想清楚“为什么按结束时间排序就是最优的”。我当时在笔试时其实没证明只是凭直觉写的。但复盘后我找了《算法导论》第16.1节看了下它给出了严格的证明思路先证明存在一个最优解包含最早结束的活动再用数学归纳法证明贪心选择的正确性。理解这个证明的价值在于面试官可能会追问“为什么”如果你只回答“这是经典贪心”印象分会差不少。我后来还特意整理了一个“贪心笔试速查表”区间问题选点、选区间、区间覆盖统一优先按右端点排序哈夫曼编码问题用优先队列加油问题汽车加油次数最少用贪心最大堆延迟选择。贪心题在笔试里不会考得太偏核心就那么几种模型都准备到就不会慌。3. 数据结构与字符串算法栈、队列与KMP的实战3.1 栈与队列的考察栈和队列在卷子中的出现方式比较“朴素”但越是朴素的题越能看出编码功底。有一道题是用两个栈实现队列要求支持push、pop、peek、empty四个操作。这道题看着简单但它经典的“坑”在于pop操作时如果出栈不为空则不能盲目从入栈倒数据必须等出栈的元素全部弹出后才能执行倒灌。否则多次依次pop时会出现顺序错乱的问题。另一个常见的变体是单调栈。网络安全场景里有些日志分析题会隐式用到单调栈比如“给一个数组求每个元素右边第一个比它大的元素”。卷子里有没有直接考到单调栈我不能完全确定但这类题在算法笔试中的出镜率实在太高了建议当作必会内容对待。单调栈的代码模板其实特别固定用java写就是维护一个栈栈内存下标遇到当前元素比栈顶元素大时就弹出栈顶并记录结果。关键点是弹出的时机是在“遇到更大的当前元素”时而不是遍历完后再处理。我当时刷题时总在这个地方写错后来我把模板固化成“四个步骤”for循环遍历数组while栈不空且当前元素大于栈顶元素pop并记录结果push当前元素入栈。笔试时直接套不费脑子。3.2 KMP的next数组我栽过的跟头KMP算法是这份卷子的重头戏。题目给出的模式串是pabacaba要求计算next数组。next[i]的定义是“模式串前i个字符组成的子串中最长相同前后缀的长度”——注意这里有个版本差异有的教材的next数组是从0开始的有的从-1开始还有的将next[i]定义为“失配时应该跳转到的位置”。如果不知道题目用的是哪个定义做出来的结果可能有差异。我当时按“前后缀最长匹配长度”来算手算结果如下先写出模式串各个前缀p[0..0]a没有真前后缀next[0]0p[0..1]ab前缀a后缀b不相等next[1]0p[0..2]aba前缀a后缀a相等且长度为1前缀ab后缀ba不相等next[2]1p[0..3]abac前缀a后缀c不相等前缀ab后缀ac不相等前缀aba后缀bac不相等next[3]0p[0..4]abaca前缀a后缀a长度为1前缀ab后缀ca不相等前缀aba后缀aca不相等前缀abac后缀baca不相等next[4]1p[0..5]abacab前缀a后缀b不相等前缀ab后缀ab长度为2前缀aba后缀cab不相等更长前后缀都不等next[5]2p[0..6]abacaba前缀a后缀a长度为1前缀ab后缀ba不相等前缀aba后缀aba长度为3更长前后缀不等next[6]3所以next数组为[0,0,1,0,1,2,3]。这道题如果采用“从-1开始”的版本结果会变成[-1,0,0,1,0,1,2]形式上差一个“整体右移并补-1”。笔试时遇到这类题我的经验是先看题目给的定义如果题目明确写了“next[i]定义为前i个字符的最长相同前后缀长度”那就用标准定义如果题目什么也没写建议在代码里用0开头版本并且在注释里说明含义。这样就算结果和判题系统有偏差至少思路是清晰的。KMP除了手算next数组还可能考“给定主串和模式串求匹配位置”。这种题我在“牛客”上刷过不少核心代码是KMP的匹配循环i指向主串j指向模式串如果匹配则i、j如果j等于模式串长度则说明匹配成功记录起始位置i-j并让jnext[j]继续找下一个匹配如果失配且j0则jnext[j-1]否则i。3.3 二叉树的遍历变形二叉树相关的题目在卷子里也占了一席之地。最经典的“层序遍历二叉树”要求按层输出节点用队列实现每层输出前先记录当前队列长度然后只处理这个长度个数的节点。这个细节很关键如果不记录长度直接用queue.size()当循环条件会导致层与层之间混淆。变形题是“之字形遍历”也就是第一层从左到右第二层从右到左第三层再从左到右。这里我推荐用“双端队列层号判断”的方式或者更简单一点先按层序收集每一层的列表然后根据层号奇偶性决定是否reverse。笔试场景下reverse虽然多了一次遍历但代码简单不容易错。还有一种常见变形是“最大深度与最小深度”。最大深度用递归一行搞定Math.max(maxDepth(root.left), maxDepth(root.right)) 1。最小深度则需要小心如果根节点只有左子树没有右子树最小深度不是1而是左子树的最小深度加1。这个边界条件特别容易错我在卷子里遇到过类似的题当时第一版写成了“取左右子树最小深度加1”结果在单支树上直接就错了。4. 图论与经典算法从Dijkstra到拓扑排序4.1 最短路径与Dijkstra奇安信这份卷子里也出现了最短路径问题。Dijkstra算法是限定在“边权非负”的条件下使用的如果图里有负权边那就需要Bellman-Ford或者SPFA来处理。我在复盘时发现很多同学容易忽略一个考点Dijkstra为什么不能处理负权边因为Dijkstra在每次从优先队列中取出距离最小的点后就认为这个点的最短路径已经确定了贪心思想但如果存在负权边后续可能出现“通过另一个点再绕过来反而距离更小”的情况这就推翻了这个点的“最短距离已经确定”的前提。手写Dijkstra时我建议用优先队列优化版本也就是“堆优化的Dijkstra”。核心模板如下int[] dist new int[n]; Arrays.fill(dist, Integer.MAX_VALUE); dist[start] 0; PriorityQueueint[] pq new PriorityQueue((a, b) - a[1] - b[1]); pq.offer(new int[]{start, 0}); while (!pq.isEmpty()) { int[] cur pq.poll(); int u cur[0], d cur[1]; if (d dist[u]) continue; // 跳过过期的队内元素 for (int[] edge : graph.get(u)) { int v edge[0], w edge[1]; if (dist[u] w dist[v]) { dist[v] dist[u] w; pq.offer(new int[]{v, dist[v]}); } } }笔试时容易忽略的细节是“跳过过期元素”那一行。如果不写虽然逻辑上不影响结果但同一节点会被重复入队多次复杂度会退化。加上这行判断代码看上去更专业效率也有保证。4.2 拓扑排序与有向图拓扑排序在这份卷子里是以“课程安排类问题”的变体出现的。题目大概意思是给定一组任务之间的依赖关系判断这些任务能否全部完成如果可以输出一种合法的执行顺序。这就是经典的拓扑排序问题可以用Kahn算法解决也就是“不断删除入度为0的节点”。思路很简单先把所有入度为0的节点入队每次取出队首节点把它加入拓扑序列然后删除这个节点发射的所有边即把它的邻居节点入度减1如果邻居节点入度变为0则入队。最后判断拓扑序列的长度是否等于节点总数如果不等说明图里有环。Kahn算法的代码模板非常固定笔试时我建议直接背下来。这里有一个容易忽略的细节如果题目要求“输出字典序最小的拓扑序列”那就要把普通队列换成优先队列保证每次取出的都是当前入度为0且编号最小的节点。我在不少笔试里都见过这个变形而且很多同学会因为没注意到“字典序”三个字而丢分。5. 机器学习与深度学习算法岗位的额外一关5.1 机器学习基础概念这份卷子的后半部分转向机器学习基础重点是概念理解而不是手推公式。但这里的“概念理解”并不是能说出定义就行而是要能解释原理、说明应用场景、对比不同算法的优劣。举个典型的例子KNNK近邻这个算法。题目可能会问“KNN的三个核心要素是什么”。标准答案是“距离度量、K值的选择、分类决策规则”。距离度量常用欧氏距离或曼哈顿距离K值太小会过拟合太大则模型过于平滑分类决策规则一般是多数投票回归问题取均值。KNN是典型的“惰性学习”算法——训练阶段什么都不做预测阶段才进行计算。这就带来了一个问题当训练集很大时每次预测的耗时都很高。还有一道可能出现的题是“梯度下降的几种变体对比”。批量梯度下降BGD每次迭代使用全部样本准确但慢随机梯度下降SGD每次只用一个样本快但震荡大小批量梯度下降Mini-batch GD是两者的折中也是实际工程中最常用的方案。我当时复习时习惯用“打靶”来类比这三者的区别BGD是瞄得很准再开一枪SGD是看到了就打一枪Mini-batch是半自动连发。类比不一定严谨但确实容易记。5.2 智能优化算法粒子群与模拟退火这是我做这份卷子时比较意外的一部分。网络安全领域经常需要求解一些组合优化问题比如网络流量调度的最优策略、日志特征选择的最优子集等这些问题的求解往往依赖启发式算法。因此奇安信考粒子群算法和模拟退火算法就说得通了。粒子群算法的核心概念是“粒子”和“速度-位置更新公式”。每个粒子代表解空间中的一个候选解粒子在每一轮迭代中根据个体历史最优位置pbest和群体历史最优位置gbest来更新自己的速度再根据速度更新位置。速度更新公式是v w * v c1 * r1 * (pbest - x) c2 * r2 * (gbest - x)其中w是惯性权重控制粒子保持原有速度的能力c1是“个体认知”学习因子c2是“社会认知”学习因子r1、r2是[0,1]之间的随机数。模拟退火算法的核心概念则是“以一定概率接受更差的解”。它的名字来源于金属退火工艺金属加热到高温后缓慢冷却原子在冷却过程中逐渐进入能量最低的晶体状态。算法在每次迭代中会随机生成一个新解如果新解更优则接受如果新解更差则以exp(-ΔE/T)的概率接受其中ΔE是新旧解的差值T是当前温度。温度随着迭代而降低这意味着算法前期可以“容忍差解”来跳出局部最优后期则逐渐收敛到最优解附近。对于这类算法我当时复习时倾向于把它当作“选择题”级别的知识点来对待——记住核心公式理解算法的核心思想知道应用场景。如果在Java或C里手搓一个粒子群算法题量会比较大但真遇到时也不用慌按照“初始化粒子群—评估适应度—更新个体极值与全局极值—更新速度和位置—判断是否终止”这个流程写不会有太多意外的坑。6. 笔试实战经验时间分配、环境与避坑6.1 时间分配策略复盘整套卷子后我意识到时间分配可能是决定成绩的关键因素。这套卷子的题量不小除了算法题还有概念题和简答题如果在一道题上卡太久后面的题很可能来不及做。我的建议是将时间分成三块。第一块时间给“送分题”链表反转、数组操作、括号匹配这类基础题尽量在30分钟内搞定而且一次写对不要反复调试。第二块时间给“核心题”KMP手算、层序遍历、贪心与DP题这一部分是拉开差距的关键。第三块时间留给“简答题/概念题”和检查。尤其是手算next数组、画树、写状态转移表这类题即使算法题没写完检查一遍手算结果也能挽回一些分数。6.2 环境与代码细节笔试时用的在线编辑器通常没有本地IDE那么智能没有自动补全没有代码错误提示。这种情况下平时依赖IDE的同学会特别吃亏。我自己就有过惨痛经历在本地IDE上写得飞快一上笔试环境连import都要手写结果一个字节输入流的读取代码写了好久。所以我的建议是笔试前一周去牛客或者力扣的在线模拟环境里练至少两次完整的笔试流程包括读题、写代码、自测用例。重点练两件事一是Scanner和BufferedReader的快速写法二是自己构造测试用例的能力。特别是树和图类型题目输入数据往往需要用数组或邻接表手动构造这个环节不熟练会很浪费时间。还有一个细节部分在线判题系统对Java主类名有要求通常必须是Main否则无法编译。有时候代码里忘了写import java.util.*在本地IDE能跑但在判题系统里直接编译失败——这类“非算法能力”的低级失误其实是最可惜的失分点。6.3 复盘后的几点体会把这份卷子从头到尾复盘完我最大的感触是安全公司的算法笔试远远不只是考算法本身。它更像是一场“基础能力工程习惯领域认知”的综合测试。基础能力体现在数据结构、字符串匹配、图论这些经典算法上这部分没有捷径就是踏踏实实把教科书里的经典算法吃透。工程习惯体现在代码风格、边界条件、输入输出处理这些“非算法”细节上。领域认知则体现在机器学习、智能优化算法的题目上——你需要理解这些算法在真实安全业务场景中是怎么用的。我在笔试时曾经觉得“粒子群算法怎么会出现在算法卷子里”后来做了几份安全公司的真题才明白这些算法恰恰是安全领域的常用工具。比如在流量异常检测中需要在高维特征空间里搜索最优特征子集这时候用粒子群算法就比暴力搜索高效得多。理解了这层关系再去看这类题就不会觉得突兀了。最后再分享一个小技巧。做任何算法卷子先花三分钟通读全卷给每道题标注难度一看就会的标A需要想一想的标B完全没思路的标C。然后按A→B→C的顺序做题。这看起来是很简单的操作但它能保证你不在最后没时间的时候才发现后面有两道送分题没做。我在后续的很多笔试里都用了这个方法效果比“按顺序硬做”好不少。
返回列表