深蓝黑背景封面。左侧是一组现场演算:九个灰色数字方块横排写着 1 到 9,代表 ISBN 前九位;每个方块下方标着对应权重 ×1 到 ×9;再往下一条青色算式条写「加权和 = 1×1+2×2+…+9×9 = 285」,箭头指向一个紫色圆角框「285 mod 11 = 10」,再指向结论框「余数 10 记作 X,识别码就是 X」。演算区下方一行小字注「前 9 位分别乘 1~9 求和,对 11 取余,余 10 时用字母 X 顶替」。右侧大标题两行「ISBN 的最后一位识别码」「是怎么算出来的」,副标题「一次加权求和加一次模 11,就能验出一本书的‘身份证’真伪」,小注「从算识别码到验整串:同一套规则的正反两用」。左上标注「面试经典 · 算法」,右上圆角胶囊标注「Day 10 · 12-07」。 图 1:本文封面,ISBN 识别码的加权求和演算全过程。

先说明属性:这是一篇笔试真题复盘,题目出自本地历史笔试题集整理,与任何公司的当前招聘无关。题面很生活化:一本正式出版的图书都有一个 ISBN 号码,前 9 位是数字,第 10 位是识别码;识别码的算法是前 9 位从左到右分别乘 1 到 9 再求和,对 11 取余,余数就是识别码,余 10 时记作大写字母 X。任务是读入号码的前三段,算出识别码并输出完整号码;输入不合法就输出错误。这是一道典型的工程模拟题,不考数据结构巧思,考的是你能不能把一条文字规则毫无歧义地落成代码。

一句话结论:识别码等于前 9 位加权和对 11 取余,余 10 记 X;生成与校验是同一段逻辑的正反两用,真正的区分点在输入合法性检查。

先把规则讲透:一次加权求和加一次模 11。

ISBN-10 共 10 位,前 9 位是纯粹的数字(语言区、出版社、书序号),第 10 位是专门用来防错的识别码。拿公开样例 0-306-40615-? 走一遍:去掉分隔符后前 9 位是 030640615,从左到右乘权重 1 到 9,即 0×1+3×2+0×3+6×4+4×5+0×6+6×7+1×8+5×9=145,145 对 11 取余得 2,识别码就是 2,完整号码写作 0-306-40615-2。为什么要有个 X?因为模 11 的余数有 0 到 10 共 11 种取值,阿拉伯数字只有 10 个,多出来的 10 就用字母 X 顶替,保证识别码永远只占一个字符位——前 9 位是 123456789 时,加权和 285 对 11 取余正好是 10,识别码就是 X。

两个方向,同一段逻辑。

这道题其实有两种考法。生成方向:给定前 9 位,算出第 10 位,笔试题面就是这个问法。校验方向:给定完整 10 位,判断它是不是一个合法号码,常见于表单录入防呆。校验不需要另写公式——用同一个函数算出「应有的识别码」,跟第 10 位比对即可。数学上它等价于另一个常见表述:10 位分别乘权重 1 到 10 求和,结果必须是 11 的倍数。两种表述相通,因为第 10 位的权重 10 在模 11 意义下就是 -1。面试时把这个等价关系点出来,说明你真的理解了而不是背了一段代码。

完整实现:生成、校验、输入合法性一把抓(Node.js 22.15.0,node isbn.js):

function checkCharOf(nineDigits) {
  let sum = 0;
  for (let i = 0; i < 9; i++) {
    sum += Number(nineDigits[i]) * (i + 1); // 权重是 i+1(1~9),不是下标 0~8
  }
  const r = sum % 11;
  return r === 10 ? 'X' : String(r);        // 余 10 映射成 X,漏掉这步会算错一成号码
}

// 生成方向:给前 9 位(可带分隔符),输出完整 ISBN
function toFullISBN(raw) {
  const digits = raw.replace(/-/g, '');
  if (!/^\d{9}$/.test(digits)) return 'ERROR'; // 前 9 位必须全是数字
  return raw + '-' + checkCharOf(digits);
}

// 校验方向:给完整 10 位,先查输入合法性,再比对识别码
function isValidISBN(raw) {
  const s = raw.replace(/-/g, '').toUpperCase(); // 清洗分隔符、统一大写
  if (s.length !== 10) return false;             // 长度必须是 10
  for (let i = 0; i < 9; i++) {
    if (s[i] < '0' || s[i] > '9') return false;  // 前 9 位出现 X 也判非法
  }
  const last = s[9];
  if (!/\d/.test(last) && last !== 'X') return false; // 第 10 位只能是数字或 X
  return checkCharOf(s.slice(0, 9)) === last;
}

实测输出:

== 生成方向 ==
0-306-40615  ->  0-306-40615-2
1-234-56789  ->  1-234-56789-X     (加权和 285,余 10 记 X)
0-804-42957  ->  0-804-42957-X
== 校验方向 ==
0-306-40615-2    -> true
1-234-56789-X    -> true
0-306-40615-3    -> false   (识别码错一位)
0-306-40651-2    -> false   (相邻两位换位也能抓到)
0-306-4061X-2    -> false   (X 出现在非末位)
0-306-40615      -> false   (长度不够)
1-234-56789-x    -> true    (小写 x 统一大写后也认)

输入合法性检查才是这道题的区分点。

加权求和谁都会写,拉开差距的是四个易错点。第一,权重是 1 到 9,不是数组下标 0 到 8,循环里要写 i + 1。第二,X 只在第 10 位合法,前 9 位出现 X 必须直接判错——把校验函数写成「遇到 X 就当 10 算」会放过非法输入。第三,比较前统一大写,用户手输小写 x 不该被拒。第四,先清洗再判定:去掉空格与连字符之后才谈长度和字符集,顺序反了就会误判带分隔符的合法输入。这四个点正好对应笔试里「输入不合法输出错误」那一半分值。

为什么偏偏是模 11?

因为 11 是大于 10 的最小质数,每位权重互不相同且与 11 互质,于是任何一位抄错、任何相邻两位写反,加权和的余数都会变——实测里换位用例被判 false 就是这个性质的直接体现。作为对比,2007 年起启用的 ISBN-13 改用 13 位、加 978 前缀,权重 1 和 3 交替、对 10 取余,识别码不再有 X,但对「差值为 5 的相邻换位」存在盲区。模 11 加 X 正是为了换来两类常见录入错误的全检出。

深蓝黑背景概念图,顶部小字「概念图 · 加权校验码」,主标题「从输入到判定:五步走完 ISBN 校验」。画面主体是一条自上而下、中部带分支的流程图:第一步「读入字符串并清洗:去掉空格与连字符」,第二步菱形判断「长度是否为 10?前 9 位是否全为数字?第 10 位是数字或 X?」,右侧引出分支「任一不满足 → 直接判非法输入」,主线下行到第三步「加权求和:sum = Σ i × dᵢ,i 取 1 到 9」,第四步「取余:r = sum mod 11,并把 r 映射为字符:0~9 直写、10 映射为 X」,第五步菱形判断「映射字符 == 第 10 位?」,向下两个出口分别落到「合法 ISBN」与「非法 ISBN」两个结论框。流程图右侧一张竖向备忘卡,标题「实现易错点」,列出四条:权重是 1~9 而不是 0~8;X 只可能出现在第 10 位;比较前统一大写;取余结果 10 必须映射成字符 X。底部总结条:算识别码与验 ISBN 是同一段逻辑——生成时把余数写入第 10 位,校验时比对第 10 位;最底行小字:时间复杂度 O(n),n 为固定位数 10,实际就是常数时间。 图 2:从输入到判定的五步校验流程与四条实现易错点。

面试现场的答题结构。

先一句话复述规则并给出识别码公式,再主动分两个方向讲:生成方向给前 9 位算第 10 位,校验方向用同一函数比对末位,顺带点出「权重 1 到 10 求和是 11 的倍数」这个等价表述。然后写代码,重点不是循环本身,而是把输入合法性四条易错点逐条处理掉。最后升华一句工程视角:校验码本质是给数据加一位冗余换取错误检出能力,模 11 的选择换来单字符错误与相邻换位的全检出,同样的思路在银行卡号、身份证尾号里都能看到。你在项目里写过类似的校验码或防呆逻辑吗?踩过「输入合法性没查全,脏数据混进库」的坑吗?欢迎聊聊。