首页
/ tech-interview-handbook 实战复盘:用澄清问题掌控编码面试,并用表驱动开发解决批量定价问题

tech-interview-handbook 实战复盘:用澄清问题掌控编码面试,并用表驱动开发解决批量定价问题

2026-09-05 11:05:26作者:柯茵沙

本文基于 tech-interview-handbook 仓库中的博客文章《Take Control Over Your Coding Interview》,围绕一个真实的校招面试真题(按购买数量分档定价)展开:先拆解两种解法的评分差异背后的面试类型差异,给出"提问 + 口头推演(narrate)"的掌控面试方法,再结合仓库内的二分查找参考实现,深入讲解表驱动开发(table-driven development)与二分优化,以及两种写法在 Big O 层面为何同为 O(1) 的底层原因。

1. 一道"比 Fizz Buzz 还简单"的面试真题

设想你是一名大学毕业生,正在参加一场初中级工程师的技术编码面试。面试官给出这样一张价格表,购买量越大,单价越低:

purchase quantity                                  price per unit
    1-5                                              5 dollars
    6-10                                             4 dollars
    11-20                                            3 dollars
    20+                                              2.5 dollars

题目要求:写一个函数,输入购买数量,按表格返回对应单价。输入 5 返回 5,输入 6 返回 4,以此类推。

题目到这里就结束了。直觉上它"甚至比 Fizz Buzz 还简单",你可能预想着会有二叉搜索之类的追问,于是写出了这个最直接的解法:

function getPrice(amount) {
  if (amount >= 1 && amount <= 5) return '5';
  if (amount >= 6 && amount <= 10) return '4';
  if (amount >= 11 && amount <= 20) return '3';
  if (amount > 21) return '2.5';
  return 'unknown price';
}

然而出乎意料的是:面试官在此之后没有追问任何优化点,直接切换了话题。第二天,候选人收到了拒信。

1.1 面试官的真实评分标准:一条爆火的推文

这个问题源自一个真实故事,其出处是 2021 年 9 月一条在中国程序员圈爆火的推文(作者为某公司面试官 @TooooooBug,原文用中文撰写):

最近面试校招生的一个感想:有非常多的同学会写出解法1的代码,这让我很难理解。以至于只要看到有解法2的样子,印象上就会先加分了。

其中"解法 1"就是上面这一堆 if 语句,"解法 2"则是把价格区间和单价抽成一个结构体数组、再对数组做线性查找的表驱动写法。在面试官看来,表驱动解法更模块化、可扩展、可维护;没能写出这个解法,就意味着在面试中被淘汰。

原作者(博客作者 Zhenghao He,Senior Software Engineer at Instacart, ex-Amazon,作者信息见 静态目录)对这道题的评价是:对校招生而言,这是一道相当无意义的面试题,第一种解法完全没问题。文章的三点结论(tl;dr)如下:

  1. 不同风格/类型的面试要求不同的答案,且这一点应当在面试中明确传达。作为一道算法题,第一个解法完全合格。
  2. 掌控你的面试,不要靠猜面试官在想什么——做法是不断提出澄清问题(clarifying questions),直到面试官的所有考察点都对你清晰为止。
  3. 表驱动方法优化的是"变更成本"(optimizes for changes),这正是面试官淘汰未给出该答案的学生的原因。

全篇的主旨:掌控你的编码面试,避免被迫猜测面试官心中的标准答案。

2. 不同类型的编码面试要求不同的答案

一般来说编码面试分两大类:

  1. 算法与数据结构导向的面试;
  2. 实际应用/功能构建(practical app/feature-building)导向的面试,考察的是动手工程能力。

通常只需看题目就能区分两类:翻转二叉树是典型的算法题,而"设计并实现一个自动补全搜索功能"则更偏向实际构建。但两者有时会融合——例如在讨论自动补全服务设计时,可能被要求(部分地)实现一个 Trie

2.1 算法型面试:速度是主要目标

在纯算法型(LeetCode 风格)面试中,你的主要目标是利用合适的数据结构、尽快给出在内存/时间复杂度权衡上正确的高效算法。代码是否可读、可维护、可扩展,或者是否符合社区/行业当前最佳实践,都不是重点,至少是次要的。变量名写成 inpq,原地修改输入数组,只要解法在时间和内存限制内通过测试用例,就不会被判分。

正如 Joel Spolsky 在其博客中写的:算法面试题要看候选人是否足够聪明,能否"几秒钟内推导出一个递归算法,或者以你手写白板最快的速度实现用指针操作的链表函数"。

2.2 工程型面试:可维护性与软件全生命周期

对于更资深的工程师,编码面试往往偏向实际构建这一类:可读、可维护的代码与可扩展的程序结构才是考察重点,软件开发生命周期的方方面面——甚至包括错误处理——都是合理的提问范围,因为它们在实际软件构建中至关重要。

作者在此不作"哪类面试更好"的价值判断,只是指出这两类面试存在,且面试官通过不同类型的面试考察不同的能力。

3. 掌控面试:把问题问在最前面

作为候选人,你需要先弄清这道题属于哪一类,因为底层核心评估标准完全不同——你不想在达成主要目标之前,就把大脑工作内存的容量浪费在次要任务上。

3.1 边做边叙述,口头化所有假设

掌控面试的一个方法是:边做边口头推演你的思路(narrate your thoughts),并明确陈述你做出的每一个假设,以便面试官确认你的方向正确,或者帮你纠偏。

如果作者本人是这道题的候选人,他开场会先问:

"我应该写符合良好工程实践的生产级代码,还是你更关心我用什么算法/数据结构来攻克这个问题?"

这可能是整个面试中投入产出比(ROI)最高的问题。面试官要么让你按时空约束写出一个可工作的解法,要么把它当作一个有真实权衡的现实工程问题来处理。

这一做法在仓库的主站文档中同样被强调:如何求解编码面试题 开篇即指出,拿到题目后候选人应当"先提出澄清问题(clarifying questions),并与面试官讨论几种可能的解法",而这里正是大多数候选人卡住的地方。也就是说,"先提问定边界、再讨论方案"在本书的方法论中是通用的第一步,本文只是用一道真实翻车案例把它讲透。

3.2 为什么这道题是"坏题目"

这道题本身没有考察任何能区分优秀程序员与平庸程序员的核心能力。从面试官心中的"标准答案"(表驱动)来看,它真正考察的是:一个没有任何软件行业工作经验的 CS 应届生是否知道如何用表驱动方法实现这样一个函数。

作者并不相信:一个能在面试中轻松搞定图遍历的聪明大学生,会在几个小时内学不会表驱动这种模式;反之,任何恰好读过《Code Complete》的平庸程序员都能写出表驱动解法并通过面试。知道良好工程实践当然是好事,但把它作为招聘应届生的唯一标准是无意义的,也无助于找到聪明的孩子。

作者还引用了 Joel 的观点:理想的校招面试题至少应覆盖计算机科学两大概念之一——递归(recursion)或指针(pointers)。问这两个概念不是为了它们在日常代码库中无处不在,而是因为它们是测试"在抽象中推理和思维能力"的好工具,而正是这种心智能力区分了优秀的程序员。

故事的寓意:不要害怕提出澄清问题。当面试官没有明确表达期望时,别去猜他在意什么。

4. 表驱动开发:把数据从逻辑中分离出来

解法 2 类似于经典编程书籍《Code Complete》中描述的表驱动开发(table-driven development)。即使没读过这本书,有软件开发经验的人多半也见过这个模式。

它的核心是把数据从逻辑中分离:不要把定价策略(数据)以字面量形式硬编码在函数(逻辑)内部,而是把它们分开。

定价策略是一种业务规则,而业务规则往往是频繁变更的来源。把它编码到一个外部数据结构(即本例中的 priceByRanges 数组)里,程序就能更容易地适应未来变化:定价策略变更时,只需修改数组中的条目,而不用碰函数逻辑。换句话说,我们把不稳定的部分隔离出来,从而把变更的影响范围限制住。

不过作者强调,解法 2 只是"形似"生产代码中的表驱动,还差两点:

  1. 定价数据并未完全从逻辑中分离——priceMap 数组仍定义在函数内部;
  2. 魔法数字(magic numbers)仍然存在。

4.1 更彻底的表驱动写法:数据外置到配置文件

根据定价数据的来源不同,这道题的一个更完整的表驱动变体如下:

// config.js
export const priceByRanges = [
  { min: 1, max: 5, price: '5' },
  { min: 6, max: 10, price: '4' },
  { min: 11, max: 20, price: '3' },
  { min: 21, max: Number.MAX_SAFE_INTEGER, price: '2.5' },
];

// app.js
import { priceByRanges } from './config.js';

function getPrice(amount) {
  // error handling for amount outside the range
  return priceByRanges.find(
    (priceByRange) => amount >= priceByRange.min && amount <= priceByRange.max,
  ).price;
}

此时定价数据独立存放在 config.js 中,priceByRanges模块加载时就被解析完成,业务规则变更完全不需要触碰 app.js 中的逻辑代码。

5. 进一步优化:用二分查找定位价格区间

如果 priceByRanges 数组始终按价格区间有序排列,就可以进一步利用二分查找优化查找逻辑:

const priceByRanges = [
  { min: 1, max: 5, price: '5' },
  { min: 6, max: 10, price: '4' },
  { min: 11, max: 20, price: '3' },
  { min: 21, max: Number.MAX_SAFE_INTEGER, price: '2.5' },
];

function getPrice(amount) {
  if (amount < priceByRanges[0].min) {
    return 'unknown price';
  }

  let start = 0,
    end = priceByRanges.length - 1;

  while (start <= end) {
    const mid = (start + end) >>> 1;
    if (priceByRanges[mid].max < amount) {
      start = mid + 1;
    } else {
      end = mid - 1;
    }
  }

  return priceByRanges[start].price;
}

这段代码的写法与仓库中提供的通用二分查找参考实现高度一致:binarySearch.js 展示了标准的 left/right/mid 收敛模板(mid = left + Math.floor((right - left) / 2)),Python 版本的 binary_search.py 更进一步给出了 bisect_left / bisect_right 两种"插入位置"变体——本文中的二分变体正是这种"找到第一个满足 priceByRanges[mid].max >= amount 的区间"的 lower-bound 思路:循环不变式是"答案始终在 [start, end] 内",while (start <= end) 收敛后 start 即为目标区间下标。对照这两份带断言自测用例的参考实现,可以直观验证循环边界(start <= end vs left < right)不同写法各自返回语义的差异。

5.1 >>> 运算符是什么?

>>>无符号右移 1 位,等价于"除以 2 再向下取整"。例如对 11(二进制 1011)右移一位得到 101(十进制 5)。它避免了对 Math.floor 的显式调用,是 JS 中求 mid 的常见技巧。

6. 性能与 Big O 注记:if 链和循环查找同为 O(1)

直觉上,第一个"笨拙的" if 链解法似乎比第二个"循环"遍历数组的表驱动解法性能更好。

实际上从 Big O 分析看,两种方法具有相同的常数时间复杂度:因为操作次数(即 amount 与价格区间的比较次数)不会随输入(amount)变大而增长。if 链固定最多比较 4 次,find 循环固定最多遍历 4 个元素——常数随"区间条数"而非"输入大小"变化,所以两者都是 O(1)。

更有趣的问题是循环 vs "展开循环"(loop unrolling,即把循环体逐行手写死)的性能差异。作者指出:快速搜索表明 V8 等主流 JavaScript 引擎对循环做了重度优化,但想通过这类**微基准测试(micro-benchmarking)**得到准确结果非常困难,因为性能因引擎和循环体内代码的不同因素差异很大——这提醒面试场景中不必为这种级别的差异做过度优化。

6.1 回到面试场景的完整建议

把全文收束成面试中的可执行动作清单:

  1. 拿到题目先问类型:"这是算法题还是工程题?"——一句话消除最大的不确定性,是整场面试 ROI 最高的问题;
  2. 边写边叙述、口头化假设,让面试官随时可以纠偏,而不是等你写完才发现跑偏;
  3. 算法题:优先正确、快速地给出 O 复杂度正确的解法,if 链完全够用;
  4. 工程题:主动走向表驱动——数据外置(如 config.js)、消灭魔法数字、预留变更入口;若区间有序,可再给二分优化展示功底;
  5. 复杂度的讨论要基于"操作次数是否随输入规模增长",而不是"代码里有没有循环"。

这套方法的依据可在仓库中继续延伸阅读:求解编码面试题的核心技术 提供了"可视化问题、手工求解、补例、拆解、套用常见数据结构"等结构化求解步骤,与本文"先提问、再求解"的主张互为补充。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384