
1. 笔试前的信息战一场从邮件就开始的淘汰赛先说个很多人忽略的事实PayPal 这类外企的实习生笔试从你收到笔试邮件的那一刻起考核就已经开始了。2018 年那场在线笔试我印象最深的反而不是题目本身而是邮件里那几行不起眼的说明——浏览器版本要求、网络环境建议、是否允许本地 IDE、代码提交后能否反复运行用例。这些细节决定了一批人还没打开题目就输了。那年 PayPal 的笔试走的是 HackerRank 平台。国内很多同学对这个平台不熟悉第一次打开会被它的界面搞懵左边是英文题面右边是代码编辑区下面还有一排测试用例的 tab。和牛客网、赛码网那种“中文题面 判题结果直接红绿显示”的交互完全不同HackerRank 的判题反馈比较“含蓄”有些题目甚至不会告诉你具体哪组用例没过只给一个抽象的分数。这种信息不对称对第一次接触的人极其不友好。我当时做了一件后来被证明非常关键的事在笔试正式开始前一周专门花了一个晚上去 HackerRank 官网刷了十几道算法题把它的编辑器快捷键、自动补全行为、输入输出格式都摸了一遍。尤其是输入输出——HackerRank 默认很多题目是从标准输入读、往标准输出写但部分题目会提供已经封装好的函数签名你只需要补全函数体。这两种模式的切换如果没提前适应临场很容易在格式上翻车。笔试时偶尔会有人因为输出多余的空格或额外的调试信息被判 Wrong Answer这种死法最冤。还有一点值得提醒笔试邮件的发件人通常带 paypal.com 或 corp.paypal.com 后缀但平台链接会指向第三方域名。当年有同学把链接当垃圾邮件直接忽略了等想起来时已经过了截止时间。建议所有投了外企实习的同学在投递后的一到两个月内养成每天检查邮箱包括垃圾箱的习惯并且把 HackerRank、HackerEarth、Codility 这类平台的域名加入白名单。2. 考题拆解PayPal 笔试到底在考什么2.1 整体结构算法为主附加题库为辅2018 年 PayPal 实习生笔试的题量不算大我记得是 3 到 4 道算法题时间大约 90 分钟。这个配置放在今天看也算标准——不考八股文不考框架细节全部是数据结构与算法。相比国内大厂动辄 10 道选择 3 道编程的配置PayPal 的笔试风格更接近硅谷系公司题目少但每道题都留了足够的时间去思考、优化、验证。从题目难度梯度来看大致是一道 easy 到 medium 的签到题一道 medium 偏上的数据结构题一道需要一点数学观察或贪心思维的 medium/hard 题。这个结构很有讲究——它既能筛掉完全没有准备的人又能在剩下的人里区分“会写代码”和“会解决问题”的差距。PayPal 要的不是刷题机器而是能在支付场景下对时延、一致性、异常恢复敏感的工程师。2.2 三道代表性的题目方向我根据自己的回忆和当年一起笔试的同学交流整理出几个出现频率较高的题目方向虽然具体题面记不全了但核心思路是清晰的第一类字符串处理与状态机。比如解析一个简单的类 CSV 输入或者根据规则压缩/展开字符串。这类题看着简单实际上考的是“是否能处理边界条件”——空字符串、转义字符、连续分隔符、超长输入。2018 年那套题里有一道和字符串展开相关的题本质上就是维护一个栈记录数字、字符串和重复次数。这题的陷阱在于嵌套层数可能很深递归解法容易爆栈必须改成显式栈。第二类图论与拓扑排序。支付业务里大量涉及交易状态流转、风控规则依赖所以图相关的题目出现在笔试里毫不意外。我记得有一道题是给定一组任务和依赖关系要求输出一种可行的执行顺序。这就是标准拓扑排序但题目给了个变体如果存在多个入度为 0 的节点要按照某种优先级输出。这就把 Kahn 算法变成了“优先队列 入度表”的组合。第三类带限制的最优化问题。比如在某个资源池里分配额度要求满足一系列约束使得某个目标函数最大化或最小化。这类题通常是贪心 排序或者是二分答案 检查可行性。有一道题特别像“区间调度”的变种给定若干任务的开始时间和截止时间、每个任务的收益求在单线程机器上能获得的最大总收益。标准解法是按截止时间排序后用最小堆维护已选任务实时判断收益是否值得替换。2.3 为什么没有考系统设计或业务题有同学可能会问PayPal 不是做支付的嘛为什么不考支付流程、风控策略、数据库设计原因很简单实习生笔试是海选环节面对的是来自不同学校、不同专业背景的候选人如果要考业务那对非计算机专业的同学极不公平。算法题是全世界 CS 领域公认的“通用语言”它不依赖特定业务背景只需要数据结构、数学、逻辑思维这些基本功。但这不意味着业务不重要。笔试只是第一步后续的面试环节会深入问你“对支付清结算的理解”“如何设计一个高可用钱包服务”这类问题。所以在笔试阶段把算法题打好才是进入下一轮的入场券。3. 核心解题思路还原从拿到题到 AC 的完整链路3.1 字符串展开显式栈怎么写才不出错先还原那道字符串展开题。题面大致是输入一个形如3[a2[c]]的字符串输出展开后的完整字符串accaccacc。数字表示重复次数方括号表示需要展开的子串可能有嵌套。很多人第一反应是递归。递归写法确实简洁但这题如果括号嵌套层数深比如几千层Python 之类的语言直接 RecursionErrorJava 也能被栈溢出干趴。我那年直接选了显式栈用两个栈分别存数字和字符串片段def decode_string(s: str) - str: num_stack [] str_stack [] current_num 0 current_str for ch in s: if ch.isdigit(): current_num current_num * 10 int(ch) elif ch [: num_stack.append(current_num) str_stack.append(current_str) current_num 0 current_str elif ch ]: prev_num num_stack.pop() prev_str str_stack.pop() current_str prev_str current_str * prev_num else: current_str ch return current_str这个解法的关键在于遇到[时把当前累积的数字和字符串压栈然后重置遇到]时从栈里取出上一层的字符串和当前层的重复次数拼接后作为新的当前字符串。这样无论嵌套多深都不会有递归栈溢出的风险。复杂度是 O(n)n 是展开后的字符串长度。我在实际写的过程中踩过一个坑数字可能是多位数比如12[a]。如果只取一位数字就会把1和2当成两次独立的重复结果完全错误。所以每次遇到数字字符时必须用current_num current_num * 10 int(ch)累积而不是直接赋值。3.2 拓扑排序变体优先队列 入度表的配合第二道典型的题是给定 N 个任务编号从 1 到 N以及 M 条依赖关系 (a, b)表示 a 必须在 b 之前完成。要求在满足依赖的前提下输出字典序最小的任务执行序列。如果有环输出特定标记。这题的标准解法是 Kahn 算法但“字典序最小”这个约束要求我们每次从所有入度为 0 的节点中取编号最小的那个所以需要用优先队列而非普通队列import heapq def find_order(n: int, edges: list[tuple[int, int]]) - list[int]: indegree [0] * (n 1) graph [[] for _ in range(n 1)] for a, b in edges: graph[a].append(b) indegree[b] 1 heap [] for i in range(1, n 1): if indegree[i] 0: heapq.heappush(heap, i) result [] while heap: node heapq.heappop(heap) result.append(node) for neighbor in graph[node]: indegree[neighbor] - 1 if indegree[neighbor] 0: heapq.heappush(heap, neighbor) if len(result) ! n: return [] # 有环 return result这题真正的难点不在算法本身而在于你必须意识到“普通队列换优先队列”这一步。如果题意没明说“字典序最小”而是用“在满足所有依赖的前提下希望任务的执行顺序尽可能接近自然顺序”这种描述很多人就会漏掉这个优化直接输出 BFS 顺序。PayPal 特别喜欢在这种地方埋坑——题目不难但考察你能不能读懂约束背后的意图。3.3 区间调度变种最小堆的替换策略第三类题的典型模型是有若干个任务每个任务有截止时间和收益每个单位时间只能做一个任务求最大收益。经典解法是——按截止时间排序用一个小根堆维护当前选择的任务集合import heapq def max_profit(tasks: list[tuple[int, int]]) - int: # tasks: (deadline, profit) tasks.sort(keylambda x: x[0]) heap [] total 0 for deadline, profit in tasks: heapq.heappush(heap, profit) total profit if len(heap) deadline: total - heapq.heappop(heap) return total这个做法的精妙之处在于当当前已选任务数量超过了当前任务的截止时间说明无法在截止时间内全部完成那就必须砍掉一个收益最低的任务。每次砍的时候弹出堆顶最小收益从而保证总收益最大。整个过程不需要回溯也不需要动态规划代码只有十几行。在笔试那种时间压力下很多人一看到“任务 收益 截止时间”就条件反射地想到 DP但 N 可能高达 10 的 5 次方二维 DP 直接超时超内存。这时候如果能冷静下来想到贪心 堆就是一道送分题。我那年就是在这类题上省下了大量时间才能回头检查前面题目的边界用例。4. 考场策略90 分钟怎么分配才合理4.1 拿到题目后的前 10 分钟别敲代码不管你有多少年刷题经验拿到题面后的前 10 分钟一定不要直接开写。先花 3 分钟把三到四道题全部读一遍标注每道题的数据范围、时间复杂度假设、输入输出格式。这一步能帮你建立全局观哪道题是必拿的,哪道题可能需要优化哪道题干脆可以先放一放。我记得当年读完所有题后我给自己定的策略是先做字符串展开思路明确代码量小测试用例好构造再做拓扑排序变体需要一点细节但架构清晰最后啃最优化那题需要数学观察。实际的做题顺序也确实是这个顺序而且前两题写完通过样例后我已经有了不错的心理优势。4.2 每个题的时间盒Time Box我给每道题设了一个大概的时间上限到点还没完全 AC 就先切换到下一题签到题/字符串题20 分钟数据结构题30 分钟最优化题35 分钟剩余时间5 分钟这个时间盒策略的本质是“止损”。笔试的判分通常是按用例通过比例给分的即使不能 AC只要你能跑通一部分用例也能拿到部分分数。卡在一道题上死磕导致后面所有题都来不及写——这是笔试的大忌。4.3 测试用例自己造别只靠样例HackerRank 的题目通常会给你一组示例输入输出但示例往往非常温和根本不包含边界情况。我自己的习惯是在提交之前至少手造三组特殊用例空输入或最小规模输入比如字符串长度为 0任务数为 1最大规模输入数据范围上限用于预估时间复杂度和内存占用有陷阱的输入比如多位数重复、依赖关系有环、存在平局场景造完特殊用例之后还有一步很多人会漏掉用朴素解法验证。如果时间允许我会写一个明显正确但复杂度较高的暴力版本随机生成小规模数据把暴力结果和优化版本的结果做对照。这个“对拍”的做法能极大概率发现思维盲区。虽然笔试现场做对拍有点奢侈但对于那些你不确定正确性的题这可能是唯一能保命的方法。5. 笔试之外2018 年 PayPal 笔试的隐藏考察点5.1 英文题干读得懂吗PayPal 的笔试题面是全英文的。虽然题目本身用词不算刁钻但如果没有提前适应会把“consecutive”连续的、“lexicographically”字典序、“mutually exclusive”互斥这类词理解错整道题的思路就会跑偏。我的建议是在笔试前两周每天看 5 道 LeetCode 的英文原题不用全做但要保证自己能在 5 分钟内读明白题面准确提炼出输入、输出、约束条件这三个要素。如果连 LeetCode 英文题都读得头疼HackerRank 的题目只会更慌。5.2 时差和网络问题2018 年那会儿 PayPal 的笔试链接是按候选人当地时区开放的理论上不存在“半夜爬起来做题”的情况。但网络问题真实存在——HackerRank 的服务器在海外如果校园网访问不稳定代码提交后可能长时间转圈。务必在笔试前检查一下自己的网络。可以提前打开 HackerRank 的某个练习题页面跑一次完整的提交-判题流程确认没有 30 秒以上的延迟。如果公司给的是专门的笔试入口非公开题库不要提前用那个入口去测试以免被系统误判为异常操作。5.3 代码风格会被人工查看吗有些公司在笔试结束后面试官会翻看候选人的代码考察代码风格和注释习惯。PayPal 的流程我不确定是否一定会人工看但“把代码写得整洁一点”这个习惯永远不亏。变量命名、缩进、是否抽了 helper 函数、有没有留无意义的调试代码——这些细节在面试复盘时都有可能成为聊资。那年我在做拓扑排序题时给变量起名用了inDegree、readyQueue这种语义化名字后来一面时面试官确实提了一句“我看到你笔试题里用了优先队列能讲讲你的思考过程吗”。这说明什么说明你的笔试代码是有人看过的。6. 复盘与资源清单现在看当年哪些准备是值得的6.1 值得庆幸的准备回顾整个流程最值得的准备有两件事一是提前熟悉 HackerRank 平台这和刷了多少题没关系纯粹是“减少未知变量”二是坚持用英文刷题把“读题”这个动作变成肌肉记忆。这两件事不能让你把不会的题做对但能确保你会的题不丢分。还有一件事是我当时做了但后来才知道有多重要的准备了一个“笔试模板”文件里面存好了常见的输入解析代码、并查集模板、拓扑排序模板、二分查找模板。这套模板放在本地 IDE 里通过复制粘贴快速起步。我见过太多人卡在“如何快速读取二维数组”这种问题上白白浪费了 10 分钟。6.2 复盘时发现的失误一次很丢分的失误是在那道“区间调度变种”题上我一开始没意识到收益可能包含负数于是我写的堆替换逻辑在处理全是负收益的任务时会把所有任务都选进去导致结果变成负数。后来提交前手动跑了一个负收益用例才发现问题补了一个if profit 0 and len(heap) deadline: continue的判断才通过。这个失误说明审题时不仅要看数据范围还要看“数值域”。题目说收益是整数可没说一定是正数。负数的存在会让很多“默认正确”的逻辑失效。6.3 对后来者的资源建议如果你现在正在准备类似的笔试我给一份按优先级从高到低排列的准备清单LeetCode 前 200 题的中等难度题重点覆盖数组、字符串、哈希表、栈、队列、二叉树、优先队列、图论基础。每个模板题不只要做对还要做到能在 5 分钟内无脑敲出来。特别是拓扑排序、并查集、Dijkstra、前缀和、单调栈。刷题时用英文题面。如果觉得吃力先从 LeetCode 的“数据库”或“Shell”类题目的英文题面开始过渡逐步增加算法题的比重。练至少 3 次完整的限时模拟。模拟时要模拟真实环境避免使用自动补全以外的任何辅助。7. 写在经验之后的一条提醒很多人在面经里只写“考了哪些题、怎么解的”但我想额外提一个容易被忽视的点PayPal 的笔试系统有时候不按你代码中if __name__ __main__的写法执行而是直接调用你定义的函数入口。如果你没有按照它给定的函数签名来写很可能导致编译错误。这种问题几乎是所有在线笔试平台的通病。解决办法只有一个考前仔细看题目给出的“函数签名/输入输出”部分。即使你本地跑得好好的提交到平台前也必须对照一遍函数名和参数列表。这个检查动作 10 秒钟但可以避免最无谓的失败。笔试只是整个实习申请流程中的一环它考察的不是你的“毕生所学”而是你在有限资源下快速解决问题的能力。把能准备的全部准备好然后保持冷静。能走到笔试这一步你已经比很多犹豫不决、连投递邮件都没发出去的人强很多了。