☰
相亲遇到算法题:从两数之和到电梯调度的程序员真实复盘
2026/10/11 16:17:09 网站建设 项目流程

相亲这种事,我见过不少离谱的,但被对方在餐巾纸上写代码题现场考验,确实是头一回。

事情起因很简单,家里安排一场相亲,约在某家茶馆。我当时还特意早到了十分钟,想着第一次见面总得留个稳重的印象。结果我落座没多久,对面的女生也到了——她非常自然地放下包,从口袋里掏出一支圆珠笔,然后把餐巾纸推到我面前。

我以为是让我写联系方式。低头一看,上面写的是:

two_sum(nums, target)

我愣了一下,抬头看她。她很认真地说:“先别急着自我介绍。想聊聊这个吗?”

这就是整件事的开端。作为一个写了快十年业务的程序员,相亲桌上被考算法题,这个场景新鲜到让我后来复盘了很久。这篇文章就聊聊这段奇遇,以及我在每道题背后的真实思考。

1. 缘起:相亲地点选在茶馆,第一句话是道算法题

整个相亲过程与其说像约会,不如说像一场即兴的技术面试。我后来才意识到,她递过来的那行函数签名,其实是一道筛选题目——她在测试我听到“算法题”三个字时的第一反应。

很多程序员被突然甩一道算法题的时候,会进入一种默认的防御状态,觉得对方在刁难自己。我的第一反应倒是还好。因为我觉得,既然人家愿意花时间在这张餐巾纸上写下函数签名,那说明她对“程序员”这个身份至少是认真对待的,而不是觉得你只会修电脑。

当时我反问了她几个问题,现在想想,这些恰恰是“面试官”最想听到的:

  • 是要求手写代码,还是可以讲思路?
  • 返回值是下标,还是返回数值本身?
  • 数组里有没有重复元素,需不需要考虑顺序?

她听我说完之后笑了一下:“你想得还挺细的。都可以,你想怎么写就怎么写,讲清楚就行。”

我后来复盘这一段,才慢慢想明白:她根本没有预设答案。她想知道的是,面对一道不设规则、没有明确输入输出约定的半开放问题,我会不会先问清楚,还是直接闷头开写。这是程序员的专业习惯,也是一个人长期做事习惯的缩影。

那天的程序题一共三道,外加一道设计题。我把过程完整复盘一遍,每道题都附带我在相亲当下的真实心路历程和事后反思。

2. 第一道题:两数之和——别急着秀HashMap

2.1 经典解法背后藏着的“送命题”

两数之和确实是LeetCode的入门第一题,大部分人都刷过。但问题是,这道题有很多容易翻车的细节。她出的这道题看起来简单,实际上考验的是“解题前的问询”环节。

常规解法是用哈希表,时间复杂度 (O(n)),空间复杂度 (O(n)):

def two_sum(nums, target): seen = {} for i, num in enumerate(nums): complement = target - num if complement in seen: return [seen[complement], i] seen[num] = i return []

我一笔写完,正准备递回去。她按住餐巾纸说:“等一下。如果 nums 是排好序的呢?空间能省吗?”

这个问题才是真正的考验。排序数组的两数之和,可以用双指针从两端向中间夹逼,时间复杂度 (O(n)),但空间复杂度能压到 (O(1)):

def two_sum_sorted(nums, target): left, right = 0, len(nums) - 1 while left < right: current = nums[left] + nums[right] if current == target: return [left, right] elif current < target: left += 1 else: right -= 1 return []

如果面试官问“能不能用双指针”,其实是考察你知不知道处理有序和无序数据的区别。我之前见过一些人,不管什么变体都先HashMap走起,遇到这个追问就卡住——因为极少有人在写“两数之和”的时候,真的跑过“返回所有组合”和“要求不重复”的场景。

2.2 她从这道题里看出来的东西

事后我才知道,她出这道题真正的意图不是看我会不会写哈希表。她后来给我的评价是:“你会在下笔之前问清楚返回下标还是返回值,这让我很意外。”

这句话让我意识到,很多人面对“简单题”往往容易轻敌。尤其是两数之和这种刷过无数次的基础题,最容易犯的错误是背答案,而不是真正理解题意。比如:

  • 如果要求返回所有解,通常还需要排序加去重。
  • 如果数组长度极大但值域很小,计数数组反而更高效。
  • 如果允许最多一次排序,双指针的时间复杂度会接近于排序的复杂度。

这些要比“会不会写HashMap”重要得多。她出这道题,其实是在筛掉一类人——把刷题当表演、遇到变体就慌的人。很遗憾,这种人在相亲市场上并不少见,有些人简历写得天花乱坠,一上桌连题目都没听清楚就开背。

3. 第二道题:最长回文子串——从时间争夺到心态观察

3.1 中心扩展法的优势

第一道题算是平稳落地。她收回餐巾纸,看了一眼,又推过来第二道题。这次她甚至没等我说“好”,就直接开口了:

“最长回文子串。你可以用你惯用的方式写。”

这道题常见解法有三种:暴力枚举、动态规划、中心扩展。暴力不说了,一般就是用来垫底的。动态规划倒是很多人喜欢用,但那个二维数组往往把简单题目搞复杂了。我选了中心扩展法:

def longest_palindrome(s): if not s or len(s) < 2: return s start, max_len = 0, 0 def expand_around_center(left, right): while left >= 0 and right < len(s) and s[left] == s[right]: left -= 1 right += 1 return left + 1, right - 1, right - left - 1 for i in range(len(s)): # 奇数长度回文 l1, r1, len1 = expand_around_center(i, i) # 偶数长度回文 l2, r2, len2 = expand_around_center(i, i + 1) if len1 > max_len: start, max_len = l1, len1 if len2 > max_len: start, max_len = l2, len2 return s[start:start + max_len]

中心扩展法的核心逻辑很直白:枚举每一个可能的中点,从中心向外扩散,直到两边字符不相同为止。关键点是回文串有两种形态,奇数长度和偶数长度,所以每个位置要做两次扩展。

当时她看我没选动态规划,问了一句:“为什么不用动态规划?”

我喝了口茶水,说:“这道题动态规划也能做,但空间复杂度是 (O(n^2))。我们只要求最长子串,不要求所有子串的答案,所以没必要把整张表算出来。中心扩展是 (O(n^2)) 时间、(O(1)) 空间,写起来不容易错,边界情况也少。”

她没说话,但我注意到,她的眼神里多了一点“行,你有点意思”的味道。

3.2 她把计时器放上桌:压力下的真实表现

让我真正紧张的,是在我讲完思路、准备落笔的时候,她从包里拿出一个手机,打开计时器,翻过来扣在桌上。

我第一次面对“相亲版限时算法题”,那个计时器的界面我至今记得,白色背景,黑色大字,一秒一秒地跳。

说实话,我手确实抖了一下。但抖完之后,我做了一件事——我没有立刻写代码,而是把嘴里的水咽下去,在心里把“枚举中心”“向两侧扩展”“奇偶分治”这三个步骤重新过了一遍。这个过程大概只花了二十秒,但对我来说很关键。

我当时在想:如果她真是想看我被时间卡住之后怎么表现,那我抱怨和慌张都没有意义。唯一能做的事,就是盯着问题本身拆解。正因为那二十秒的“停顿”,我反而找回了平时写代码的节奏。

最后耗时大概七分半,加上讲解一共九分钟左右,比一道中等难度的正式面试题正常耗时还短一些。她按掉计时器,说了一句:“把时间放宽到正常的两倍再评价你,会更公平。你能在压力下先把结构理清楚,这点我很欣赏。”

我后来才明白,第二道题真正考察的是一个人的“情绪稳定度”。刷题刷得多,谁都能背出中心扩展法的代码。但能在计时器翻面之后,依然按照自己的节奏先把结构理清楚,这才是她在观察的东西。

这个经验我想分享给任何人:在任何时间受限的场景下——不论是算法题还是生活中的突发事件——最重要的不是“反应快”,而是“先想清楚再动”。慢下来反而更快。

4. 第三道题:二叉树最近公共祖先——递归边界的灵魂拷问

前两道题顺利过关,我以为这顿饭局大概就是要以一个“我刷题水平还行”的结论收尾了。结果她话锋一转,说:“再来一道树的题吧,考过二叉树的人都懂,这是最容易露馅的部分。”

她这次自己写了一个结构:

class TreeNode: def __init__(self, val): self.val = val self.left = None self.right = None

然后在下面写了:lowest_common_ancestor(root, p, q)。

我一看这题目,心里就稳了。这道题我练过很多次,递归写法其实非常简洁,难点不在于代码本身,而在于边界条件。标准解法是:

def lowest_common_ancestor(root, p, q): if not root or root == p or root == q: return root left = lowest_common_ancestor(root.left, p, q) right = lowest_common_ancestor(root.right, p, q) if left and right: return root return left if left else right

我写完这段,正准备讲递归过程,她又补了一句:“如果树很大,递归深度可能炸掉,栈会爆。你能改成迭代吗?”

这是个很经典的空间复杂度追问。递归版本的时间是 (O(n)),空间是树的高度 (O(h))。如果树退化成链表,高度等于 (n),递归深度就有上万层,直接崩掉。她显然很清楚这件事。

我思考了一下,给了个方案骨架:用后序遍历的迭代版本模拟递归,维护“左右子树都处理完再决定返回值”的语义。核心是用一个栈保存状态,或者用双栈模拟后序,并额外记录p和q是否已在子树中找到:

def lowest_common_ancestor_iter(root, p, q): stack = [(root, False)] ancestors = {root: None} # 第一遍后序遍历:记录每个节点的父节点 while stack: node, visited = stack.pop() if not node: continue if not visited: stack.append((node, True)) stack.append((node.left, False)) stack.append((node.right, False)) else: if node.left: ancestors[node.left] = node if node.right: ancestors[node.right] = node # 此时p的路径已经可以回溯出来 p_ancestors = set() while p: p_ancestors.add(p) p = ancestors[p] while q not in p_ancestors: q = ancestors[q] return q

当然,这个思路只是把递归改成了显式栈,工程上会更可控。她还追问了一个边界用例:“如果 p 本身就是 q 的祖先,你的递归代码会正确返回吗?”

这正是递归代码容易翻车的精妙之处。很多人的递归版本在遇到root == p时直接返回root,没有继续向下找 q。但如果 p 就是 q 的祖先,那么找到 p 即可直接返回 p,不需要再判断 q 是否存在——因为 q 一定在 p 的子树里。这个“提前返回”看着反直觉,其实是正确的。但如果方向反了,假设 p 在左子树、q 在右子树,那么递归回溯到最近的交汇节点时,左右都不为空,返回那个节点才对。

我认为这道题她出得最妙的地方在于:很多人刷树题只记递归模板,却不知道为什么递归的返回条件是这样写。她追问的每一个细节,其实都在拆穿“背题”的人和“真的理解”的人之间的区别。

5. 附加题:电梯调度设计题——从算法到工程思维的跳转

我以为到这里就结束了。结果她喝完一口茶,非常平静地说:“算法题我们就到这里。再聊一个设计题吧,和现实更贴近一点。”

“假设你在一栋六层的写字楼里,设计一套多电梯调度系统。核心需求是减少平均等待时间。你会怎么搭核心逻辑?”

我做好心理准备的问题清单里真没有这一题——电梯调度不是纯算法题,它本质上是场景建模和工程取舍的综合题。但这反而激起了我的兴趣,因为这种题才是平时项目里真正会碰到的问题。

我先列了三个最要命的未知数:

  • 是单电梯还是多电梯?多电梯是否共用一个召唤按钮?
  • 电梯内外的请求优先级是“主方向优先”还是“最近楼层优先”?
  • 系统允许超载吗?超载后如何重新分配客流?

她说:“设定为六层楼、两部电梯,外面的上下行按钮共用。外部请求优先响应,符合当前主方向的顺路请求优先停靠。”

于是我给了个核心思路:把每部电梯建模成一个有限状态机,状态包括上行、下行、空闲。每次有新请求时,先判断请求的方向与电梯当前方向是否一致、是否在电梯行进路径的顺路楼层上。在实现上,可以用两个优先队列(一个上行请求堆,一个下行请求堆)来维护目标楼层集合:

  • 电梯向上时,每次停靠距离当前楼层最近的、且方向为上行的外部请求。
  • 内部按钮请求始终实时并入对应方向的堆里。
  • 当某个方向的请求清了,切换状态,开始处理反方向。

这个过程本质上是一种“扫描线”思想——把所有请求按楼层和方向组织起来,再用方向切换来模拟电梯的实际运动。如果请求分布极不均匀(比如所有人都同时在一楼等上行),那就需要引入“响应优先级轮转”来避免某部电梯被彻底闲置。

她听完之后说:“思路是清楚的。但我更想听的不是理论,而是你会怎么部署。比如,你会如何测试这套系统?”

这个转折特别关键。设计题回答得再好,如果落不到测试、上线、迭代上,就始终是纸上谈兵。我回答的时候强调了三件事:

首先,我会设计一个高效的离散事件模拟器,用随机客流生成脚本模拟早高峰和午高峰,比较不同调度策略的平均等待时间。其次,我会给系统加入“故障注入”,故意让一部电梯在某层卡住,观察另一部能否顶上。最后也是最重要的,我会先明确衡量指标——平均等待时间太虚了,真实业务中还得看“最长等待时间”和“电梯空转率”。

她说:“这个回答比前面码代码的时候还要好。”

6. 最后的反转:她真正看重的不是答案,而是——

到这里,我已经完全进入了“面试者”状态。正当我等着她继续出题的时候,她却放下了圆珠笔,把餐巾纸翻了回去。

“没什么要考的了。你知不知道,我为什么要在相亲桌上做这件事?”

我摇头。她说:“上一周我们组里招人,面试了一个简历写得非常漂亮的工程师。笔试阶段表现极好,结果到了白板环节,他写不出来却开始强行编答案,编不圆就反过来责怪面试官出题太偏。技术其实可以慢慢学,但心态和沟通方式很难改。我那天就想,如果相亲也能像面试一样干脆就好了。”

这句话让我在茶馆里沉默了挺久。她其实是拿相亲在做一次“最低成本的信任测试”——算法题不是目的,目的是观察我在陌生场景、高压环境下,会不会诚实地说“我需要想一下”,而不是硬编;会不会先问清楚需求,而不是自以为是地开写;会不会在遇到不会的边界问题时,大大方方承认,然后一起拆解。

那顿饭的结局和大多数人想的不一样,我们之后又聊了很多和代码无关的事:近期的书、常听的播客、对工作的态度。技术题只是一张入场券,真正让两个人聊起来的,反而是在技术考完之后的那杯茶。

我事后花了两天把这些题目重构了一遍,也把那几天在脑子里翻来覆去复盘出来的思考写成文字。有些经验想留给其他程序员参考:

  • 不熟算法的人可能觉得“相亲让人刷LeetCode”很奇葩,但换个角度想,如果一个人愿意花时间出题来了解你,说明她认真对待这次见面,也比坐在那里查户口有意思得多。
  • 被突然抛来问题时,别急着进入防御模式。先拆解问题、确认边界,这个习惯在任何场景下都是加分的。
  • 就算题目卡住了也不丢人,“诚实地说我需要想一下”比“硬着头皮编一个自己都不信的回答”要好得多。这个道理我在代码评审里验证过无数次,没想到在相亲桌上同样成立。

最后再分享一个小技巧。如果有人也愿意在类似场景下“出题考验”你,不要把它当成面试,可以把它当成一次回音测试:你的性格、思维方式、面对压力时下意识的反应,都会在这个过程里被折射出来。装是装不久的,不如坦荡地把真实的自己展示出来——遇到懂的人,这反而是最好的沟通方式。

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

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

立即咨询