尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

校招数据结构笔试题解析:从链表到哈希表的备考攻略

校招数据结构笔试题解析:从链表到哈希表的备考攻略 1. 唯品会这套A卷到底在考什么拿到这份《2018校招数据结构笔试题A卷》的时候我第一反应是它其实不像很多人想的那样是一份“难题怪题集”。恰恰相反它更像是给所有计算机科班和非科班同学划了一条清晰的线——你数据结构学到什么程度直接决定你在这张卷子上能拿多少分。这套A卷的定位在2018年那个时间段里非常典型移动互联网业务增速还在高位电商大促的峰值流量一年比一年猛唯品会这类平台对后端研发、算法岗候选人的考察特别看重“能不能用数据结构解决真实业务里的性能问题”。所以卷子里很少出现那种“背一背就能过”的概念填空更多的是“给你一个场景你用什么结构、为什么这么选、复杂度是多少”这类综合题。站在今天回看这套题依旧有很强的参考价值。原因很简单数据结构这块知识十年二十年都不会过时。链表、栈、队列、二叉树、哈希表、排序查找这些是任何语言、任何框架底下都绕不开的地基。哪怕你面的是Go、Java、C哪怕面试官考的是Redis底层、系统设计、算法题归根结底都在考你数据结构内功。这篇文章我想换个角度写。不逐题贴答案而是把A卷的设计逻辑拆开结合我自己刷题、带人、实际面试的经验讲清楚每一类考点“为什么考”“怎么答才加分”“代码怎么写才稳”。你把这篇文章吃透再去刷任何一家公司的数据结构笔试题思路都会清晰很多。2. 从A卷反推考纲核心数据结构全图谱2.1 线性表链表必考栈队列常考线性结构是数据结构第一道门槛。A卷里链表相关的题目几乎年年出现而且出题角度五花八门单链表反转、判断是否有环、找到环入口、合并两个有序链表、删除倒数第N个节点、链表交点……这些题表面上花样多本质上就是考两件事指针操作熟不熟、边界条件想得全不全。我见过太多人在笔试环节链表题翻车不是因为不会思路而是代码细节一堆bug空指针没判、头节点丢了、循环条件写错、甚至反转之后返回了原来的头节点。这些都是低级错误但在限时笔试里非常致命。栈和队列在A卷里通常不会单独出大分题而是作为辅助工具混在其他题目里考。典型场景包括用两个栈实现队列、括号匹配、表达式求值中缀转后缀、单调栈解决“下一个更大元素”问题等。这些题的核心逻辑其实一句话就能说透栈解决“最近相关性”队列解决“先进先出”的公平性。比如“用两个栈实现队列”这道经典题很多人第一次看到会懵。其实思路很朴素一个栈负责入队push操作直接进stackIn一个栈负责出队pop时如果stackOut为空就把stackIn里的元素全部倒进去再弹出。这个过程恰好把元素的顺序翻转了两次负负得正就恢复了队列的FIFO顺序。理解了这一点代码就能写得很干净。2.2 树与二叉树遍历是基础递归是灵魂树这块在A卷里占比相当重。二叉树的前中后序遍历、层序遍历、根据前序中序重建二叉树、求树的高度、找最近公共祖先、判断是不是二叉搜索树……这些几乎构成了校招笔试题的“基本盘”。为什么树这么重要因为树结构天然适合表达“层级关系”和“二分决策”。文件系统、数据库索引B树、编译器语法树、路由表、搜索引擎的倒排索引底层全是树的变形。面试官只要看到你树题写得好基本就能推断出你的递归功底、分治意识、抽象建模能力在什么水平。复习树的时候我强烈建议不要死记代码模板。你要真正想清楚三件事递归的终止条件是什么、单层递归要做什么、返回值向上传递什么。这三件事想通了不管是遍历、重建、还是求各种属性代码都是水到渠成。比如求二叉树最大深度最简单的递归写法是public int maxDepth(TreeNode root) { if (root null) { return 0; } int left maxDepth(root.left); int right maxDepth(root.right); return Math.max(left, right) 1; }这段代码的递归逻辑就是空节点返回0终止条件非空节点返回左右子树深度的较大值加1单层逻辑。就这么简单。但很多人写递归老是绕不明白本质上是没有把“递”和“归”拆开看。2.3 图DFS与BFS是主力拓扑排序和最短路径是进阶图在A卷里的出现率没有树那么高但只要是A卷里的图题往往是拉开分数差距的关键。图的存储方式邻接矩阵 vs 邻接表、深度优先搜索、广度优先搜索、拓扑排序、单源最短路径Dijkstra这些属于必须掌握的范畴。我特别想提醒大家图的题不一定以“图”的面貌出现。比如“课程表”问题本质是拓扑排序“岛屿数量”问题本质是DFS/BFS“单词接龙”问题本质是图的BFS最短路径。面试官非常喜欢把图包装成看似无关的场景题来考察你“能不能识别出底层结构”。掌握DFS和BFS我个人的经验是抓住“每个节点只访问一次”这个核心配合visited数组防止死循环。BFS用队列实现天然适合求最短路径DFS用栈递归实现天然适合遍历所有可能性。分清这两个使用场景图题就赢了一半。2.4 排序与查找八种排序吃透二分查找背熟排序算法在A卷里属于“必考但很少单独当大题”的板块。考法通常是手写快排、手写归并、比较几种排序的时空复杂度、稳定性的判断、给定数据特征选最优排序算法。也有不少公司喜欢考“排序算法的应用变形”比如求第K大的数其实是快排的partition思想。这里有个高频易错点稳定性判断。冒泡、插入、归并是稳定的选择、快排、堆排是不稳定的。很多人会困惑为什么快排不稳定——因为partition过程中基准元素会和后面的元素交换可能把相同元素的前后顺序打乱。这个点笔试如果考选择题十个人里至少有三个人会栽。二分查找也是A卷高频考点。不过现在很少直接考“在一个排好序的数组里找目标值”这种基础版了更多是考“旋转数组找最小值”“在排序数组中查找元素的第一个和最后一个位置”这类变体。二分查找的精髓是维护一个循环不变量——每次循环都把答案范围缩小一半边界条件必须严格一致否则很容易死循环或者漏答案。2.5 哈希表与字符串空间换时间的利器哈希表在A卷里的地位很微妙。它一般不单独出题但几乎所有需要“快速查找”的题目里它都是最优解之一。两数之和、无重复字符的最长子串、字母异位词分组、LRU缓存机制……这些高频笔试题背后站着的都是哈希表。如果你准备过Redis就知道哈希表还是Redis底层最核心的数据结构之一。Redis的hash、set、zset底层都有哈希表的影子zset更是把哈希表和跳表结合在了一起。所以面试官问你“哈希表冲突怎么处理”表面上考数据结构实际上也在考察你对真实工程里哈希实现的了解程度。字符串题目A卷爱考的是回文串、子串匹配、KMP这些。KMP的next数组是很多人的噩梦但说实话校招笔试里让你完整手写KMP的公司并不多。反倒是滑动窗口 哈希表这种组合题出镜率极高比如“无重复字符的最长子串”这题我认为是必须做到2分钟内能默写出来的程度。3. 高频题型的解题思路与代码实现细节3.1 链表反转基础中的基础细节中的细节链表反转是“面试敲门砖”级别的题目。思路本身一句话就能说清楚从头到尾遍历链表逐个把当前节点的next指针指向前一个节点。但真正写起来一个常见的错误版本长这样public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { curr.next prev; // 先把当前节点的next改了 prev curr; curr curr.next; // 问题此时curr.next已经是prev了丢掉了原来的next } return prev; }这段代码的bug在于改了curr.next之后你再也找不到原来的“下一个节点”了。正确做法是先用一个临时变量把下一个节点保存下来public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode nextTemp curr.next; curr.next prev; prev curr; curr nextTemp; } return prev; }这两段代码的差别就一行但结果天壤之别。笔试的时候紧张很容易就写成错误版本。我的建议是链表题动手之前先把三个指针的关系画出来。画出来再写代码出错率能降低一半。3.2 二叉树层序遍历队列的标准应用层序遍历BFS在笔试里出镜率极高而且经常带变体按层输出、之字形遍历、求每层最大值、填充next指针等。最基础的核心代码一定要滚瓜烂熟from collections import deque def levelOrder(root): if not root: return [] result [] queue deque([root]) while queue: level_size len(queue) current_level [] for _ in range(level_size): node queue.popleft() current_level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(current_level) return result关键点在于进入每一层循环之前先记录当前队列的长度level_size这样就能准确知道这一层有多少个节点而不会把下一层的节点混进当前层。这个“先固定层大小再处理”的思路几乎所有层序遍历变体题都能沿用。3.3 快排与Top K分治思想的两种落地快速排序的partition操作是很多“求第K大/第K小”题目的底层原理。快排的核心思路是选择一个基准元素把数组分成小于等于基准的左半部分和大于等于基准的右半部分然后递归处理左右两边。手写快排时我建议用“挖坑填数”这个版本逻辑最清晰public void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[left]; int i left; int j right; while (i j) { while (i j arr[j] pivot) { j--; } arr[i] arr[j]; while (i j arr[i] pivot) { i; } arr[j] arr[i]; } arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }这个版本的核心是基准值先“挖出来”然后从右往左找比基准小的填坑再从左往右找比基准大的填坑最后把基准放回i所在的位置。整个过程非常直观也不容易出现指针越界的问题。把快排改成“求第K大”的思路也很简单partition之后基准元素的位置i已经是最终位置。如果i正好是第K大的位置第K大对应下标K-1直接返回arr[i]如果i大于K-1说明目标在左半部分只递归左边反之递归右边。这样平均复杂度从O(n log n)降到O(n)这就是经典的“快速选择算法”。这个优化在笔试里是明显的加分点。3.4 哈希冲突的工程处理链地址法与开放寻址法哈希表相关题目要么直接考底层原理要么考“给定数据求hash值”这种计算题。A卷很可能涉及哈希冲突处理方式这里我想多说几句因为这是很多科班学生都容易混淆的地方。开放寻址法思路是发生冲突时继续探测下一个空闲位置。常见探测方式有线性探测依次往后看、二次探测按平方步长跳跃、双重散列用第二个哈希函数计算步长。这种方案适合数据量小、装载因子低的场景因为一旦表快满了探测次数会急剧上升。链地址法拉链法则是把冲突的元素放到一个链表里。Java的HashMap就是典型代表当链表长度超过阈值8且数组长度超过64时还会转成红黑树这就是为了应对极端哈希冲突下的性能退化。这里有个面试常见追问“为什么链表转红黑树的阈值是8”简单回答是在负载因子0.75、哈希函数随机的前提下链表长度达到8的概率极低约千万分之一超过8说明哈希函数可能出了问题用红黑树兜底。做题时候记住关键结论链地址法在键值分布不均匀时更稳开放寻址法对内存的利用更紧凑、缓存更友好。答到这个层次面试官基本会认可你真的理解哈希表。3.5 LRU缓存数据结构综合应用题这里必须单独讲一讲LRU缓存机制因为它实在是太经典了。几乎所有公司的笔试题库都有它A卷也不例外。LRU的核心需求get操作和put操作的时间复杂度都要求O(1)且缓存满时要淘汰最久未使用的数据。想要O(1)的查找必须有哈希表想要O(1)的插入删除并且保持顺序必须有双向链表。两者一结合就是标准答案哈希表 双向链表。Java里最偷懒的实现是直接用LinkedHashMapclass LRUCache extends LinkedHashMapInteger, Integer { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); this.capacity capacity; } Override protected boolean removeEldestEntry(Map.EntryInteger, Integer eldest) { return size() capacity; } }但笔试如果只写这个答案通常拿不到满分。面试官更想看的是你自己手动实现双向链表 哈希表因为这才叫真正理解原理。手写版本的核心是维护一个虚拟头节点和一个虚拟尾节点这样在插入和删除时不需要特判边界情况代码能清爽很多。这个“哨兵节点”技巧在链表相关题目里经常用到建议熟练掌握。4. A卷隐藏的“加分项”从会做到答得漂亮4.1 复杂度分析每道题都必须主动交代A卷的主观题部分很多时候并不会明确要求你写出时间复杂度和空间复杂度。但恰恰是这种“没明确要求”的地方才是拉开差距的机会。每写完一段核心代码我的习惯是紧接着补一句“时间复杂度O(n)空间复杂度O(n)其中n是输入规模”。为什么这是加分项因为笔试阅卷人一天要看上百份卷子最头疼的就是那种“代码写对了但说不清为什么对”的考生。你主动写复杂度说明你有工程思维知道自己写的代码在什么规模下会挂、在什么场景下能扛。这套思维在真实业务里太重要了——比如你在唯品会做秒杀系统一个接口的QPS要上万你写个O(n²)的算法可能直接把服务搞垮。复杂度的分析也有讲究。递归类题目很多人只会背“T(n)2T(n/2)O(n)所以T(n)O(n log n)”但你要是能画一下递归树解释清楚每一层的时间开销和层数之间的关系面试官会觉得你是真的懂而不是背的。4.2 边界条件空值、单元素、重复元素一个都不能漏我在帮同学做笔试题复盘时发现一个惊人的规律能写出核心解题思路的人有很多但能一次通过所有测试用例的人占比很低。问题往往出在边界条件上。以二叉树的题目为例至少要考虑这些边界根节点为null返回什么只有一个节点左右子树都为null代码还能跑吗树的深度非常大比如10000层递归会不会栈溢出是否需要改成迭代法树里有重复值判断BST的时候用严格大于还是大于等于链表类题目同理传入的链表为空、只有一个节点、只有两个节点、有环、有重复元素……每一种情况都值得在脑海里过一遍。一个实用的检查方法写完代码后自己在稿纸上模拟跑一遍“最小输入”和“极端输入”各跑一遍。最小输入比如空链表、单节点树极端输入比如全相等元素的数组、完全逆序的数组、全是重复字符的字符串。这两类输入能帮你暴露绝大多数边界bug。4.3 代码风格命名和结构也是隐形评分点笔试的代码HR和面试官第一眼看的不是算法对不对而是整体观感。一段整洁的代码和一段乱糟糟的代码即便功能相同给阅卷人的心理评分也完全不同。我个人的代码风格约定如下变量名用有意义的英文。链表头用head、当前节点用curr/current、前驱节点用prev不要用p、q、t这种看不出来含义的字母。循环和递归的缩进统一大括号风格一致。核心步骤写注释但不要写废话。比如“// 找到基准元素的最终位置”这种注释是有价值的而“// 令i”这种注释纯粹是噪音。方法拆解清晰。一个方法只做一件事如果某段逻辑可以独立出来比如链表的反转、数组的partition就抽成私有方法。这些习惯平时刷题就要养成。别等到笔试那几天再临时抱佛脚手写代码的书写习惯是练出来的不是背出来的。4.4 时间把控先易后难别在送分题上丢分笔试题从来不要求你“每道题都做出来”而是考察你在有限时间内的得分率。A卷通常题量不小如果前面选择题、判断题花了太多时间纠结后面的大题很可能写不完。我的做题策略是固定的先快速扫一遍全卷把题目分为三档——有把握的、需要想一想能做的、完全没思路的。先做第一档把该拿的分稳稳拿住再集中火力做第二档第三档如果时间实在不够就写核心思路和伪代码哪怕只是写上“这题可以用BFS解决从起点出发逐层扩展”这几个字也比空着强出百倍。优先做“代码量少但考察点清晰”的题比如链表反转、二叉树遍历、二分查找这类。这类题代码短、得分率高、不容易超时。复杂工程题比如手写红黑树的旋转如果你不是绝对熟练建议放到最后再碰。5. 备考路线与方法从真题到实战5.1 基础阶段的安排教材 刷题双线并行数据结构复习我见过的最大误区是只看书不动手。严蔚敏老师的《数据结构C语言版》是经典里面的伪代码逻辑非常严谨但如果你只翻书不做题合上书之后可能连单链表反转都写不顺。我比较推荐的时间规划是“三遍法”第一遍第1~2周过教材牢牢掌握每个数据结构的定义、操作、时间复杂度和典型应用。这一遍不追求刷多少题重点是建立知识框架。每学完一个结构就自己动手把基础操作实现一遍比如用数组实现栈、用链表实现队列、手写一棵二叉搜索树的插入删除。第二遍第3~4周按专题刷题。每两三天集中攻一个专题链表刷20题、栈和队列刷15题、树刷30题、图刷20题、排序和查找刷20题。刷题过程中记录错题每道错题都要追溯到具体的知识点而不是“这题我没见过”。第三遍考前1周做整套的模拟卷和真题卷严格限时。做完以后逐题复盘不仅仅是看答案对错而是分析自己每题花了多久时间、卡在哪个环节、下次怎么提速。5.2 语言选择不是“哪个好”而是“哪个熟”刷数据结构题用C、Java、Python、Go都可以。但不要今天刷题用Python、笔试要求写Java、面试又拿C去讲这样三个语言的库函数和语法细节全混在一起很容易乱。我给学生和同事的建议是选一门主语言笔试面试都用它。目前校招笔试的主流选择还是Java和C因为这两门语言的表达能力均衡不会因为语法太简洁而让面试官怀疑你的功底。Python也行特别是算法岗代码量少、写起来快但带来的风险是部分面试官对Python手写数据结构的代码要求会更高毕竟Python很多结构都封装好了。自测的方法很简单用一门语言手写无序——单链表反转、用两个栈实现队列、层序遍历二叉树、快排、二分查找。这五道题如果都能在10分钟内写完且没有明显bug说明这门语言的基本功过关了。5.3 题库与刷题工具推荐刷题工具方面我最有发言权的是LeetCode。国内版和美版都可以关键是利用好“题库标签”功能按数据结构分类刷题。如果目标是校招优先刷“热门100题”和“面试高频题”这两个列表性价比最高。除了LeetCode牛客网也是校招必备。上面有大量真实公司的笔试题库包括唯品会历年的校招真题它的在线笔试系统和企业面试系统比较接近刷题时可以模拟真实考试环境。还有一点经常被忽略把往年真题当教材来啃。每一道真题都值得研究三遍——第一遍自己做第二遍看题解并理解不同解法的优劣第三遍站在出题人角度想“这个考点有什么坑、哪类候选人会在这里栽跟头”。做到第三遍你捕捉考点敏感度的能力会有质的提升。5.4 笔试现场的小技巧最后说几个笔试现场容易被忽视的细节。第一看懂题目再动手。每年都有人把“求最长递增子序列”看成“求最长连续递增子序列”这两个题解法完全不同一旦看错20分钟就白费了。读题时拿笔圈出三个关键词输入范围、输出格式、边界条件。第二如果是在线笔试先写好输入输出模板。很多平台的输入输出处理比核心算法还烦人尤其这种类型——有多个测试用例、每个用例格式不同、字符串里可能有空格。先把模板写好后面做题会省很多时间。第三不会的题先把思路写在草稿纸上。哪怕是“说一下思路”也尽量用文字表达清楚。阅卷时“思路正确代码有bug”通常比“代码写出一堆但思路混乱”得分高。因为前者说明你有能力但有失误后者说明你可能只是在堆代码碰运气。第四带手表。笔试时屏幕上往往有一个计时器但有些平台的设计会让你忽略它。自己戴一块简单的手表每隔20分钟抬头看一次时间确保整卷题量的进度在计划内。6. 常见问题与避坑实录6.1 笔试题型速查表这里我直接把A卷里最可能出现的数据结构题型和对应的解决思路整理成一个速查表方便大家考前快速过一遍数据结构高频考点核心思路常考复杂度链表反转、环检测、合并有序链表、删除倒数N个节点双指针快慢指针、哨兵节点O(n)栈括号匹配、表达式求值、单调栈栈顶元素做最近匹配均摊O(n)队列层序遍历、滑动窗口最大值、双端队列双端队列维护窗口内单调性O(n)二叉树遍历、最大深度、最近公共祖先、二叉搜索树属性递归分治 迭代栈O(n)或O(log n)堆第K大元素、合并K个有序链表、Top K高频元素大小为K的小顶堆O(n log k)图岛屿数量、课程表、单源最短路径DFS/BFS visited数组O(VE)哈希表两数之和、无重复字符最长子串、LRU缓存空间换时间O(1)查找O(n)排序快排、归并、堆排、稳定性比较partition 递归/迭代O(n log n)二分查找旋转排序数组、查找边界、求平方根循环不变量 缩小区间O(log n)字符串最长回文子串、KMP、滑动窗口子串匹配动态规划、双指针、前缀函数O(n)或O(n²)这张表不是让你去背而是拿来对照自己看到某个考点能不能立刻说出思路、写出代码、分析复杂度。如果有一项卡壳马上去针对性刷题。6.2 我踩过的坑链表题丢指针、递归栈溢出、二分死循环这里说几个真实的翻车现场都是我自己的或者我陪跑过的同学踩过的坑。第一个是链表题“丢指针”。写链表反转时先改了curr.next然后让curr curr.next此时curr已经跑到旧链表的尾部去了整个链表的后半部分全丢。这个问题我在平时刷题时见过不下二十回。根治办法就是我在前面强调过的先保存next再修改next。写出代码前先在草稿纸上画三个框分别表示prev、curr、next移动过程一目了然。第二个是递归栈溢出。有一道题是求二叉树的最大深度我刚开始用递归写法自测没问题。后来面试官追问“如果树的高度是10万你的代码会怎样”我当时愣了一下然后在面试官提示下才想到递归深度过大会导致StackOverflowError。后来我养成了习惯拿到一道递归题先问树的高度有没有可能特别大。如果可能改写成迭代法用显式栈模拟递归流程比如二叉树的前序遍历用栈、层序遍历用队列。第三个是二分查找的死循环。比如寻找左边界时如果区间更新条件是left mid而不是left mid 1且right mid那么当left与right相邻时mid left更新后left不变死循环就出现了。这个问题的根源是mid的取整方向和区间收缩方式不匹配。我的习惯是每次更新后立即检查一下区间是否严格缩小如果出现left和mid相等的情况手动调整mid计算方式比如mid (left right 1) / 2取上整。这个细节值得单独刷几道二分边界题练习到形成肌肉记忆为止。6.3 如何高效复盘一份真题刷完一套A卷不要直接扔到一边。我的复盘流程是这样第一步对照答案把所有错题标记出来。注意“错题”不只是做错的题还包括“做对了但花了太长时间”的题。后者往往说明你的方法不够熟练或是有更优解没掌握。第二步每一道错题写三行复盘笔记第一行题目考察的核心数据结构是什么。第二行我的解法哪里出了问题思路错、细节错、超时。第三步最优解法的关键一步是什么比如“用快慢指针找环入口”“用单调栈将O(n²)降为O(n)”。第三步一周后重新做一遍错题。不是看着答案做而是完完整整地重新写一遍代码。如果这遍能流畅做出来说明你真的吸收了如果再错就要再深挖一次自己的知识盲区。复盘的价值在于把“做过”变成“会了”。刷十套题不复盘不如刷三套题并深度复盘。这个道理在面试准备里同样适用——数量从来不是目的质量才是。7. 写在最后的经验之谈从这套A卷再往远处看数据结构面试题这几年其实一直在“题型升级”。早年爱考“数组排序”“单链表反转”这种教科书原题后来流行“Top K”“LRU”这类工程场景题再往后开始看重“Redis底层用了什么结构”“海量数据去重怎么做”这种和真实生产环境强绑定的题。但骨子里的东西从来没变过分析问题的能力、权衡结构的能力、把思路变成干净代码的能力。我个人见过太多同学备考时执着于“刷满500题”刷完之后心里依然没底。反而是那些只刷了200题但每道题都吃透的人笔试现场表现非常稳定。所以如果你时间有限我建议你把这套A卷里出现的所有考点挨个检验到自己能默写的程度比盲目滚动刷题有用得多。最后分享一个小技巧也是我这些年面试别人时特别看重的回答任何一道数据结构题都试着从“如果数据量放大一万倍这个方案还行不行”的角度重新审视一遍。这个习惯能帮你写出真正有工程价值的代码而不是只会在面试题里打转的“做题家”代码。希望你能从这套A卷里挖出真正有用的东西笔试顺利上岸。
返回列表