
那年校招季我在电脑前点开唯品会2018校招数据结构的笔试链接A卷的时限是九十分钟。说实话做之前我以为会是网上流传的那种大小堆求中位数手写红黑树之类的硬核题真正把卷子从头到尾过了一遍之后我的感受是这套题并不偏难怪但它非常刁。刁的地方在于它不怎么考你背没背过某个冷门算法而是考你在有限时间内能不能用最扎实的数据结构基本功把一道看似普通的题做对、做快、做完整。唯品会作为电商平台业务场景里天然带着高并发读、海量商品、实时推荐、订单状态流转这些关键词所以它的数据结构笔试题并不像纯算法竞赛那样追求奇技淫巧而是更看重你对基础数据结构的理解深度、边界条件意识以及把结构与实际场景挂钩的能力。这篇文章我把A卷涉及的核心考点、我当时踩过的坑、以及对每一类题目的复盘思路整理出来给后面准备电商类校招笔试的同学做个参考。不管你是刚把《数据结构》教材翻完的在校生还是刷题量已经上去但总在笔试环节差一口气的求职者这篇文章都值得你花十分钟认真读完。下面我按照A卷实际涉及的知识模块从具体题目类型入手把考察意图、解题思路和易错点一次说透。1. 笔试的整体气质九十分钟里的陷阱与分水岭先说说这套A卷给我的整体观感。整张卷子分成了客观题和编程题两大块客观题以选择题和填空题为主覆盖了复杂度分析、线性表、树、图、查找、排序这些常规章节编程题是两道手写代码题一道偏重链表操作一道偏重二叉树遍历。为什么要强调观感因为校招笔试题的区分度并不全在最后两道编程大题上。像唯品会这种体量的公司笔试环节要筛掉的人远多于能进入面试的人所以客观题里埋了大量看起来会、细想就错的陷阱。我印象很深的一道选择题在一个长度为 n 的顺序表中在第 i 个元素之前插入一个新元素需要向后移动多少个元素很多人一眼扫过去觉得答案是 n - i实际上顺序表插入的标准表述应该是移动 n - i 1 个元素。注意题目说的是在第 i 个元素之前插入如果 i 从 1 开始计数那第 i 个元素到第 n 个元素都要往后挪一共是 n - i 1 个。这个 1 就是命题人留的分水岭基础不扎实的同学这里就被划走了。再比如另一道关于循环队列的填空题给定了队头指针 front 和队尾指针 rear问队列长度怎么算。背过公式的人都知道是 (rear - front maxSize) % maxSize但题目里把 maxSize 换成了数组容量 m然后 rear 和 front 的初始关系给的是队尾指针指向队尾元素的下一个位置。两个条件叠加很多同学就开始懵。这类题目本身不难难在你能不能在不同的表述方式里准确识别出它考察的其实是同一个知识点。这一部分我复盘下来的核心结论是校招笔试题里的数据结构客观题本质是在考概念的精确性和边界条件的敏感度。你光知道顺序表插入要移动元素是不够的你得知道确切移动多少个你光知道二叉树中序遍历是左根右也是不够的你得知道当树的存储结构换成线索二叉树时前驱和后继指针该如何判断。2. 链表类题目唯品会笔试的高频主角与递推陷阱链表几乎是所有校招数据结构笔试题里跑不掉的主角唯品会A卷也一样。选择题里考了单链表的插入、删除操作的时间复杂度编程题里有一道是反转从位置 m 到 n 的链表。这一节我重点聊聊链表这一块因为它在选择题和编程题里的出现方式完全不同需要分开拆解。2.1 选择题里的链表操作步骤与指针指向A卷选择题里有一道非常经典的题在单链表中已知指针 p 指向某个结点要在 p 之后插入一个新结点 s正确的操作顺序是什么这个题的标准答案相信大家都背过s-next p-next; p-next s;先让 s 的指针域指向 p 的后继结点再让 p 的指针域指向 s。但如果你只是背了答案碰到变体题就容易翻车。唯品会这道题的变体在于它把在 p 之后插入换成了在 p 之前插入而且没有给头指针只给了 p 指针。单链表是没有办法直接找到前驱的这题的答案就不是简单的调整两个指针了而是要借助偷天换日的思路把 s 插入到 p 的后面然后交换 s 和 p 的数据域。这种考法比单纯背代码更能看出一个人是不是真的理解了链表的物理结构。再来看一道考察链表删除的选择题删除单链表中 p 指针所指向结点的后继结点。这个比前面那道更基础但依然有同学会选错。标准操作是先定义一个临时指针 q 指向 p-next然后让 p-next q-next最后释放 q。容易出问题的地方在于如果题目把释放 q 的内存写成了释放 p 的内存你还能不能一眼看出来这类题目在A卷里反复出现其实是在提醒你纸上写代码不是写给自己看的是写给机器跑的每一个指针的指向都必须落到实处。2.2 编程题里的链表反转区间链表的完整解法A卷的编程题里链表的题目是反转从位置 m 到 n 的链表要求只遍历一次。这道题如果你只会反转整个链表是很容易在区间边界上翻车的。我在考场上用的解法分三步第一步找到第 m-1 个结点记为 pre。这一步骤要注意如果 m1那么 pre 就是 null这种情况下头结点会变需要特殊处理。第二步在 m 到 n 的区间内执行标准的头插法反转。这一步的关键是记住每次把当前结点 cur 的下一个结点 temp 摘下来插到 pre 的后面然后 cur 继续向后走。第三步处理边界。如果 pre 是 null新的头结点就是反转后的区间第一个结点如果 pre 不是 null就把 pre 的 next 指向反转后的第一个结点。这是我当时写的核心代码我把它整理成了可运行的版本struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; ListNode* reverseBetween(ListNode* head, int m, int n) { if (head NULL || m n) return head; ListNode dummy(0); dummy.next head; ListNode* pre dummy; for (int i 1; i m; i) { pre pre-next; } ListNode* cur pre-next; for (int i m; i n; i) { ListNode* temp cur-next; cur-next temp-next; temp-next pre-next; pre-next temp; } return dummy.next; }这段代码里有两个点值得多说一句。一是 dummy 结点的使用它让我不用单独判断 m1 的情况极大简化了边界逻辑二是头插法的操作顺序一定要先把 cur-next 指向 temp-next再把 temp 的 next 指到 pre-next最后更新 pre-next。如果这个顺序错了链表就会断开甚至成环。这道题在LC力扣上有原题原题编号是92。但如果笔试里碰到一定要警惕它的变体比如要求你输出反转后的链表而不是返回头结点或者要求你不使用 dummy 结点完成同样的操作。变体考的往往不是你会不会反转而是你对指针操作的理解够不够深。2.3 链表题的易错点与考后复盘链表这部分的错误我复盘了一下主要集中在三个地方空指针访问。很多同学在反转链表时没有判断 cur 和 temp 是否为 NULL一旦链表长度为边界值程序直接崩溃。笔试环境里虽然没有编译报错但面试官看代码时一眼就能发现。丢结点。头插法的过程中如果没有先用临时变量保存 cur-next那么在修改 cur-next 之后原来的后续结点就找不回来了。返回值错误。题目要求返回反转后的链表头结点结果你返回了 dummy.next 但忘了重新赋值 head或者返回了反转区间的中间结点。如果你正在准备校招我建议你在本子上画一遍链表反转的指针变化图每一步都画出 pre、cur、temp 三个指针的指向。画完你就会发现这类题真正难的不是反转本身而是在反转的过程中如何不丢结点。这个道理笔试里用得到面试手撕代码更用得到。3. 二叉树与遍历A卷里的递归与迭代之争二叉树这一块A卷考察的密度相当高。选择题里出现了根据前序和中序序列重建二叉树的问题填空题里考察了完全二叉树的结点编号关系编程题里则有一道二叉树层序遍历的变体。3.1 根据遍历序列重建二叉树必考题型与考场思路已知某二叉树的前序遍历序列为 ABCDEF中序遍历序列为 CBAEDF求后序遍历序列。这道题是A卷选择题里我印象最深的一道因为它是典型的看起来简单做起来需要耐心的题目。解题思路其实很固定前序遍历的第一个结点是根结点在中序遍历里找到这个根结点根结点左边是左子树的中序序列右边是右子树的中序序列然后根据左右子树序列的长度把前序序列也分成两段递归处理。但考场上的时间压力很容易让人手忙脚乱。我的建议是在草稿纸上用分块法来做不要在脑子里空转。先把前序序列的第一个字符 A 圈出来然后把中序序列里 A 左边的 CB 和右边的 EDF 分别框起来再回到前序序列里数出对应的块 BC 和 DEF。这样一层一层往下拆后序遍历序列自然就出来了。这道题考察的实际上是对递归思想的掌握程度。如果你只会用递归做那笔试是够了但如果你想在面试里加分最好能额外掌握迭代做法的思路也就是用栈模拟递归过程。不过唯品会A卷的笔试阶段递归解法已经完全够用。3.2 层序遍历的变体从逐层输出到之字形输出A卷编程题里二叉树那道题表面上是层序遍历但加了一个限制按之字形顺序打印二叉树即第一行从左到右第二行从右到左第三行再从左到右以此类推。这题在剑指Offer里有原题考察的核心是层序遍历但多了一个层级方向判断。我的解法是用双栈法也就是维护两个栈奇数层结点先左后右入栈偶数层结点先右后左入栈。具体来说vectorvectorint zigzagLevelOrder(TreeNode* root) { vectorvectorint result; if (root NULL) return result; stackTreeNode* s1; stackTreeNode* s2; s1.push(root); bool leftToRight true; while (!s1.empty() || !s2.empty()) { vectorint level; if (leftToRight) { while (!s1.empty()) { TreeNode* node s1.top(); s1.pop(); level.push_back(node-val); if (node-left) s2.push(node-left); if (node-right) s2.push(node-right); } } else { while (!s2.empty()) { TreeNode* node s2.top(); s2.pop(); level.push_back(node-val); if (node-right) s1.push(node-right); if (node-left) s1.push(node-left); } } result.push_back(level); leftToRight !leftToRight; } return result; }这段代码的关键在于偶数层时要先把右子结点入栈再把左子结点入栈。这个顺序一旦反了之字形的效果就出不来。我在考场上就因为这个细节卡了几分钟还好最后检查出来了。顺带说一句如果笔试环境不允许你声明两个栈用队列加反转数组的方法也能实现。每层正常从左到右入队列收集完一层的结果后如果是偶数层就调用 reverse 函数反转后再加入结果集。这种写法在时间复杂度上并没有额外损失因为每层反转的总代价仍然是 O(n)。3.3 完全二叉树与堆隐藏在填空题里的联系A卷填空题里有一道一个完全二叉树有 1001 个结点问叶子结点有多少个。这道题如果不记得公式可以用最后一个非叶子结点的下标是 n/2这个性质来推。完全二叉树中如果从上到下、从左到右给结点编号那么最后一个非叶子结点的编号是 n/2向下取整所以叶子结点的个数就是 n - n/2。当 n1001 时最后一个非叶子结点的编号是 500叶子结点数量是 1001 - 500 501。这类题整体上不难但它把二叉树和堆排序联系在了一起。如果你能想到堆本质上就是一棵完全二叉树那么很多看似独立的题目其实背后是同一条知识链。唯品会A卷在这里设置题目也是在暗示你数据结构各个章节不是孤立存在的树和堆、栈和递归、队列和图遍历它们之间都有千丝万缕的联系。4. 排序与查找大数据量场景下的取舍智慧唯品会A卷在排序与查找这一块出的题是最能体现电商业务特点的。因为它考的并不是快排怎么写归并怎么分治这种默认你会的基础而是把排序和查找放在具体的场景里问你在某种限制条件下哪种方案更合适。4.1 稳定排序与不稳定排序选择题里的高频陷阱有一道选择题让我印象很深下列排序算法中哪些是稳定的选项给了冒泡排序、快速排序、堆排序、归并排序、直接插入排序、希尔排序。这个题如果是死记硬背很容易搞混。我当时的判断方法是基于相邻交换的排序通常是稳定的冒泡、直接插入、归并都是稳定排序而跳跃式交换的排序往往不稳定快排、堆排、希尔都是不稳定排序。快排虽然也是交换排序但它交换的两个元素可能相距很远所以不稳定。这个判断方法不敢说适用于所有排序算法但对校招笔试常考的这几种基本不会出错。但唯品会的命题人不会让你这么舒服地拿到分。它给出的选项里还有一句描述对 nearly sorted 的数组以下哪个排序算法效率最高这道题如果你只记得快速排序平均时间复杂度最低就容易掉坑。实际上对于近乎有序的数组插入排序的时间复杂度可以逼近 O(n)而快速排序在这种情况下如果不做优化反而可能会退化到 O(n²)。电商场景里很多数据都是每天新增一小部分、大部分保持原序这种场景下插入排序的实战价值往往被低估。4.2 海量数据找 Top K堆排序与快排思想的结合查找部分最有意思的一道选择题是这样的在 10 亿个整数中找出最大的 100 个数内存只有 100MB问最高效的数据结构是什么。选项里有红黑树、最小堆、哈希表、双向链表。这道题显然不能直接把 10 亿个数全部装入内存所以常规的排序算法全部失效。正确思路是维护一个大小为 100 的最小堆遍历 10 亿个数每次和堆顶元素比较如果比堆顶大就把堆顶替换掉然后向下调整。这样遍历完一遍堆里剩下的 100 个数就是最大的 100 个数。时间复杂度是 O(n log k)这里的 k100几乎可以看成 O(n)。这个场景和唯品会海量商品中找出销量TOP100的业务需求高度吻合。笔试里考它其实是在看你会不会把数据结构的知识迁移到真实业务里。如果你只会在教科书上写堆排序的建堆过程但不知道堆可以在海量数据里充当Top K 筛选器那这道题你就拿不到分。4.3 二分查找的边界条件一个让无数人栽跟头的细节A卷里还有一道二分查找的题但不是裸的二分而是给了这样一个场景在一个按升序排列的数组中查找第一个大于等于目标值的位置。这道题如果直接把标准二分查找的代码写上去很可能得到的是任意一个大于等于目标值的位置而不是第一个。正确的写法是在nums[mid] target时收缩右边界同时用 res 记录当前 mid在nums[mid] target时收缩左边界。循环结束后res 就是第一个满足条件的位置。这种写法本质上是二分查找左边界的问题它和标准二分查找的区别在于找到目标值后不是立即返回而是继续向左搜索直到找到最左边的那个。我在复盘时意识到这类题其实考察的是二分查找的变体意识。很多人会写标准二分但一碰到第一个最后一个插入位置这些词就开始凭感觉改代码改着改着就出了边界问题。应对方法是把二分查找的几种常见变体整理成模板比如第一个等于 target 的位置最后一个等于 target 的位置第一个大于等于 target 的位置最后一个小于等于 target 的位置笔试前每个模板手写两遍考场上才能稳。5. 哈希与字符串电商场景下的隐藏考点哈希和字符串这两章在唯品会A卷里不是主角但出现的题目都很巧妙。它们以选择题和填空题的形式出现却考出了你是否理解底层实现的深度。5.1 哈希冲突的解决方案从链地址法到开放定址法A卷有一道题问哈希表中解决冲突的方法有哪些选项里列出了链地址法、开放定址法、再哈希法、建立公共溢出区。这道题没什么难度但它后面跟了一道小题在链地址法中如果哈希函数设计得不好导致大量元素映射到同一个桶此时哈希表的查找时间复杂度会退化成多少答案是 O(n)也就是退化成了链表的顺序查找。这个知识点本身不难但放到电商系统里用户ID做哈希取模分库分表如果某些热点用户ID扎堆会导致某个库负载特别高这个场景里意义就不一样了。唯品会作为电商平台后端服务里有大量哈希表的使用场景从缓存到路由表从消息队列到分库分表哈希函数设计得好不好直接影响系统的稳定性。笔试里考这个说到底是在筛选有工程敏感度的人。5.2 字符串匹配KMP算法的最长公共前后缀字符串部分A卷考了一道 KMP 算法里 next 数组的填空题给定模式串 ababaca求它的 next 数组。这个题说难不难说简单也不简单因为 KMP 的 next 数组有两种不同的定义方式一种是最长公共前后缀长度另一种是最长公共前后缀长度减一。如果你用的是严蔚敏教材next 数组的定义是后者如果你用的是王道数据结构定义又是前者。两份教材的 next 数组数值对不上但 KMP 的整体思路是一样的。所以我的建议是笔试前先确认你用的教材是哪种定义然后把 next 数组的手算方法练熟。以 ababaca 为例如果用最长公共前后缀长度的定义next[0] -1或者 0取决于教材next[1] 0next[2] 1next[3] 2next[4] 3next[5] 0next[6] 1。如果你算出的是另一组数也不用慌只要你的 next 数组和模式串匹配时的移动逻辑自洽笔试改卷通常按关键步骤给分。值得多说一句的是KMP 算法在唯品会这种体量的公司笔试里出现的频率并不低因为电商场景里的敏感词过滤、商品标题匹配、搜索关键词纠错背后都能看到字符串匹配算法的影子。虽然业务团队不一定真的手写 KMP但笔试方希望通过这类题筛选出学过数据结构并能理解其设计思想的人。6. 图的遍历A卷里最容易被忽略的送分题说实话图这一章在唯品会A卷里占比不大但有一道关于图的遍历的选择题我觉得值得拿出来说说因为它也是一道典型的基础题里藏陷阱。6.1 深度优先遍历与广度优先遍历顺序问题不能想当然题目是这样的给定一个无向图的邻接表存储结构从顶点 V0 出发进行深度优先遍历以下哪个序列是不可能得到的这道题的关键在于邻接表里每个顶点的邻接点顺序是由建图时边的插入顺序决定的。如果不考虑邻接表的具体结构直接按照字典序最小去遍历就很容易选错。我记得当年这道题给出的四个选项里有两个序列看起来都像是从 V0 出发的 DFS 序列但其中一个违反了邻接表里的实际存储顺序需要你画出邻接表才能判断。这类题在教材里很常见但很多同学平时练习时用的都是邻接矩阵或者干脆在纸上画图、目测遍历顺序忽略了邻接表里边的存储顺序会影响遍历结果这一关键点。我的经验是遇到邻接表的遍历题务必先在草稿纸上把邻接表画出来再模拟遍历过程不要凭直觉选答案。6.2 拓扑排序入度为零的结点顺序A卷还有一道拓扑排序的填空题给了一个有向无环图要求写出一个拓扑排序序列。这道题本身不难但我注意到它考察的方式不是给你图让你排序而是给出一个序列让你判断它是不是合法的拓扑排序序列。这就比单纯的生成拓扑排序要难一点因为你需要对拓扑排序的定义有清晰理解对于图中的每一条有向边 u→vu 在序列中的位置必须在 v 之前。这种判断合法性的考法比生成更考验概念的精确性。很多同学会拓扑排序的代码但你真的给他一个序列让他逐条边去验证合法性他反而会犹豫。我当时的做法是把每一条边都列出来逐一检查宁可写慢一点也不要漏掉任何一条边。7. 考场实战复盘从审题到代码的完整时间分配最后这部分我想完整复盘一下我在做唯品会A卷时的总体时间分配和答题策略这对你后续参加任何校招笔试都有参考价值。把时间分配讲清楚比单独讲某一道题更有用因为它能帮你建立一套属于自己的考场节奏。7.1 时间分配选择题快、填空题稳、编程题留足时间我的策略是选择题平均每题不超过 1.5 分钟填空题每题不超过 2 分钟剩下的时间全部留给两道编程题。为什么这么分配因为选择题和填空题考察的是是否知道编程题考察的是能否做到。知道的题目你花再多时间也不太可能突然灵光一现想到正确答案所以快速过、标记不确定的题后面再复查而编程题需要完整的思路构建、代码书写和边界检查没有充足时间很容易写出半吊子代码。从我身边同学的反馈来看翻车的往往不是不会做编程题而是前面客观题耗时太久到了编程题只剩二十分钟只能草草写个思路骗点步骤分。7.2 草稿纸的使用方法分块演算与指针追踪笔试题里的编程题除非是纯手写代码的纸质考试一般会提供在线编辑器但草稿纸依然很重要。我自己的习惯是遇到链表和树的题目先在草稿纸上画出结构图把每一步指针变化都标出来再往编辑器里写代码。这样做的好处非常明显很多边界情况比如头结点变化、空树、只有一个结点你在图上很容易发现而在脑子里想就容易忽略。以反转区间链表为例我在草稿纸上画了三步先把 m-1 的 pre 标出来然后从 m 开始逐个把结点摘下来头插到 pre 后面最后画出反转完成后的链表形状。画完之后代码里的指针操作顺序就有了明确的物理意义写起来几乎不会错。7.3 复查环节回看不确定的题目我大概留了八到十分钟做复查重点看两类题一类是之前标记过不确定的选择题另一类是编程题的边界条件。复查选择题时我会把题目里的之前之后第几个是否稳定这类关键词重新圈一遍确认自己没有因为看错题目而选错答案。复查编程题时我会在草稿纸上模拟几组测试用例比如空链表、只有一个结点的链表、奇数个结点的树、偶数个结点的树。有些边界用例考场上写代码时很容易忽略复查环节多花几分钟往往能挽回好几分。我现在还记得当年那套A卷的编程题里有一道关于链表的题目我在复查时发现自己只考虑了 m1 的情况没有考虑 n 等于链表长度的情况。虽然最后两段代码在核心逻辑上是对的但如果少了这个边界处理在严格的测试用例下是会出问题的。离交卷还有三分钟时我补上了这个边界条件那几分大概是救回来了。8. 备考心法我不是在教你背题而是在教你怎么忘掉背题写到这里我想换个角度聊聊比具体题目更重要的东西。你可能已经发现了这套唯品会A卷考来考去核心其实就两件事一是你对数据结构基础概念的掌握是否精确二是你能不能把数据结构的技术选型与真实业务场景挂钩。前者靠背能解决一部分但解决不了第 i 个元素之前插入要移动几个结点这种扣细节的题后者靠刷题能解决一部分但解决不了为什么 Top K 问题要用最小堆而不是最大堆这种考察理解深度的题。我个人的体会是应对这类电商公司的笔试比刷题更重要的是把每一个数据结构的为什么想清楚。为什么链表插入是 O(1) 而查找是 O(n)为什么快速排序平均复杂度低但不稳定为什么哈希表能 O(1) 查找却需要处理冲突这些问题的答案才是数据结构笔试真正的考点。那段时间我把严蔚敏的《数据结构》和王道的《数据结构考研复习指导》对照着看前者帮我打基础后者帮我抓考点。我还做了一件事把每种数据结构的典型应用场景整理成一张表。链表对应 LRU 缓存栈对应表达式求值队列对应消息队列树对应文件系统堆对应优先队列哈希表对应缓存系统图对应社交网络关系。整理完这张表之后再看到任何XX场景适合用哪种数据结构的题目我基本不需要思考就能选出来。最后再分享一个小技巧笔试前一周不要追求刷难题而是把教科书上所有的基础代码重新手写一遍。单链表反转、双链表插入删除、二叉树前中后序遍历、层序遍历、快排、归并、堆排、二分查找、KMP、Dijkstra……每个都手写一遍写的时候留意边界条件。这套动作做完你的手感基本就回来了。唯品会A卷的题目本身到现在已经过去好几年了但数据结构笔试的底层逻辑并没有太大变化。它依然在用这种方式提醒每一个求职者基础不牢地动山摇。希望这篇复盘对你接下来的笔试准备有帮助。