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