深蓝黑背景品牌封面:左侧大标题「字符串 ↔ 序号」,副标题「互转算法与进制思维」;右侧原创装饰——一张竖排的进制对照小表:A=1、Z=26、AA=27、AB=28,表旁一条双向箭头把「字符串」与「整数序号」两个方块连起来,暗示两个方向的互转;左上标注「历史题目复盘 · 数据处理」,右上标注「Day 05 · 10-13」 图 1:本文封面,A=1、Z=26、AA=27 这张小表就是全部题意。

先说明属性:这是一篇历史题目复盘,题目出自本地历史面试资料,与任何公司的当前招聘无关。这类「字符串和序号互转」的设计题,本质是考察你能不能认出它是一张换皮的进制转换卷。

想象面试现场:「设计两个算法:把 A、B、…、Z、AA、AB 这样的字符串映射成 1、2、…、26、27、28 这样的序号,还能转回来。说说你的思路和边界情况。」

第一步,点破本质:这是二十六进制,但没有 0。

从左到右读字符串,每一位的权值是 26 的幂,这和十进制、十六进制是同一套结构。差别只有一处:常见进制的每一位取值是 0 到 25(或 0 到 9),而这套字母体系的每一位取值是 1 到 26——没有 0。A 是 1,Z 是 26,AA 是 27。没有 0 这一点看着小,却是本题全部的坑所在:编码方向每取一位,都要先做一步「余 0 当 26、高位借 1」的修正。

第二步,两个方向的算法。

解码(字符串到序号)是简单方向:从左到右扫,结果等于结果乘 26 加上当前字母的值。以 AB 为例:1 乘 26 加 2,得 28。

编码(序号到字符串)是麻烦方向:反复对序号减 1、除 26 取余,余数映射回字母,从低位向高位逐位生成,直到商为 0。为什么要先减 1?因为没有 0 的进制里,「整除」不代表这一位是 0,而是代表这一位是 26(Z),同时高位要被借走 1。先减 1 再取余,就把借位动作一次性做掉了。

第三步,写出可运行代码并实测。

下面这份实现包含两个方向的互转、经典样例、一百万次往返自验,以及一个对照变体——短网址风格的 62 进制编码(有 0,不需要借位)。用 Node.js 22.15.0 直接运行(node excel.js)即可:

'use strict';

// 字符串 -> 序号:二十六进制累乘(A=1 … Z=26)
function titleToNumber(s) {
  let n = 0;
  for (const ch of s) n = n * 26 + (ch.charCodeAt(0) - 64);
  return n;
}

// 序号 -> 字符串:没有 0 的进制,每位先「减 1 借位」再取余
function numberToTitle(n) {
  let s = '';
  while (n > 0) {
    n -= 1;                                    // 余 0 当 26 用,高位要借 1
    s = String.fromCharCode(65 + (n % 26)) + s;
    n = Math.floor(n / 26);
  }
  return s;
}

// 经典样例
for (const s of ['A', 'Z', 'AA', 'AB', 'AZ', 'BA', 'ZY', 'ZZ', 'AAA']) {
  console.log(`${s} -> ${titleToNumber(s)}`);
}

// 往返自验:1 到 100 万,编码再解码必须回到原数
let bad = 0;
for (let i = 1; i <= 1000000; i++) {
  if (titleToNumber(numberToTitle(i)) !== i) bad++;
}
console.log('1..1000000 往返自验,失败数:', bad);

// 同构变体:短网址常用的 62 进制(0-9 a-z A-Z),因为有 0,不需要借位
const ALPHA62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
function idToShort(id) {
  let s = '';
  while (id > 0) { s = ALPHA62[id % 62] + s; id = Math.floor(id / 62); }
  return s || '0';
}
function shortToId(s) {
  let n = 0;
  for (const ch of s) n = n * 62 + ALPHA62.indexOf(ch);
  return n;
}
console.log('短编码示例: 123456789 ->', idToShort(123456789), '-> 解码回', shortToId(idToShort(123456789)));

实测输出如下:

A -> 1
Z -> 26
AA -> 27
AB -> 28
AZ -> 52
BA -> 53
ZY -> 701
ZZ -> 702
AAA -> 703
1..1000000 往返自验,失败数: 0
短编码示例: 123456789 -> 8m0Kx -> 解码回 123456789

样例全对,一百万次往返零失败;最后两行顺手证明了同一件事:短网址把数字 ID 编成 62 进制短串,Excel 列号是 26 进制,模型词表里 token id 与文本互转——全是「字符串↔序号」这同一个映射换了个字母表。

原理加算法图:上部点破本质——这是二十六进制计数,但每一位的取值是 1 到 26、没有 0,所以比常见进制多一步「余 0 当 26、高位借 1」的修正。中部左右两条算法路径——解码(字符串到序号):从左到右,结果等于结果乘 26 加上当前字母的值,例如 AB = 1 乘 26 加 2 = 28;编码(序号到字符串):反复对序号减 1 后除 26 取余,余数映射回字母,从低位向高位逐位生成,直到商为 0。下部是同构应用——Excel 列号是 26 进制、短网址把数字 ID 编成 62 进制短串、模型词表的 token id 与文本互转,全是同一个「字符串↔序号」映射的换皮。底部结论:认出进制,题目就解了一半;剩下的一半是处理「没有 0」带来的借位细节 图 2:进制本质、两个方向的算法与同构应用。

可能的追问与易错点。

「输入校验呢?」要约定非法字符、空串、序号为 0 或负数的行为——映射定义在 1 到正无穷上,0 没有对应字符串。「序号很大怎么办?」十进制序号乘 26 会涨得很快,要确认目标语言的整数精度:JavaScript 的安全整数上限约 9 千万亿,超长字符串需要换大整数类型。易错点集中在编码方向:忘记先减 1,遇到 26 的倍数(Z、AZ、ZZ)就会错一位;或者把生成顺序搞反,拼出的字符串低位在前。自验也有一句方法论:这种互转题最硬的验收标准就是大规模往返测试,比盯着边界样例看半天可靠得多。

回到面试现场,建议的答题节奏:先用十进制类比点破「这是进制转换」,再指出「没有 0」这个差异,解码方向随手写出,编码方向重点讲借位修正,最后用往返自验收尾。认出进制,题目就解了一半——另一半是细节,而细节正是面试要看的部分。

你第一次发现某道面试题「其实是另一道题的换皮」是什么时候?欢迎分享。