2018年春季,360公司春招笔试的编程题合集在各个求职论坛上传得挺火。作为一个当年亲身参加过360笔试、后来也参与过校招面试环节的过来人,我对这套题目的印象很深——它不像某些大厂笔试那样一上来就是竞赛级难题,而是把大量精力放在了基础数据结构、字符串处理和边界条件上。这篇文章我就把360春招笔试编程题的核心拆解一遍,说说每道题背后到底在考什么,以及我当年踩过的那些坑。
如果你是准备投递360或者其他互联网公司后端、算法岗的应届生,这篇内容会帮你理清准备方向。即使你不面360,这套题目的出题风格也很有代表性:不追求偏题怪题,而是考察你在有限时间内能不能把基础题写出干净、正确、高效的代码。这恰恰是校招笔试最真实的筛选逻辑。
1. 360春招笔试考什么:整体结构与考察意图
1.1 题型构成与时间分配
2018年360春招笔试的编程题部分,整体上分为两个大的方向:一类是纯算法题,需要手写完整解法;另一类是偏工程实现的选择题或简答题,考察语言基础和操作系统、网络等计算机基础知识。编程题的数量一般在3到4道左右,时间大约是两个小时,也就是说每道题平均只有30到40分钟。
这个时间压力是很真实的。我当年拿到试卷的第一反应是先扫一遍所有题目,而不是从第一题开始闷头做。为什么?因为不同题目的难度差异可能很大,如果第一题卡住了,后面能做出来的题也没时间写。我的建议是:先花两分钟通读全部题目,按“能不能马上想到思路”给题目排个序,把最有把握的题先拿到分,再回头啃难题。这套策略看着简单,但我在考场上帮了大忙。
1.2 笔试真正的考察目标
很多人以为笔试就是在考“会不会做题”,其实不完全是。360的笔试题目有一个很明显的特点:它更看重你的代码是否完整、是否能处理边界情况、是否有良好的代码习惯。举个例子,同样是实现一个字符串处理函数,能想到处理空字符串、超长输入、特殊字符的考生,和只写了一个主流程就交卷的考生,在面试官眼里的差距是巨大的。
另外,笔试中出现的算法题,大部分是《剑指Offer》和LeetCode中等难度题目的变体。这意味着你不需要去刷那些极难的数据结构题,但一定要把常见算法模板练熟。我当时在准备阶段就把“手写快排”“手写二分”“手写链表反转”这类基础操作练到了肌肉记忆的程度,后来在笔试中确实派上了用场。
1.3 网上热词里的另一面
顺便说一句,最近网上关于360安全卫士纯净版、360壁纸卸载、360浏览器怎么彻底卸载的讨论很多。作为技术人员,我对这些工具类问题不太感冒,但这里提醒一句:在准备笔试的过程中,与其花时间去折腾怎么卸载某个软件,不如把精力放在算法题上。360这家公司虽然以安全软件被大众熟知,但它的笔试题目其实非常正统,更看重你的计算机基础功。这部分我们接下来细说。
2. 高频题型一:字符串处理与模拟类题目
2.1 字符串类题目的通用套路
字符串处理是360笔试的高频考点,几乎每年都会出现。这类题目的特点是对代码实现的精细度要求很高,常见考察点包括:括号匹配、字符串解码、子串查找与替换、正则表达式简化版本等。
我做字符串题总结了一套通用思路:第一,优先考虑用栈来处理嵌套结构,因为字符串的嵌套匹配本质上是栈的天然场景;第二,如果题目要求对字符串进行多次变换,要特别注意每次操作后索引是否失效;第三,边界条件必须单独处理,比如字符串为空、长度为1、末尾是分隔符等情况。
很多同学在笔试时字符串题容易超时,不是思路不对,而是用了一些O(n²)的暴力解法。比如“判断一个字符串是否由另一个字符串循环移位得到”这类题,如果你去拼接字符串再用contains判断,Java里就是O(n*m)的复杂度,但原题考察的其实是字符串匹配的优化思路。这种“看似简单、实则要优化”的题,正是360笔试喜欢出的类型。
2.2 经典题“字符串解码”的详细解析
当年360春招出现过一道字符串解码的题目,我记得很清楚,大意是输入一个形如“3[abc]2[de]”的压缩字符串,要求输出解压后的完整字符串“abcabcabcdede”。这道题在LeetCode上也有原题(394. Decode String),但考场上没有编译器提示,全靠自己写对边界,难度会高不少。
解题思路有两种。一种是递归法:解析到数字后,递归解析后续字符串,直到遇到与当前层匹配的右括号。核心代码如下(Python版):
def decode_string(s: str) -> str: def dfs(i: int): res = [] num = 0 while i < len(s): if s[i].isdigit(): num = num * 10 + int(s[i]) elif s[i] == '[': i, inner = dfs(i + 1) res.append(inner * num) num = 0 elif s[i] == ']': return i, ''.join(res) else: res.append(s[i]) i += 1 return i, ''.join(res) _, result = dfs(0) return result另一种是栈解法,用两个栈分别保存“数字”和“当前层拼接结果”。遍历时遇到左括号把当前结果入栈,遇到右括号出栈并重复拼接。推荐栈解法,因为不需要递归的额外栈空间,也更容易扩展到更复杂的语法。
这道题我在考场上第一版就漏了“嵌套数字可能是多位数”的情况,比如“12[a]”应该解析成“aaaaaaaaaaaa”而不是“2[a]”。这个坑希望大家注意,笔试中多写几个测试用例自测一下是值得的。
2.3 模拟类题目的边界处理心得
模拟类题目在360笔试中也占一定比例,比如“按照规则模拟一个进程调度”“模拟一个库存系统”。这类题本身算法难度不大,但特别容易在“题意理解偏差”上失分。
我的经验是把题目中的规则逐条翻译成代码注释,一条规则对应一个函数或一个分支,比如“如果库存不足则拒绝本次请求”“如果时间冲突则跳过该任务”,这样写出来的代码结构清晰,也方便自我检查。另外特别注意:模拟题经常会考察“同一时刻发生多件事”的处理顺序,要先决定优先级再动手写代码,否则改起来非常痛苦。
3. 高频题型二:数据结构设计与LRU缓存
3.1 为什么笔试钟情LRU
360笔试中多次出现“设计一个LRU缓存”这道题。它之所以被各家公司轮流考察,是因为它一道题就能考察多个核心能力:双向链表操作的熟练度、哈希表的运用、O(1)时间复杂度的设计思路,以及对“缓存淘汰策略”背后的业务理解。
很多同学能背出LRU(最近最少使用)的概念,但一到手写就卡住了。卡住的核心原因是:不知道为什么要用“哈希表 + 双向链表”的组合。哈希表负责O(1)查找,双向链表负责O(1)插入和删除。如果只用数组,每次访问后调整顺序需要O(n);如果只用链表,查找需要O(n)。只有两个结构配合,才能保证get和put都是O(1)复杂度。
3.2 手写LRU的实现与复杂度分析
我直接给出Java版本的标准实现,这是我在笔试中常用的模板:
import java.util.HashMap; class LRUCache { class Node { int key, value; Node prev, next; Node(int key, int value) { this.key = key; this.value = value; } } private final int capacity; private final HashMap<Integer, Node> map = new HashMap<>(); private final Node head = new Node(-1, -1); // 哨兵节点 private final Node tail = new Node(-1, -1); public LRUCache(int capacity) { this.capacity = capacity; head.next = tail; tail.prev = head; } public int get(int key) { if (!map.containsKey(key)) return -1; Node node = map.get(key); moveToTail(node); return node.value; } public void put(int key, int value) { if (map.containsKey(key)) { Node node = map.get(key); node.value = value; moveToTail(node); } else { if (map.size() == capacity) { Node removed = head.next; removeNode(removed); map.remove(removed.key); } Node newNode = new Node(key, value); map.put(key, newNode); addToTail(newNode); } } private void removeNode(Node node) { node.prev.next = node.next; node.next.prev = node.prev; } private void addToTail(Node node) { node.prev = tail.prev; node.next = tail; tail.prev.next = node; tail.prev = node; } private void moveToTail(Node node) { removeNode(node); addToTail(node); } }这里最容易被忽略的是哨兵节点的设计。用head和tail两个哨兵,可以省去大量“节点是否为空”的判断,这也是实际工程中链表实现的常用技巧。
时间复杂度:get和put都是O(1),空间复杂度O(capacity)。面试中如果被问到“为什么能O(1)”,你就要把这个哈希表和双向链表的分工讲清楚。我当年在笔试后单独被面试官追问过“如果让你不用内置HashMap,你还能实现O(1)查找吗”,这个扩展问题考的是你有没有真正理解哈希表的原理。
3.3 缓存容量选择的考量
虽然笔试中LRU的capacity是输入参数,但实际系统设计时容量选择大有文章。比如你给数据库查询做缓存,容量太小命中率低,容量太大占用内存。通常的做法是根据平均单个缓存项的大小和可用内存来估算,比如每个缓存项平均4KB,机器可用内存2GB,加上系统其他开销,那么容量大概可以设为10万量级。
笔试不需要你考虑这么细,但这个思考过程可以用来应对面试的追问。我当时就补充了一句“如果缓存项大小差距大,可以用加权LRU的变体”,面试官明显对这轮回答比较满意。
4. 高频题型三:动态规划与贪心算法
4.1 经典题“圈地运动”的几何思考
360笔试考过一道有点意思的题,叫作“圈地运动”。题目大意是:给你一组正整数数组,每根木棍的长度已知,问从数组开头取连续的前n根木棍,最少取多少根,才能围成一个闭合多边形。
我第一次见这道题时愣了一下,因为它披着几何的外衣,实际上是一个数学判断加贪心扫描的题。核心是“多边形判定定理”:给定n条边,能围成多边形的充要条件是“最长边小于其余所有边之和”。换句话说,n > 2且maxLen < sum - maxLen。基于这个判断,直接从前向后扫描,每次维护前缀和和当前最大值,第一个满足条件的位置就是答案。
def min_fence_count(lengths): prefix_sum = 0 max_len = 0 for i, length in enumerate(lengths): prefix_sum += length max_len = max(max_len, length) if i >= 2 and max_len < prefix_sum - max_len: return i + 1 return -1这个解法的复杂度是O(n),空间O(1)。如果你去暴力枚举所有组合,那复杂度是O(n³),在n较大时必然超时。这道题告诉我们:笔试中的“几何题”往往是幌子,真正考的还是数学建模和扫描法的基本功。
4.2 动态规划的状态设计思路
动态规划是360笔试的绝对主力题型。我记得出现过类似“找零钱最少硬币数”的变体和“最大子数组和”的变体。这类题目的核心不是写代码,而是“定义状态”。
我总结了一个遇到DP题的思考顺序:第一步,看题目能否分解成更小的相同问题;第二步,定义一个数组dp[i],明确dp[i]表示“以i结尾时的最优值”还是“前i个元素的最优值”;第三步,找状态转移方程,用前一个状态表示当前状态;第四步,确定初始化条件。以“最大子数组和”为例,dp[i]表示以第i个元素结尾的连续子数组的最大和,则dp[i] = max(nums[i], dp[i-1] + nums[i]),最终答案是max(dp)。
笔试时最容易错的是初始化和边界。比如数组为空时返回什么?只有一个元素时dp数组能不能直接遍历?我自己的习惯是写DP之前先想好“最小规模的例子”,比如n=1时程序会怎么走。这个习惯帮我避开了大量低级错误。
4.3 什么时候用贪心,什么时候用DP
有些题目看起来既可以用贪心也可以用DP,比如“跳跃游戏”这类。360笔试中如果出现这种题,我的经验是:优先尝试贪心,因为贪心代码量更少、不容易出错,但如果题目要求“求所有方案中的最优数量”,大概率要用DP,因为贪心只能求“是否可行”。
这里有一个经典区分点:如果每一步的选择会影响后面的选择,且局部最优不一定导致全局最优,那就需要DP;如果每一步都可以通过一个简单规则选出当前最优,且这个选择不会影响后续判断,那就是贪心。考场上判断错了会非常浪费时间,所以建议大家考前把两类题目各刷20道,形成直觉。
5. 高频题型四:图论与搜索
5.1 最短路径与拓扑排序的实战
360笔试中的图论题一般不会太复杂,常见的是单源最短路径和拓扑排序。单源最短路径如果图中没有负权边,直接用Dijkstra;如果节点数少但边数多,也可以考虑Floyd。但考场上最保险的其实是“从每个节点出发做BFS”的暴力思路,因为笔试题的图通常不大,正确性比最优复杂度更重要。
拓扑排序考得也很多,特别是和“课程安排”“依赖关系”相关的题目。判断有向图是否存在环,最常见的解法是Kahn算法(基于入度)和DFS三色标记法。我建议把Kahn算法背熟,因为它还能顺便输出拓扑序列,适用面更广。
5.2 并查集在连通性问题中的应用
并查集是笔试中的“隐藏常客”。很多看起来是图搜索的题,其实用并查集能写得更简洁。比如“判断两个节点是否连通”“统计岛屿数量”这类题,并查集的代码简洁且不容易错。
class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 路径压缩 return self.parent[x] def union(self, x, y): root_x, root_y = self.find(x), self.find(y) if root_x == root_y: return if self.rank[root_x] < self.rank[root_y]: self.parent[root_x] = root_y elif self.rank[root_x] > self.rank[root_y]: self.parent[root_y] = root_x else: self.parent[root_y] = root_x self.rank[root_x] += 1这段代码里的路径压缩和按秩合并是性能关键。理论上,加入了这两个优化后,并查集单次操作的时间复杂度约为反阿克曼函数,在现实中可以认为接近O(1)。笔试时不需要跟面试官解释反阿克曼函数,但“为什么find里要路径压缩”这个问题要能答上来。
6. 实战模拟:完整笔试过程中的踩坑记录
6.1 时间分配失误的典型场景
我当年做360笔试时,时间分配出现过一次失误。前两道题我花了大把时间去做最复杂的优化,结果第三道题虽然思路很简单,但因为剩余时间太少,代码写得太急,出现了下标越界的低级错误。这个教训很惨痛:笔试不是竞赛,不求“最优化解”,而是求“完整AC”。
我的调整策略是:每道题先写一个最暴力的版本,保证小规模数据能通过,然后如果时间充裕再优化。暴力版本往往能帮你理清思路,而且在测试用例不大时暴力版本也能拿不少分。千万不要一上来就写最复杂的解法,一旦中间断逻辑,调试时间就会成倍增加。
6.2 编译器与IDE选择
360笔试的平台通常支持你自己选择语言和本地IDE。我的建议是:用你最熟悉的语言,不要为了“显得高级”去用不熟的冷门语言。Java选手一定要把HashMap和LinkedList的API记熟;Python选手要注意递归深度,如果题目数据量较大,手写栈比递归更稳妥。
还有一个很多人会忽略的点:本地代码能跑通,不代表平台能跑通。原因可能是主类名、包名、输入输出格式不对。笔试平台通常要求输出“严格匹配”,多一个空格、少一个换行都可能判错。提交前务必检查:是否把调试用的System.out.println删掉了?是否按题目要求处理了多组输入?这些细节我见过太多人丢分。
6.3 多测试用例的推导技巧
笔试现场其实允许你用“小数据测试法”来验证算法。比如写一段随机数据生成器,或者手写几个极端案例,包括:空输入、单元素输入、全部相同输入、逆序输入、最大数值输入。这比盲目改代码有效得多。
我自己的经验是,每个算法写完必须验证这三类案例:一是规模最小的(长度为0或1),二是规模大但值分布的(如全正、全负、正负交替),三是数据边界值(如用int的最大值)。把这些案例过一遍,至少能排查掉八成以上的隐性bug。
7. 备战建议与常见问题排查
7.1 刷题优先级
如果你距离笔试还有一个月,我给一个可执行的刷题优先级:第一优先级是线性表操作,包括链表反转、删除重复元素、合并有序链表;第二优先级是字符串问题,尤其是LeetCode字符串分类里的中等难度题;第三优先级是DP的经典模型,包括背包问题、最长公共子序列、最长递增子序列;第四优先级才是图论和高级数据结构。
这个顺序是根据360以及同类公司笔试出题频率排的。先把基础题刷透,把代码写得又快又准,比刷一百道难题但都是“看了答案才会”要强得多。刷题过程中建议自己写题解,不用发出来,写给自己看就行。写题解的过程会强迫你想清楚“为什么这么做”,而不是“我背了个模板”。
7.2 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 本地输出正确但平台判WA | 输出格式不匹配 | 检查空格、换行、大小写 |
| 数组越界 | 边界条件未处理 | 在循环入口加索引判断 |
| 递归栈溢出 | 递归深度过大 | 改为显式栈或迭代 |
| 死循环 | while条件未更新 | 检查循环内是否有break或变量更新 |
| 超时 | 算法复杂度过高 | 换用哈希表/前缀和/动态规划优化 |
| 输出多了调试日志 | 忘了删除System.out.println | 提交前全局搜索print |
说到底,笔试只有一件事:在压力下写出正确代码。与其焦虑题海无边,不如把历年真题反复做三遍,每一遍都按考试标准要求自己。当你拿到一套题,能稳定地在一小时内AC两道中等题、半小时内AC一道简单题的时候,通过笔试就不成问题了。
我在实际备赛过程中最深的感受是:题库会变,但考察的能力不会变。360这套题合集里暴露出的要点——字符串处理、LRU、DP状态设计、图论基础——放在今天依然不过时。你把这些基础能力练扎实了,不管是去360还是其他公司,笔试这条路都会好走很多。最后再分享一个小技巧:每次做完题,花十分钟把题目的核心考点写在一张索引卡上,考前翻一遍比重新刷一遍题效率高得多。