1. 2017年滴滴工程岗笔试到底在考什么:岗位画像与考察逻辑
2017年秋招那会儿,网约车行业刚经历一轮大洗牌,滴滴技术团队处在快速扩张期,校招笔试的筛选强度相当高。我当年也是从这批题目里摸爬滚打过来的候选人之一,后来做了技术面试官,又回过头来带校招生,再反观这批笔试题,发现它其实是很有代表性的——它不追求题目本身有多难,而是特别看重三件事:基础功底扎不扎实、代码能不能一把过、业务场景下有没有工程直觉。这份真题汇总并不是某个人凭记忆写出来的“标准答案”,而是当年一批候选人考完后在各个社区、群里对题复盘沉淀下来的集体回忆,再结合我自己的应试和阅卷经验重新整理的。对于现在准备大厂工程岗笔试的人来说,这套题隔着几年去看,反而能看出大厂笔试出题的“底牌”。
1.1 当年的技术栈与岗位画像
先还原一下2017年滴滴的技术背景。那时候主后端语言是Java,Python大量用于数据处理和策略团队,Go开始在一些基础架构团队试点。所以工程岗的笔试题目里,Java系的考题占比非常重,比如JVM内存模型、线程池参数、HashMap在JDK 7和8之间的差异,这些几乎年年出现。客户端工程岗则更偏向Java/C++基础、Android的Activity启动模式、iOS的RunLoop这类偏平台的题。算法岗单独有算法和概率统计题,纯后端工程岗不会考那么深,但也会有一两道梯度题来筛人。
1.2 笔试的整体结构与时间分配
在线笔试一般是90到120分钟,题型组合通常是“单选题 + 多选题 + 编程题 + 简答题”混着来。单选题15到20道,覆盖Java基础、操作系统、计算机网络、数据库;编程题2到3道,难度梯度明显,第一道是水题,第二道是经典算法变形,第三道往往带点业务色彩;简答题给的是场景设计或智力题,数量不多,但很致命,因为很多人前面编程题写太久,简答题直接没时间写。整张卷子的核心逻辑是:先看你的算法基本功,再看工程素养,最后用场景题测试思维方式。
1.3 不同岗位方向的出题侧重
后端工程岗的侧重点非常明确:Java并发、数据库索引优化、Spring里的依赖注入和事务传播、分布式理论里的CAP和一致性哈希。客户端工程岗则更关注内存管理、界面渲染机制、网络请求的优化策略,还有多线程同步。算法岗会考AUC、ROC、过拟合处理、朴素贝叶斯的实际使用场景。大数据工程岗则会出现Hadoop的shuffle过程、MapReduce的combiner作用、SQL的窗口函数这类题目。整个笔试的成绩不是按单一分数线的,而是综合权重算出排名,所以战略性放弃某一块是可行的,但算法题的分值加权极高,可以说“得算法者得笔试”。
2. 算法与数据结构真题还原:四道典型题的完整拆解
算法题是整张卷子的命脉,面试官阅卷时其实不会一行一行看代码,而是看你的思路是否符合最优解,边界处理是否周到,代码风格是否干净。我把当年出现频率最高的四类题原原本本拆解一遍,每一道都会给出题面、输入输出样例、最优解思路和完整代码,并且会把我在阅卷时看到的高频错误一并指出。
2.1 链表反转:循环与递归两种写法都要熟练
这道是笔试里最省时间的送分题,但也是挂掉人数最多的一道。题面很简单:“给定一个单向链表,返回反转后的链表头节点。”要求时间复杂度O(n)、空间复杂度O(1)。很多人一上来就写递归,但忽略了递归在长链表下会爆栈的风险;还有人迭代写不熟练,把next指针的暂存顺序写错,链表直接成环。
我用迭代给出标准答案:
class ListNode: def __init__(self, x): self.val = x self.next = None def reverse_list(head): prev = None curr = head while curr: next_node = curr.next curr.next = prev prev = curr curr = next_node return prev注意三件事。第一,next_node必须先暂存,否则curr.next被改写后就找不到原来的下一个节点了;第二,循环终止时prev就是新头节点;第三,空链表和单节点链表要直接返回。递归写法可以当作扩展练习,但笔试里我建议优先用迭代,因为不会触发栈溢出,代码也更容易现场调试。当年很多候选人迭代写不熟、递归写不优雅,最后这道题反而拿了低分,非常可惜。
2.2 最长无重复字符子串:滑动窗口的经典套用
这是当年编程题第二题里出现概率最高的一道。题面:“给定一个字符串,找出其中不含重复字符的最长子串长度。”比如输入"abcabcbb",输出3,对应的子串是"abc";输入"bbbbb",输出1。
暴力解法是枚举所有子串再判重,复杂度O(n^3),在笔试数据量下能拿到部分分,但不可能满分。标准解法是滑动窗口加哈希表,用左指针和右指针维护“当前窗口内的字符集合”,右指针不断右移,遇到重复字符时左指针跳到重复字符上一次出现位置的下一个位置。
def length_of_longest_substring(s: str) -> int: char_index = {} left = 0 max_len = 0 for right, ch in enumerate(s): if ch in char_index and char_index[ch] >= left: left = char_index[ch] + 1 char_index[ch] = right max_len = max(max_len, right - left + 1) return max_len这里的边界细节是:char_index记录的是字符最近一次出现的下标,当遇到重复字符时,只有这个下标落在当前窗口内(即>= left)才需要移动左指针;否则说明重复字符已经被窗口“滑过”了,不用动。加粗这个细节是因为很多人漏写了and char_index[ch] >= left,导致左指针回跳,结果完全错误。
2.3 动态规划变形题:带代价的爬楼梯
当年的动态规划题不是直接考“爬楼梯”,而是做了变形:“每次可以爬1级或2级台阶,但爬第i级台阶需要消耗cost[i]的体力,求爬到顶层的最小体力消耗。”这其实是LeetCode 746的母题,2017年的时候还没有那么多人刷LeetCode,所以很多候选人被唬住了,实际上它背后的状态转移非常简单。
定义dp[i]为到达第i级台阶时的最小总消耗,注意题目通常允许从第0级或第1级起步,所以dp[0]=cost[0]、dp[1]=cost[1],递推公式是:
dp[i] = cost[i] + min(dp[i-1], dp[i-2])最终答案是min(dp[n-1], dp[n-2])还是min(dp[n], dp[n-1]),取决于题目里“顶层”是指越过最后一个台阶还是站上最后一个台阶。我在阅卷时发现,很多人公式写对了,栽在这个阅读理解上。这类题教会我们一件事:笔试里的动态规划题,基本都不会脱离经典模型的骨架,你要做的是在最短时间内识别出它属于“线性DP”还是“背包DP”。
2.4 Top K问题:从快排partition到海量数据的堆解法
“找出数组里第K大的元素”这类题在选择题和编程题里都出现过。简洁而有效的方案是快速选择算法,基于快排的partition思想:一次partition后,基准元素落在它的最终位置pivot_index,如果pivot_index正好是第K大对应的下标就返回;否则只递归搜索半边。
def quick_select(nums, left, right, k): if left == right: return nums[left] pivot = nums[right] i = left - 1 for j in range(left, right): if nums[j] > pivot: # 找第K大,所以用降序partition i += 1 nums[i], nums[j] = nums[j], nums[i] nums[i+1], nums[right] = nums[right], nums[i+1] pivot_index = i + 1 if pivot_index - left == k - 1: return nums[pivot_index] elif pivot_index - left > k - 1: return quick_select(nums, left, pivot_index - 1, k) else: return quick_select(nums, pivot_index + 1, right, k - (pivot_index - left + 1))期望时间复杂度是O(n),最坏O(n^2),所以面试里最好提一句“可以通过随机化pivot来避免最坏情况”。如果题目进一步变成“海量数据里求Top K”,比如内存装不下全部数据,就不能用快排了,得用小顶堆维护长度为K的堆,每来一个元素就和堆顶比较,大于堆顶就替换并堆化,这样能保证时间复杂度O(n log K)、空间复杂度O(K)。从“第K大”到“Top K”再到“海量Top K”,这个层层递进的思路本身就是笔试出题人想看到的能力。
3. 系统工程与研发基础:Java、并发、数据库与网络
算法题过了之后,决定你能不能进面试的往往是工程基础题。这部分题目看起来是选择题,但坑位特别多,一道题能牵出好几个知识坑。我按当年的真题类型,把最常考的四个主题整理一下。
3.1 线程池参数为什么是当年的高频题
单选题里几乎每年都有一道“线程池核心参数”的题,比如给定一个场景,让你选择合理的corePoolSize、maximumPoolSize、workQueue容量和拒绝策略。2017年Java工程岗在笔试里考线程池,是因为滴滴的业务天然是IO密集型的——大量网络请求、异步消息、API调用。线程池在这类场景下用得极频繁。
核心规则是:CPU密集型任务,线程数设为N+1,N是CPU核数;IO密集型任务,线程数设为2N或更高,因为线程在等IO时可以切出去跑别的任务。队列策略也需要考虑:LinkedBlockingQueue不设上限时任务会无限排队,线程数永远到不了maximumPoolSize;SynchronousQueue不缓存任务,来一个就直接尝试创建线程。拒绝策略有四种:AbortPolicy直接抛异常、CallerRunsPolicy让调用线程执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃队列中最老的任务。很多人不知道CallerRunsPolicy的价值,它其实是一种天然背压机制,能让生产者放慢速度而不丢任务。
3.2 缓存穿透、击穿、雪崩的场景题
有一道多选题的题面是:“某个热点数据突然从缓存中过期,大量请求同时涌向数据库,应该如何解决?”这就是缓存雪崩/击穿问题。缓存穿透指查询一个根本不存在的数据,每次都会落到数据库;缓存击穿指某个热点key在过期瞬间,大量请求打到数据库;缓存雪崩指大量key在同一时间过期,造成数据库瞬间压力过大。
当年正确的作答逻辑是:穿透用布隆过滤器(Bloom Filter)或者缓存空值;击穿用互斥锁保证同一时刻只有一个请求重建缓存,或者用逻辑过期方式;雪崩则通过给过期时间加随机值打散,以及多级缓存兜底。布隆过滤器我当时在笔试里详细讲了一下,核心是它用多个哈希函数把key映射到一个大的位数组,判断“一定不存在”时有绝对结论,判断“存在”时有误判率,所以适合拦截穿透请求,但要注意适时重建过滤器,否则误判率会随容量上升。
3.3 SQL与索引优化:从explain看执行计划
数据库题的基本套路是:“有一张订单表,字段包括user_id、city_id、amount、create_time,查询语句为SELECT * FROM orders WHERE city_id = ? AND create_time > ?,现在查询很慢,你会怎么优化?”答案是联合索引(city_id, create_time),这直接对应最左前缀原则——查询条件里的city_id是等值比较,create_time是范围比较,把等值字段放在前面,能最大化索引的过滤效果。
更深入的考点是EXPLAIN输出里的type字段:ALL表示全表扫描,index表示扫描整棵索引树,range表示索引范围扫描,ref表示非唯一索引等值匹配,const表示按主键或唯一索引查询。当年有一道选择题就是给你四个type值,让你按性能从好到差排序。排序规则是const > ref > range > index > ALL,记不住吗?就记“能走索引就不扫全表,能用等值就不用范围”。还有一个高频考点是覆盖索引,也就是查询的列恰好都包含在索引里,EXPLAIN的Extra字段会显示Using index,这意味着不需要回表,速度会快非常多,实际做慢查询优化时这个手段用得最频繁。
3.4 网络、操作系统与Linux命令考点
网络题的高频内容是TCP三次握手:为什么不是两次?因为三次握手能防止旧的重复连接请求突然到达服务端时建立错误连接。为什么挥手要四次?因为TCP是全双工的,两个方向需要独立关闭。TIME_WAIT为什么要等2MSL?为了让最后一个ACK确认到达,也为了让旧连接的数据包在网络中彻底消失。
操作系统题常考进程和线程的区别、死锁的四个必要条件(互斥、占有等待、不可剥夺、循环等待)、LRU缓存淘汰算法的实现。Linux命令题则是给出场景让你选命令:查看内存用free -h,查看CPU用top,查看磁盘用df -h,排查端口监听用ss -lntp或netstat -lntp。日志查询里grep和awk的组合用的最多,例如统计一个访问日志里各状态码出现次数:
awk '{print $9}' access.log | sort | uniq -c | sort -rn这道命令实际在笔试里就是以“选择最合适的命令序列”出现的,当年很多人只记得grep,但uniq -c的去重计数才是统计的关键。
4. 智力题与场景设计题:当年让很多人翻车的实战题
这部分是当年笔试里区分度最高的题。算法题好赖还能刷出来,智力题和场景设计题考察的是逻辑思维的底层能力,临时抱佛脚很难作弊。我整理了当年回放率较高的三道题和它们的完整推导。
4.1 经典过桥题:四个人过桥的最短时间
题面是:四个人在夜晚过桥,桥一次最多走两人,必须用手电筒,手电筒只有一把,四个人的过桥时间分别为1分钟、2分钟、5分钟、8分钟,问所有人都过桥至少需要多少分钟。
我直接说结论:答案不是17分钟(1和2过去,1回来,1和5过去,1回来,1和8过去),而是15分钟。15分钟的走法是这样的:第一步,1和2过去,耗时2分钟,1回来,耗时1分钟,累计3分钟;第二步,5和8过去,耗时8分钟,2回来,耗时2分钟,累计13分钟;第三步,1和2再过去,耗时2分钟,累计15分钟。
这道题的关键洞察是:不能总让最快的1来回送手电,因为5和8如果分开过桥,光这两趟就要13分钟;让两个慢的人一起过桥,虽然单次耗时8分钟,但等于省下了“把其中一个慢的人再送回对岸”的成本。它的本质是运筹学里的“最小化总通过时间”,用到的是让最慢两人结伴、最快两人负责运送手电的策略。这道题背后是“如何通过任务编排降低整体成本”的工程思想,出行调度里其实很常见。
4.2 25匹马5个赛道找前三名
这道题是网络热词和历史题目里都绕不开的经典智力题。题面:“25匹马,5个赛道,每次最多同时5匹马比赛,没有计时器,最少比多少次能找出最快的前3名?”答案是7次。
推导过程:第一轮,把25匹马分成5组各比一次,得到每组排名,共5次,这样能确定每组第一名;第二轮,让5个小组第一名比赛,得到金组、银组、铜组第一名,这是第6次。第6次的结果非常关键,因为金组第一名就是总冠军,不用再比;此时总亚军的候选只有金组第二名;总季军的候选是金组第三名、银组第二名、铜组第一名。第七次就让这5匹马比赛,取前两名,加上总冠军,就是最终的前3名。
这个题的工程意义在于:它要求你建立“候选集”的概念,不要拿所有马去重比,而是通过已知信息淘汰不可能进入前三的个体。做系统设计和算法优化时也是同一套逻辑,先剪枝,再对缩小的候选集做精确计算。
4.3 拼车调度与动态定价的业务场景题
2017年滴滴笔试的简答题里经常直接出业务题:“用户打开App,输入起点和终点,点击叫车,系统如何在几秒内给乘客派到合适的车?”这类题没有标准答案,阅卷看的是你能否拆解出完整的技术链路。
我当时的答题框架大概是这样:用户发单后,先从LBS服务获取用户的经纬度坐标,再把坐标转换为GeoHash或栅格ID,之后在周围多级网格中查找空车列表,优先用距离和预计到达时间(ETA)排序候选司机,再结合司机的实时状态(接单中、空闲、收工)、方向偏好、服务分进行第二轮筛选。更深一层的考察点是:如果附近只有一辆车,但它在五公里外,而三公里外有个司机也快要完成上一单了,系统是派这辆车还是派那个司机?这就要提到就近派单和预约派单的权衡,你需要考虑的是“乘客等待时间最短”还是“平台整体效率最高”。答题时提到多级索引、ETA计算、订单生命周期管理这几块,基本就能拿到不错的分数。
4.4 附近车辆查询与短URL设计题
“如何实现‘查询用户周边500米内所有空闲车辆’”是另一道高频设计题。最直接的思路是:把地图切成边长为0.5公里的网格,每辆车根据实时位置落入某个网格,用户查询时先定位到自己所在网格,再查周围一圈共9个网格的车辆,通过距离计算过滤掉超过500米的。这个方案的核心是“空间索引”。更高级的做法是用Redis的GEO数据结构,它内部是一个有序集合(zset),支持GEORADIUS命令直接查询一个坐标点周围指定半径内的成员,响应速度极快。2017年的时候Redis GEO已经出现了,但很多候选人不了解,这个知识点成了加分项。
短URL设计的题面是:“给定一个长链接,把它转成一个短链接,要求接口支持高并发下短链接的生成和跳转。”核心方案有两个:一是发号器,用一个全局自增ID,再转成62进制字符串作为短码;二是随机生成短码,写入数据库时加唯一索引防冲突。跳转时用短码查原链接,然后返回302或301重定向。高并发下的关键是发号器不能用数据库自增当瓶颈,需要引入Redis的INCR或者两段式ID生成器。这类设计题不要求你写到分布式级别,但至少要有“单机方案 + 瓶颈识别 + 升级方案”的三段式思路。
5. 从真题看备考策略:一份可复用的复盘路线
真题的价值不在于背下来,而在于从真题里看出题人的偏好,再用有限的时间做精准的准备。我结合自己当年备战以及后来带新人校招的经验,把这套题的备考策略拆成四步,每一步都可以直接套用。
5.1 算法题的优先级与刷题量
看这组真题就能发现,高频类型非常集中:链表、二叉树、字符串、动态规划、贪心、二分查找。这六类覆盖了笔试里八成以上的算法题,准备的时候应该优先刷透它们。刷题数量上,目标是200到400道之间,每天保证2到3道,周末可以集中刷一套模拟题。我不推荐上来就做困难题,而是按“线性表 → 二叉树 → DP → 贪心/图 → 高级数据结构”的顺序推进。每道题做完后,一定要做一次复杂度分析,并且对比不同解法的适用场景。比如同样是用哈希表,处理字符串去重和处理数组两数之和是完全不同的思路,只有自己推过一遍,笔试时才不会蒙。
5.2 手写代码的规范与速度
在线笔试的编辑器没有IDE的自动补全和错误提示,手写代码的习惯必须提前养成。命名别用a、b、c,至少用node、curr、index这样有意义但不啰嗦的名字;写完代码后要自己在心里跑一遍边界用例,尤其是空输入、单元素、全相同元素这三类。笔试时我常建议候选人先写一版“可运行的暴力解法”,把基础分拿到,再在剩余时间里优化成最优解,这和真题判分逻辑是匹配的——部分用例通过也能拿到相应的分值。
5.3 错题本的记录方式
当年我自己用的错题本很简单,就是一个Markdown文件,每条包含五个字段:题目描述、我的错误点、正确解法、复杂度分析、同类题关联。这样记录的好处是,复盘时能一眼看到自己的思维盲区。比如如果你连续错在“滑动窗口的左指针更新条件”,那就说明你对区间维护的理解有问题,应该集中刷5道滑动窗口题,而不是继续做新题。复盘频率上我建议三天一回顾,每次只看错题,不重复做全对过的题,这样花的时间最少,收益却最高。
5.4 今天的求职者还能怎么用这批2017年真题
我知道很多人会问:2017年的题放到现在还有意义吗?我的回答是,题面可能旧了一点,但考点完全没有过时——滑动窗口、动态规划、线程池、缓存三兄弟、空间索引,这些到现在依旧是各大厂工程岗笔试的核心主题。尤其是那几道业务场景题,拼车调度、附近车辆查询,放到今天依然是热门业务里的经典玩法。所以别把这批真题当成历史资料,把它当成年份较早的“高频考点清单”,结合现在的题库按同样优先级去刷,效果会好得多。
最后再分享一个我个人做这套笔试时的小经验:做题顺序决定你能不能答完。我的习惯是先做编程题,再做简答题,最后兜底做选择题。编程题分值高且需要完整思路,优先在脑子清醒时完成;简答题只要列出框架就能拿部分分;选择题就算最后时间不够,蒙对一道是一道。当年不少候选人从选择题蒙头做到最后,编程题剩十分钟才开始写,结果所有题都拿了一半分——这属于典型的策略失误,希望准备笔试的人别踩同样的坑。