☰
京东2013研发笔试题解析:算法与基础能力考察
2026/10/24 1:52:26 网站建设 项目流程

1. 为什么一份2013年的笔试题还值得拿出来聊

先说个结论:京东2013年的研发笔试卷,放到今天依然是一份非常有参考价值的面试练习材料。不夸张地讲,这卷子上考察的核心东西——算法功底、操作系统理解、网络基础、数据库设计思维、逻辑推理能力——和现在大厂校招笔试的考察方向几乎没有本质区别。变的只是题目形式,不变的是对应聘者底层能力的要求。

2013年是什么概念?移动互联网刚进入爆发期,安卓和iOS开发岗位的需求激增,电商行业的后端架构正在从集中式向分布式过渡,京东自己也在“6·18”大促的倒逼下不断升级技术体系。那会儿的研发笔试题,出题人非常务实,几乎每一题都能在真实业务场景中找到对应物。理解了这一点,你会发现卷子上的很多题就不是“为做题而做题”,而是在模拟一个研发工程师日常工作中会遇到的真实问题。

这份卷子适合谁来参考?如果你是准备校招或跳槽的开发者,可以通过它检验自己的基础是否扎实;如果你是刚入行的新人,可以通过它了解大厂研发岗到底看重哪些能力;就算你已经工作了好几年,回头看看这些题,也能重新审视自己的知识体系有没有出现断层。我自己把这些题目重新做了一遍,边做边记录了一些思考和踩坑经验,整理在这里,希望对你有实际帮助。

2. 试卷整体考察逻辑:研发岗需要什么样的基本功

2.1 从题目分布看考察重点

整份试卷的题型分布大致是:客观题(选择题/判断题)加主观题(编程题/设计题/逻辑题)相结合。客观题覆盖的面很广,包括数据结构、算法、操作系统、计算机网络、数据库、Java/C++基础等;主观题则集中在算法编程和方案设计上。

这个结构本身就透露了一个信息:大厂研发岗的筛选,不只看你会不会写代码,更要看你的计算机基础是否成体系。很多人在准备笔试时有个误区,觉得只要刷LeetCode就够了。实际上,如果操作系统、网络、数据库这三门课的基础不牢,客观题部分很容易丢分,而这些基础知识恰恰是日常工作最常打交道的部分。

以我自己的体验来说,校招笔试中最怕的不是算法题不会做,而是明明代码能跑通,却答不对一道关于进程调度或TCP握手的选择题。原因很简单:算法题可以靠短时间刷题突击,但计算机基础需要长期积累,临时抱佛脚的效果非常有限。

2.2 各模块的权重与应对策略

如果给这份试卷的各个模块排个优先级,算法和数据结构是绝对的核心,大概能占到三成以上的分值。操作系统、网络、数据库加起来也差不多三成。剩下的是逻辑推理和综合设计题,考察的是你在信息不完整的情况下做出合理判断的能力。

针对这个权重分布,备考策略就比较清晰了:

  • 算法题必须亲自手写代码,不能只看不练。看十遍别人的题解,不如自己完整写一遍。2013年的笔试卷大部分还是纸上写代码,虽然现在很多公司改成了在线评测,但手写代码的训练仍然有价值,因为它强迫你在没有编译器的环境下把逻辑想清楚。
  • 基础概念要形成自己的知识网络。比如TCP的三次握手和四次挥手,不是背下来就完事,要能说清楚为什么需要第三次握手,为什么断开连接比建立连接多一次挥手。
  • 数据库设计题要结合业务场景思考,不要只背范式和索引原理。考官更想看到的是你在具体场景下的取舍能力。

3. 核心题型解析:算法与数据结构类——从题目到思路

3.1 数组与字符串处理题

这一部分通常会有一到两道基础题,考察对数组和字符串的熟练度。2013年的卷子里有一道比较有代表性的题目:给定一个数组,要求将数组中的0移动到末尾,同时保持非零元素的相对顺序。

这道题现在看起来很简单,但很多人在当时会写出双重循环的解法,时间复杂度是O(n^2)。最直接的优化是用双指针法:

void moveZeros(vector<int>& nums) { int slow = 0; for (int fast = 0; fast < nums.size(); fast++) { if (nums[fast] != 0) { swap(nums[slow], nums[fast]); slow++; } } }

这里的slow指针指向下一个非零元素应该放的位置,fast指针负责扫描整个数组。每次遇到非零元素,就把它换到前面去。整个过程只遍历了一次数组,时间复杂度O(n),空间复杂度O(1)。

这道题想考察的其实是两个核心能力:一个是能否从暴力解法出发进行优化,另一个是能否快速识别出“双指针”这个经典套路。在实际面试中,如果你先给出暴力解法,然后主动分析其不足并给出优化方案,面试官对你的评价会明显更高。因为这展示了你具备“先实现、再优化”的工程思维,而不是只知道一个标准答案。

3.2 链表操作题:反转与合并的进阶思路

链表题是笔试的常客,因为C/C++和Java的面试官都爱考。2013年的题目中有出现反转链表和合并两个有序链表的变体。

先看一个迭代法反转链表的基础模板:

ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr != nullptr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; }

这个代码的关键在于:在改变当前节点的next指针之前,必须先保存它的下一个节点,否则链表就断掉了。很多人在第一次写这道题时都会犯这个错误。

合并两个有序链表,递归写法非常优雅:

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { if (l1 == nullptr) return l2; if (l2 == nullptr) return l1; if (l1->val < l2->val) { l1->next = mergeTwoLists(l1->next, l2); return l1; } else { l2->next = mergeTwoLists(l1, l2->next); return l2; } }

但要注意,递归写法虽然简洁,却存在栈溢出的风险。如果链表很长,递归深度可能达到数万层。在实际工程中,我建议使用迭代法来实现:

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode* tail = &dummy; while (l1 != nullptr && l2 != nullptr) { if (l1->val < l2->val) { tail->next = l1; l1 = l1->next; } else { tail->next = l2; l2 = l2->next; } tail = tail->next; } tail->next = (l1 != nullptr) ? l1 : l2; return dummy.next; }

这里我用了“哨兵节点”(dummy node)的技巧,避免处理头节点为空的边界情况,代码会干净很多。这个技巧在实际工程中非常实用,处理链表类问题时可以多用。

3.3 排序与查找:快排不是背个模板就完事

排序算法在2013年的卷子里大概率会出现,最常见的是手写快速排序。快排的核心思想是分治:选一个基准元素,把比它小的放左边,比它大的放右边,然后分别对左右两边递归排序。

void quickSort(vector<int>& arr, int left, int right) { if (left >= right) return; int pivot = arr[left + (right - left) / 2]; int i = left, j = right; while (i <= j) { while (arr[i] < pivot) i++; while (arr[j] > pivot) j--; if (i <= j) { swap(arr[i], arr[j]); i++; j--; } } quickSort(arr, left, j); quickSort(arr, i, right); }

这里要注意几个细节:

  • 基准选择:取中间位置的值(而不是第一个元素),可以避免数组基本有序时退化到最坏情况O(n^2)。
  • 边界条件:递归的结束条件是left >= right,不能写成left == right,否则可能漏掉单元素区间。
  • 相等元素的处理:while循环里用的是arr[i] < pivot和arr[j] > pivot,而不是<=和>=,这样可以让相等的元素尽量均匀地分布在两侧,避免无限循环。

2013年的笔试环境通常不允许用调试器,代码写错了只能靠眼睛看。所以平时练习的时候,建议养成“写完代码立刻自己用几个测试用例在脑海里走一遍”的习惯,能大幅提高正确率。

3.4 树的遍历:递归之外的层序实现

树相关的题目也经常出现在笔试卷里。除了前序、中序、后序这些递归遍历,层序遍历(BFS)的迭代实现也很重要。

vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> result; if (root == nullptr) return result; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int size = q.size(); vector<int> level; for (int i = 0; i < size; i++) { TreeNode* node = q.front(); q.pop(); level.push_back(node->val); if (node->left) q.push(node->left); if (node->right) q.push(node->right); } result.push_back(level); } return result; }

注意这里必须先用int size = q.size()保存当前层的节点数,再进入for循环。如果直接在循环里写q.size(),因为队列在动态变化,你无法确定当前层到底有多少个节点,整个遍历逻辑就会错乱。

4. 计算机基础考点逐项拆解:操作系统、网络与数据库

4.1 操作系统:进程线程、死锁与内存管理的核心逻辑

操作系统相关的选择题和简答题,考察点通常集中在进程与线程的区别、线程同步、死锁产生的条件、虚拟内存和页面置换算法等。

先说“进程与线程的区别”这类题。很多人的回答是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话没错,但仅凭这一句拿不到满分。更好的回答应该展开一个层次:

  • 从资源角度看,进程拥有独立的地址空间,线程共享进程的地址空间。所以多线程编程天然就共享数据,但也因此需要加锁来保证线程安全。
  • 从调度角度看,线程的切换比进程切换的代价小,因为线程切换不需要切换页表等地址空间相关的上下文。
  • 从稳定性角度看,多进程程序的隔离性更好,一个进程崩溃不会影响其他进程;多线程程序中一个线程崩溃可能导致整个进程退出。

再来看死锁。死锁产生的四个必要条件:互斥条件、持有并等待条件、不可剥夺条件、循环等待条件。笔试中常考的点是“如何避免死锁”。注意区分“预防”和“避免”这两个概念:

  • 预防是破坏四个必要条件中的任何一个。比如要求进程在持有资源前一次性申请所有资源(破坏持有并等待条件),或者允许抢占(破坏不可剥夺条件)。
  • 避免是使用银行家算法等策略,在资源分配前判断系统是否处于安全状态,只有安全时才分配。

我当时备考时的一个心得是:把“死锁的四个条件”和“对应的破坏方法”做成一张表,按顺序记忆,不容易混淆。

虚拟内存部分,页面置换算法里最常考的是LRU(Least Recently Used)。题目可能会问你:在一个容量为3的缓存中,依次访问页面序列1、2、3、4、1、2、5,问发生多少次缺页。这种题就是完全靠手动模拟,但你有没有想过,用“HashMap加双向链表”的方式实现一个LRU Cache,才是真正理解LRU的关键。

class LRUCache { private: int capacity; list<pair<int, int>> items; unordered_map<int, list<pair<int, int>>::iterator> cache; public: LRUCache(int capacity) : capacity(capacity) {} int get(int key) { if (cache.find(key) == cache.end()) return -1; auto it = cache[key]; int value = it->second; items.erase(it); items.push_front({key, value}); cache[key] = items.begin(); return value; } void put(int key, int value) { if (cache.find(key) != cache.end()) { items.erase(cache[key]); } else if (items.size() >= capacity) { int lastKey = items.back().first; items.pop_back(); cache.erase(lastKey); } items.push_front({key, value}); cache[key] = items.begin(); } };

这个实现的关键思想是:双向链表记录访问顺序,链表头部是最新访问的节点,尾部是最久未访问的节点;哈希表负责在O(1)时间内定位链表中的节点。实际操作中需要注意:更新节点位置时,必须先erase再push_front,顺序搞反容易出问题。

4.2 计算机网络:TCP、HTTP与网络的层次思维

网络部分的题目多以选择题形式出现,偶尔会有简答题。重点包括TCP/UDP的区别、TCP三次握手与四次挥手、HTTP请求方法、状态码含义等。

关于TCP三次握手,面试官最喜欢追问的一个问题是:“为什么连接建立需要三次握手,而断开连接需要四次挥手?”答案是:建立连接时,SYN报文和ACK报文可以合并发送,所以只需要三次;而断开连接时,通信双方是各自独立关闭的,A发送FIN报文表示自己不再发送数据,但B可能还有数据要发给A,所以B先回复ACK,等到B的数据发完了,再发送自己的FIN报文,这样就多了一次交互。

HTTP状态码也是一个容易丢分的点。2xx是成功,3xx是重定向,4xx是客户端错误,5xx是服务端错误。具体到常见状态码:

  • 301 Moved Permanently:永久重定向,浏览器会更新书签,后续请求直接访问新地址。
  • 302 Found:临时重定向,浏览器不会更新书签。
  • 304 Not Modified:协商缓存命中,服务器返回该状态码表示客户端缓存的资源仍然有效。
  • 403 Forbidden:服务器理解请求但拒绝执行。
  • 502 Bad Gateway:网关或代理服务器收到上游服务器的无效响应。

这里建议你用抓包工具(比如Wireshark或Fiddler)实际观察一次完整的HTTP请求响应过程,比单纯背状态码有效得多。你会看到304是在条件请求(携带If-Modified-Since或If-None-Match头)之后出现的,这种直观感受是看书学不来的。

4.3 数据库:索引、事务与SQL的实用细节

数据库题在京东这类电商公司的笔试中非常重要,因为数据一致性是电商业务的生命线。考察点通常是:索引失效的场景、事务的ACID特性、事务隔离级别、SQL语句的编写和优化。

索引失效是面试官最爱考的内容之一。在联合索引(a, b, c)上执行以下查询,哪些会走索引?

  1. WHERE a = 1 AND b = 2 AND c = 3
  2. WHERE b = 2 AND c = 3
  3. WHERE a = 1 AND c = 3

答案是第1条和第3条会走索引(第3条中a是最左前缀,b被跳过后,c无法使用索引,但a仍然能用到),第2条完全不会走索引,因为它不满足最左前缀原则。

这个问题的判断标准是:查询条件是否包含联合索引的最左列(a)。只要包含了a,索引就能用上;不包含a,就一定用不上。至于a之后的条件能否继续用索引,要看有没有跳过中间的列。

事务的隔离级别,四个级别由低到高分别是:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。读未提交会脏读,读已提交解决了脏读但会出现不可重复读,可重复读解决了不可重复读但可能出现幻读,串行化解决了幻读但性能损耗巨大。

MySQL默认的隔离级别是可重复读,但这里有个细节:MySQL在可重复读级别下,通过间隙锁(Gap Lock)和临键锁(Next-Key Lock)解决了幻读问题。这是MySQL的实现细节,很多教材不会讲到,但笔试或面试中主动提到这一点,会是一个明显的加分项。

5. 逻辑思维与综合设计题:那些“不走寻常路”的题目

5.1 典型逻辑推理题的解法框架

京东的笔试卷里,逻辑推理题通常不是脑筋急转弯,而是需要你通过已知条件进行严密的推理。比如经典的“称球问题”:有12个球,其中1个球的重量与其他11个不同(或轻或重),用天平称3次,找出这个球并判断它是轻还是重。

这类题目的通用解法是“信息论思维”:每次称量有三种结果(左重、右重、平衡),3次称量理论上最多可以区分3^3=27种情况。12个球乘以轻重两种可能,共24种情况,所以3次称量在信息量上是足够的。

但真正动手做的时候,关键是合理分组。第一次称量把球分成三组,每组4个,称前两组。如果平衡,目标球在第三组,用剩余两次机会从4个球中找出目标球:把4个球中的3个拿出来,分别和已知正常的球混在一起称,即可确定。如果不平衡,说明目标球在已称的8个球中,且已经知道了它偏轻还是偏重,后续的称法就需要结合“已知标准球”来设计。

这种题的解题框架可以总结为三步:

  • 目的是什么:找出目标球并确定轻重。
  • 信息量够不够:确认总可能情况数不超过天平结果组合数。
  • 怎么分组:每次称量要确保无论哪种结果,剩余可能情况都能在剩余次数内解决。

5.2 方案设计题:系统设计思维从需求分析开始

综合设计题通常是开放性的。比如“设计一个短网址系统”或“设计一个电商秒杀系统”。这类题目没有标准答案,考察的是你分析问题和架构设计的能力。

一个合格的回答应该包含以下环节:

  • 需求分析:系统是给谁用的?预期的QPS是多少?数据量多大?需要支持哪些核心功能?
  • 方案设计:数据的存储结构怎么设计?是否需要引入缓存?如何保证并发场景下的数据一致性?
  • 细节推敲:短网址的生成算法用什么?哈希取模还是自增序列?冲突怎么处理?
  • 瓶颈分析与扩展性:系统未来可能遇到什么瓶颈,如何扩展?

以短网址系统为例,核心是“长URL映射到短URL”的算法设计。常见做法是用自增ID加BASE62编码:每来一个长URL,从全局发号器获取一个自增ID,然后将ID编码为62进制字符串作为短码。这个方案的好处是生成的短码长度固定、无冲突、方便分库分表(可以按ID区间分片)。缺点是依赖发号器,高并发下需要引入分布式ID生成方案,比如雪花算法。

虽然这是2013年的题目,但它考察的系统设计思维放到今天依然适用。现在很多所谓的系统设计题,解法和当年没有本质区别,只是多了一些新的中间件组件可以选择。掌握核心思路比背方案更重要。

6. 常见错误与实战避坑:我自己踩过的五个坑

6.1 边界条件测试缺失

笔试最常见的错误之一就是漏掉边界条件。比如反转链表时没考虑空链表和只有一个节点的链表;合并数组时没考虑一个数组已经全部遍历完的情况。我的建议是:写完代码后,至少用以下测试用例在脑子里“跑”一遍——空输入、单元素输入、正常输入、极端值输入。养成这个习惯后,代码正确率会有明显提升。

6.2 时间复杂度分析不到位

很多人能写出正确的解法,但分析不清楚时间复杂度。比如在一个循环里用了erase操作,却没意识到vector的erase是O(n)的,导致整个算法变成了O(n^2)。如果你在笔试中写出一个复杂度较高的解法,最好明确指出来,并且主动说明可以如何优化。面试官更看重的是你对自己的代码有清晰的认知,而不是盲目自信。

6.3 代码书写规范与可读性

纸上写代码或在线白板编程时,细节决定印象分。变量命名要语义化(建议不要用a、b、c,改用left、right、slow、fast),代码缩进要一致,每个函数只做一件事。2013年的笔试卷是纸质阅卷,书写工整程度会直接影响阅卷人的心情;现在虽然大部分改成了在线评测,但代码风格依然会影响后续面试官对你的评价。

6.4 基础概念混淆

我在备考时踩过的一个坑是混淆了进程和线程的切换开销。当时的记忆是“线程切换比进程切换快”,但具体快在哪里说不清楚。后来查了资料才确定:进程切换需要切换地址空间(包括页表基地址),而线程切换不需要,这是两者切换开销差异的主要来源。这种概念的深度理解,会直接体现在客观题的正确率上。

6.5 时间分配不合理

这类笔试卷的题量通常不小,如果在某道题上卡太久,后面的题就没时间做了。我的经验是:先花两分钟把所有题目浏览一遍,标记出有把握的题和无把握的题。先做有把握的,确保基础分拿到手,再回来啃难题。这个策略看似简单,但在紧张状态下能非常有效地稳定心态、提高总分。

7. 结束语:从这份卷子延伸出去的能力补给清单

这份2013年的研发笔试卷让我印象最深的不是某一题的解法,而是它背后传递出的信息:无论技术栈怎么变,计算机基础始终是研发岗位的“硬通货”。当年考的是快排和链表,现在考的是动态规划和设计模式,但考察的逻辑一脉相承——你是否具备扎实的基本功,是否有清晰的解题思路,是否能写出可读性高、鲁棒性强的代码。

一个有效的后续学习路径是:

  • 算法方面,坚持每天一到两道LeetCode,重点做数组、链表、树、动态规划、字符串这五大类题型,并整理错题本。
  • 基础方面,把《深入理解计算机系统》的进程、虚拟内存、网络部分精读一遍,配合实验更有收获。
  • 数据库方面,找一个真实项目(哪怕是个人项目)练习SQL优化,尝试用EXPLAIN观察执行计划,总结索引失效的场景。
  • 系统设计方面,多阅读成熟开源项目的设计方案,比如MySQL的主从复制方案、消息队列的高可用方案,看多了自然就有感觉。

如果你今年正在准备笔试,希望这份拆解能帮你少走一些弯路。如果你只是路过,也建议花点时间把这些基础知识重新过一遍,做技术这一行,地基稳不稳,直接决定了你能在上面盖多高的楼。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询