滴滴2016研发笔试题全解析:从TCP慢启动到派单系统 📅 发布时间:2026/8/29 21:17:24 👁 浏览次数: 1. 这套“老题”的整体设计与考察逻辑1.1 先聊聊题量和模块分布滴滴出行2016研发工程师笔试题五在公司校招题库序列里属于比较典型的“第四梯队”卷子——不算是难度封顶的那套但覆盖面和陷阱密度都很有代表性。整套题一般是120分钟客观题大概占30到40分剩下是两到三道编程题和一道场景设计题。这种结构放到现在看可能觉得平淡但在2016年前后它恰恰代表了当时互联网公司筛选研发的标准姿势基础课必须扎实代码能力必须手写过关同时还要求你能把技术落到业务场景里。模块分布通常是这样的计算机网络、操作系统、数据结构、数据库各出几道客观题占分不算高但数量多主观题部分有一道链表、一道字符串或动态规划外加一道跟滴滴自身业务强相关的场景题。这种“基础算法业务”的三段式结构其实是后来很多大厂笔试的模板只是那时还没有那么多公司把题库标准化。1.2 出题逻辑不是难倒你是过滤你我后来自己也参与过几次校招命题再看这套题能明显感觉到出题人的意图不是“把所有人都考倒”而是“快速过滤掉不适合做研发的人”。比如客观题里会埋一些常见的混淆概念你如果只是背过名词解释很容易在两个选项之间犹豫编程题如果只会背题解边界条件一改就会翻车场景题更是没有标准答案考察的是你拆解问题的思路。这套题放到今天仍然值得拿出来做不是因为题目本身有多新而是它的考察粒度控制得很好不要求你写红黑树但要求你知道哈希表和二叉树在什么场景下选哪个不要求你精通TCP所有细节但要求你理解拥塞控制和高并发服务之间的关联。这种“考核颗粒度”是很多后来者没学到的——题不在难在于能不能筛出真正有工程感的人。2. 核心笔试考点逐题拆解2.1 计算机网络慢启动到底慢在哪这套题里计算机网络部分的经典考法是给你一段关于TCP拥塞控制的状态描述然后让你判断慢启动阶段拥塞窗口的变化规律。题目大概是这样还原的在一个TCP连接中发送方的拥塞窗口cwnd初始为1个MSS经过一个RTT后变为2个MSS再经过一个RTT后变为4个MSS。请问该阶段属于TCP拥塞控制的哪个阶段拥塞窗口的增长方式是什么答案是慢启动阶段增长方式是指数增长也就是每经过一个RTT拥塞窗口翻倍。很多人会在这里混淆“慢启动”这个名字以为慢启动就是慢慢涨其实恰恰相反慢启动的“慢”是相对早期TCP直接注入大量数据而言的从1个MSS开始哪怕指数增长前期也不会一下子把网络打爆。这个考点在滴滴的业务背景下非常合理。滴滴的服务端要面对海量的长连接和短连接请求尤其在早晚高峰司机端和乘客端的实时通信非常频繁。如果TCP拥塞控制处理不当网络拥塞会导致消息延迟直接影响订单状态推送的及时性。所以考官出这道题表面是考拥塞控制实际上是在看你对高并发网络模型的底层机制有没有感觉。答题时注意不要把慢启动和拥塞避免搞混。判断标准很简单拥塞窗口小于慢启动阈值ssthresh时是慢启动指数增长达到或超过阈值后进入拥塞避免改为线性增长每经过一个RTT增加1个MSS。题目如果给状态变化的具体数值你就能很清晰地把这两个阶段区分开。2.2 操作系统进程线程考的是能不能讲清“为什么”操作系统部分的典型题目是让你判断关于进程和线程的哪个说法是错误的。四个选项通常长这样A. 进程是操作系统进行资源分配的基本单位B. 同一进程内的多个线程共享该进程的地址空间C. 线程是处理器调度的基本单位D. 线程是资源分配的基本单位答案选D。进程才是资源分配的基本单位线程是调度的基本单位。这道题的错误率其实不低因为很多人记住了“线程轻量、进程重量”这个结论但没搞清“资源分配”和“调度”分别对应谁。你只要记住一句话线程共享进程的资源自己不拥有独立资源所以它不可能是资源分配的基本单位。这道题背后还有一层意思就是滴滴这种业务场景里服务端大量使用多线程模型来处理并发请求。如果一个候选人连线程和进程的基本分工都说不清楚那后面关于线程安全、锁竞争、上下文切换开销的问题就更没法聊了。考官出这题是在给你的并发编程基础做一个底线体检。另外有个容易忽视的点如果题目追问“线程切换为什么比进程切换开销小”你要能答出“因为同一进程的线程共享地址空间和大部分资源切换时不需要切换页表”。页表切换是进程切换开销的大头这个细节能说出来说明你不是死记硬背。2.3 数据结构与数据库链表判环和索引命中数据结构部分的经典题目是判断单链表是否有环要求给出算法并分析时间和空间复杂度。标准解法是用快慢指针快指针每次走两步慢指针每次走一步如果链表有环两个指针一定会在环内相遇。时间复杂度O(n)空间复杂度O(1)。这道题的易错点在于证明“快慢指针一定会相遇”而不是“可能相遇”。很多人代码能写出来但问为什么一定会相遇就卡住了。简单说慢指针进入环后快指针已经在环内每走一步快指针相对慢指针逼近一步所以最多跑一圈多就能追上。数据库部分的题目通常会结合索引命中的场景比如表orders有索引(user_id, status)执行SQLSELECT * FROM orders WHERE status 1该索引是否会被使用答案是不会因为不满足最左前缀匹配原则。索引最左前缀的意思是查询条件必须从索引最左边的列开始连续匹配跳过第一列直接用第二列过滤会导致索引失效走全表扫描。这个知识点在滴滴的订单查询场景里非常重要运营后台经常要根据不同条件组合查订单索引设计不合理一个慢查询就能拖垮线上库。2.4 场景题派单系统背后考的不是算法是权衡整套题里最有滴滴特色的是场景设计题原题大意是系统需要把一笔新订单分配给附近的司机要求给出可行的分配方案包括数据结构、算法流程和复杂度分析。这类题目当年让很多只会刷LeetCode的候选人懵住因为没有一个固定的标准答案完全看你如何拆解。出题人想看到的是这些层次第一层能不能想到用距离排序取最近的司机第二层能不能意识到“最近”不等于“最合适”需要考虑司机当前状态、接单意愿、服务分等因素第三层能不能把多因子加权、实时更新、高并发下的性能优化这些工程问题纳入方案。你回答得越有层次说明你越接近一个真实的研发工程师而不是一个单纯的刷题机器。3. 编程题完整解法与代码实现3.1 链表翻转K个一组翻转链表的边界处理这套题的编程题第一题通常是K个一组翻转链表。题目描述很直接给定一个链表每K个节点一组进行翻转不足K个的保持原有顺序返回翻转后的链表。比如链表1-2-3-4-5K2时输出2-1-4-3-5K3时输出3-2-1-4-5。解题思路分三步走。第一步写一个辅助函数翻转一段链表并返回新的头尾节点第二步遍历主链表每数满K个节点就调用一次翻转函数第三步把翻转后的子链表和前后部分正确连接。难点不在翻转本身而在边界条件的处理链表长度为K的整数倍时怎么收尾最后一组不足K个时怎么保持原序以及头节点的更新。我给出一个C版本的参考实现struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseKGroup(ListNode* head, int k) { if (head nullptr || k 1) return head; ListNode dummy(0); dummy.next head; ListNode* prev dummy; ListNode* start head; ListNode* end head; while (true) { int count 0; while (end ! nullptr count k) { end end-next; count; } if (count k) break; // 翻转 [start, end) 区间的链表 ListNode* newHead reverseBetween(start, end); prev-next newHead; // start 翻转后成为这段的尾节点接到 end 前面 start-next end; // 移动 prev 和 start prev start; start end; } return dummy.next; }这里的翻转区间是左闭右开区间也就是说end指向的是下一组的第一个节点而不是本组的尾节点。这样设计的好处是统一处理逻辑不需要单独判断end是不是nullptr。翻转函数reverseBetween的核心写法是头插法遍历[start, end)的每个节点依次插入到新链表的头部。实际笔试时如果时间紧张可以先写一个“翻转整个链表”版本的reverse函数再在循环里裁剪出K个节点调用。这种写法虽然多了一些指针操作但逻辑更直白不容易出bug。我当年笔试就吃过亏想用递归写结果在返回条件的判断上卡了半天浪费了不少时间。3.2 字符串处理最小覆盖子串的滑动窗口实现第二道编程题常见的版本是给定一个字符串S和一个字符串T在S中找出包含T所有字符的最小子串。如果不存在则返回空字符串。比如SADOBECODEBANCTABC结果是BANC。这是经典的滑动窗口问题思路是先用两个指针left和right维护一个窗口right向右扩展直到窗口内包含T的所有字符然后left向右收缩在保持“包含T所有字符”的前提下尽量缩小窗口记录最小长度和起始位置。这里有一个关键优化点统计T中每个字符的需求量窗口内用另一个哈希表记录已包含的字符数量再用一个变量matched记录有多少个字符已经满足需求。这个matched变量的作用是把“判断窗口是否满足条件”从O(n)降到O(1)整体时间复杂度才能做到O(n)。参考实现如下string minWindow(string s, string t) { if (s.empty() || t.empty()) return ; unordered_mapchar, int need; unordered_mapchar, int window; for (char c : t) need[c]; int left 0, right 0; int matched 0; int start 0, minLen INT_MAX; while (right s.size()) { char c s[right]; right; if (need.count(c)) { window[c]; if (window[c] need[c]) matched; } while (matched need.size()) { if (right - left minLen) { start left; minLen right - left; } char d s[left]; left; if (need.count(d)) { if (window[d] need[d]) matched--; window[d]--; } } } return minLen INT_MAX ? : s.substr(start, minLen); }这道题最常踩的坑有三个。第一个是匹配条件的判断不要每次都重新遍历哈希表要用matched变量实时维护。第二个是窗口收缩时如果移出的字符是满足需求的字符要先matched再减window计数顺序反了会漏更新状态。第三个是返回值用start和minLen记录最优解最后再截取子串而不是在滑动过程中反复调用substr那样会引入不必要的开销。有些同学会问如果只要求“包含T的所有字符”而不要求顺序那是不是也可以用数组替代哈希表确实可以如果字符集限定为英文字母用int[128]的数组计数更快。但笔试时用哈希表更通用不容易因为字符集扩展而翻车。3.3 派单场景题从“最近司机”到“综合评分”编程大题之后的场景题我建议你按下面的结构来组织答案层次清晰能拿高分。先说最简单的方案把每个司机的位置看成平面上一个点新订单进来后遍历所有空闲司机计算欧氏距离取距离最近的司机派单。数据结构就是数组或列表时间复杂度O(n)。这个方案能答出来说明你具备最基本的算法意识但考官会追问司机数量多了怎么办比如一个城市10万司机每秒几百笔订单O(n)的扫描是扛不住的。这时候你要主动抛出空间索引的概念。通常的做法是把城市划分成网格每个网格内的司机用一个集合维护查询订单时只需要在订单所在网格和相邻网格内搜索司机可以把搜索范围从全城缩小到局部。进一步优化可以用四叉树或者GeoHashGeoHash在业界用得很多它把二维坐标编码成一维字符串可以做前缀匹配实现快速邻域检索。再进一步要回答“最近不是最合适”的问题。你需要提出多因子加权评分距离、司机服务分、接单率、当前载客状态、前往接驾的路况预测这些都作为评分因子最终算出每个候选司机的综合分取最高分派单。数据结构上用最大堆优先队列维护候选司机列表即可在O(log n)时间内取出最优司机。我建议你在答案中明确写出流程订单进入系统后根据订单位置计算出候选司机集合遍历集合计算评分并压入优先队列弹出一个最高分司机若该司机在几秒内未接单则回滚到第二高分设置一个超时机制防止订单长时间无响应。这种结构化、有兜底方案的回答才是出题人真正想看到的。4. 常见错误与避坑经验4.1 时间分配在客观题上死磕是最亏的那套笔试题120分钟客观题占比不高但分值再低也是分。最实际吃亏的是有些人在一道纠结的TCP题目上花了10分钟导致后面编程题没写完。我当年一个很大的教训是客观题第一感觉选完就过标记拿不准的最后如果有时间再回头看。因为客观题考察的是“熟练度”你第一反应不会的知识点再纠结5分钟也很难突然想通反而会干扰后续的做题状态。编程题的时间要留足。我给自己定的节奏是做完客观题后剩90分钟第一道编程题控制在25分钟第二道控制在35分钟剩下30分钟给场景题和复查。编程题先写核心逻辑再补边界条件而不是从第一步就开始纠结“万一根节点为空怎么办”那样容易陷入细节里出不来。4.2 代码边界条件题目做对容易全对难笔试评分通常有多个测试用例在后台跑边界条件是拉开差距的关键。链表翻转那道题特别要注意“链表长度不足k”和“链表为空”这两个边界。我在代码里用了dummy节点来统一处理头节点更新的情况这个技巧能省掉大量特判逻辑。滑动窗口那道题边界条件集中在字符串为空、T比S还长、T中的字符在S中不存在这三种情况。优雅的解法是用一个count变量记录“窗口中还缺多少个有效字符”count不为0时说明窗口还没覆盖T而不需要每次都遍历need表。这里分享一个习惯写完核心逻辑后花30秒检查一遍异常输入。不是废话很多候选人代码主体正确但因为没有判空三个隐藏用例直接挂了非常可惜。写代码前先想好“输入为空的返回值是什么”这是专业和业余的分水岭。4.3 场景题别只谈算法要谈系统场景题丢分最严重的情况是“通篇只讲了一个算法”。比如题目问“如何为新订单分配司机”有人只回答“用KD树求最近邻”然后就停了。这个答案不是错但只有算法没有系统。真实系统中还要考虑缓存怎么更新、司机位置上报的延迟怎么处理、订单和司机之间怎么防止重复匹配、派单失败后怎么降级。我建议的答题框架是先讲离线处理还是在线处理再讲数据结构选型然后讲具体算法流程最后讲容错。比如你可以说“司机位置通过长连接实时上报服务端聚合后写入Redis缓存缓存采用过期策略保证数据新鲜度订单进来时先查缓存再通过GeoHash找出候选司机利用评分模型排序后派单如果司机5秒未接单系统自动派给下一位候选司机同时将原司机短期加入黑名单防止再次派单”。这样一段话就把数据流、存储、算法、兜底都讲全了评卷人能直观感受到你有工程思维。5. 这套题背后的技术栈与备考启示5.1 从题目反推滴滴的业务形态2016年的滴滴正处于补贴大战和业务飞速扩张的时期系统要支撑的不仅是庞大的用户量还有高频的实时定位、订单匹配、价格计算、支付回调这些核心链路。你把这套笔试题的考点串起来看就会发现它们不是随机凑出来的TCP拥塞控制对应的是司机端乘客端海量长连接的稳定性进程线程对应的是高并发服务端的基础模型索引设计对应的是订单库的查询性能派单场景题对应的更是滴滴最核心的订单分发系统。所以准备这类公司笔试的时候不要只刷题还要花点时间研究公司的业务形态。滴滴强调LBS相关算法和高并发架构那你在复习时就要格外重视字符串、图论、动态规划这些高频算法同时多想想算法怎么落到真实系统里。面试官问“为什么考这个”实际也是在问“你能不能理解我们为什么在乎这个”。5.2 备考建议基础课复习的权重比想象中高很多人准备校招笔试时把90%的时间花在啃算法题上结果到了考场发现客观题里网络、操作系统、数据库的题才是最要命的。我的经验是算法题决定你能不能进到下一轮而基础题决定你是不是稳稳地把卷面分拿到手。客观题的分丢多了编程题全对也救不回来。我建议的复习配比是时间大致按照算法50%、计算机网络20%、操作系统和数据库各15%来安排。计算机网络重点复习TCP三次握手四次挥手、拥塞控制、HTTP和HTTPS的差异操作系统重点复习进程线程、死锁四个条件、进程间通信方式数据库重点复习索引原理、事务隔离级别、最左前缀匹配。这些知识点做到看到题不动脑子能选出来客观题这一关基本就没有威胁了。5.3 做一套题不如拆一套题最后多说一句。很多同学刷题数量不少但效果一般原因在于做完了就扔既不复盘错题也不尝试给题目做变形。这套滴滴笔试题我建议你按更高标准使用第一遍正常做对答案第二遍只看题目不看笔记在纸上详细写出每道题的解题思路第三遍试着把编程题改成变体比如把“K个一组翻转”改成“每隔K个节点翻转一次”把“最小覆盖子串”改成“最多包含K个不同字符的最长子串”看自己能不能马上反应出解法。这种拆解式的练习比盲目刷二十道新题更有效。因为你练的不再是见过的题而是可迁移的解题框架。框架到位了面对没见过的题目你也能快速拆解出错题人想考察的知识点。我在实际带新人时经常用这套2016年的题当练手材料。它不像现在的笔试题动不动就整复杂动态规划而是用很有分寸的难度把候选人的基础功底和工程素养同时暴露出来。我见过刷遍了LeetCode的人在这套题上栽跟头也见过基本功扎实的人花少量时间准备就高分通过。说到底笔试考的不只是你会不会更是你在压力下能不能稳定输出——这份稳定才是从笔试题到真实研发工作之间最需要的能力。