☰
年号字串与Excel列号转换:深入理解无零的伪26进制算法
2026/9/26 7:09:48 网站建设 项目流程

这题名字听着挺唬人,P605 年号字串,说白了就是把一个正整数变成一串字母:1 对应 A,2 对应 B,26 对应 Z,27 对应 AA,2019 对应 BYQ。我第一次做这道题的时候,第一反应就是“这不就是个 26 进制转换?”结果按普通进制写法写完,样例全挂,对着输出发呆了半小时。后来才想明白,它和真正的 26 进制差了关键一步:这个系统里没有 0,每一位都从 1 开始。

这篇文章适合三类人:准备算法竞赛的学生、要写 Excel 导出功能的老哥,还有面试前突击进制转换的求职者。我会把这类题的数学本质、C/Python/JavaScript 实现,以及我踩过的边界坑一次性讲透。看完你不仅能秒杀这道题,还能顺手解决 Excel 列号转换这类实际问题。

1. 题目到底在考什么:一个没有零的“伪26进制”

1.1 先看清题目:不是简单进制转换

题目本身很短:把正整数映射成字母串,映射规则是 A=1,B=2,……,Z=26,AA=27,AB=28,依此类推。这个规则一旦写出来,你应该马上能意识到它和普通 26 进制不一样——普通 26 进制里每一位的取值范围是 0 到 25,而这里每一位的取值范围是 1 到 26。

这个差异就是整道题的题眼。很多人在这一步栽了跟头,因为他们把 1 到 26 的映射看成 0 到 25 的映射,直接在循环里'A' + n % 26,结果 26 输出成了 BA 而不是 Z,27 输出成了 BB 而不是 AA。不是代码写错了,是数学模型没对上。

1.2 用三个数字验证差异

光说“不一样”不够,直接看一组对照就能彻底明白。我按“本题正确结果”和“普通 26 进制 0=A 的结果”列了一张表:

数字年号字串正确结果普通26进制(0=A)的结果问题出在哪
25YZ普通进制下 25 对应 Z,但本题 25 对应 Y
26ZBA普通进制下 26 需要进位,但本题 26 还是单字符 Z
27AABB普通进制下 27 对应 BB,但本题 27 对应 AA

看到没?普通 26 进制里,26 是“进一位”的临界点;而在年号字串里,26 还在最后一位“Z”的射程内,27 才开始产生第二位。这就是“没有零”带来的直接后果:每一位都在 1 到 26 之间走满,才轮到下一位。

1.3 为什么会出现“没有零”的映射

这需要把视角从进制转到“字符表”上。普通 26 进制把 26 个字符当成符号集合,其中第一个字符代表 0,最后一个字符代表 25。年号字串把 26 个字符当成数字集合,其中第一个字符代表 1,最后一个字符代表 26。

换句话说,普通进制是一个封闭的模算术系统,0 是合法的位值;年号字串这个系统没有 0,它从 1 开始计数。想让取模运算的余数区间 0 到 25 和字母映射区间 1 到 26 对齐,就必须把所有数字整体减 1,把 1 变成 0、26 变成 25。这是整道题算法设计的起点。

2. 核心推导:为什么每次都要先把数字减一再取模

2.1 最底层那位怎么确定

假设要转换一个数字 n,我们需要先确定它的最低位。如果 n=26,最低位显然是 Z,没有进位。但26 % 26 = 0,如果你用 0 对应 A 的做法,最低位就变成了 A,这显然是错的。

解决办法是先把 n 减 1,变成 25,再取模:25 % 26 = 25,25 对应 Z,正好。这里减 1 的本质是调整映射偏移量:原来数字 1 到 26 对应字母 A 到 Z,而取模结果的范围是 0 到 25。为了让两个区间重合,把数字整体平移一格,1 变 0,2 变 1,……,26 变 25。

2.2 下一步的 n 应该怎么更新

这个坑比第一步更隐蔽。很多人以为处理完一位后直接n /= 26就行,但正确做法是“先减一再整除”。用数学语言说:设 m = n - 1,m 可以写成m = 26 * q + r,其中 r 在 0 到 25 之间。那么原来的 n = 26 * q + (r + 1),而 r + 1 正好落在 1 到 26 之间。

转换到迭代操作上,就是:

n = n - 1 输出 A + n % 26 n = n // 26

这里的n // 26实际使用的是减 1 之后的 n,所以等价于(原n - 1) // 26。为什么不能直接原n // 26?举个例子,n=26,如果直接商是 1,意味着还要再输出一个高位字符,但正确答案就是 Z,不应该有第二位。用(26-1)//26 = 0,商为 0,循环结束,输出只有一个 Z。

2.3 2019 完整手算过程

用代码逻辑手算 2019,非常直观:

  • 第一次迭代:n=2019,先执行 n=2018。2018 % 26 = 16,对应 Q(A=0,所以 16 是第 17 个字母 Q)。2018 // 26 = 77。
  • 第二次迭代:n=77,先执行 n=76。76 % 26 = 24,对应 Y。76 // 26 = 2。
  • 第三次迭代:n=2,先执行 n=1。1 % 26 = 1,对应 B。1 // 26 = 0,循环退出。

依次收集到的字符是 Q、Y、B,因为先从低位开始,所以最后要逆序输出,得到 BYQ。这和官方答案完全一致。

从这个手算过程你能看到,整道题的核心就一个操作:每次取出当前最低位时先减一,然后取模取商。它不是什么高深数论,就是偏移量处理的直接应用。

3. 三行思路四种语言:正向转换的代码落地

3.1 一个共同模板

其实这类题的代码模板可以写得很短:

结果集合 = [] 当 n > 0: n = n - 1 结果集合.append(字符(A + n % 26)) n = n // 26 返回 结果集合 的逆序

后面所有语言实现都是这个模板的翻译。唯一的语言差异在于“字符”和“逆序”这两个操作怎么表达。

3.2 C 语言实现及其中的细节

C 语言代码里最需要注意的是字符数组的初始化和末尾的\0。直接看我给的完整实现:

#include <stdio.h> void yearToName(int n, char *out, int outSize) { char tmp[32]; int len = 0; while (n > 0) { n--; tmp[len++] = (char)('A' + n % 26); n /= 26; } if (len >= outSize) { out[0] = '\0'; return; } for (int i = 0; i < len; i++) { out[i] = tmp[len - 1 - i]; } out[len] = '\0'; } int main() { char buf[32]; yearToName(2019, buf, sizeof buf); printf("%s\n", buf); return 0; }

这里我用tmp[32]临时数组收集逆序前的字符。为什么不直接在最终数组里倒着填?因为我不知道最终长度。开 32 字节足够覆盖 int 范围内所有数字,log26(2^31-1)大约是 7 位,加上结尾符也不会超过 8 字节。不过为了稳妥和未来扩展,给足缓冲区是最简单的习惯。C 字符串相关的操作,比如长度、数组初始化、末尾\0,在这段代码里全都能复习一遍。

3.3 Python 和 JavaScript 的实现

Python 写起来最接近人的思维:

def year_to_name(n: int) -> str: chars = [] while n > 0: n -= 1 chars.append(chr(ord('A') + n % 26)) n //= 26 return ''.join(reversed(chars))

JavaScript 要注意一个原生坑:/是浮点除法,不是整除。我见过很多人在 JS 里漏掉Math.floor,导致 n 永远不能归零,直接死循环。正确写法:

function yearToName(n) { const arr = []; while (n > 0) { n--; arr.push(String.fromCharCode(65 + (n % 26))); n = Math.floor(n / 26); } return arr.reverse().join(''); }

String.fromCharCode(65 + offset)和 Python 的chr(ord('A') + offset)是一个意思,都是把整数偏移量转成字符。

3.4 为什么强烈推荐“先收集后逆序”

有人会觉得最后逆序多此一举,想直接在循环里把新字符插到字符串头部。这个做法在 Python 里可以写成s = chr(...) + s,在 JS 里可以写成arr.unshift(...),但性能都比先收集再逆序差。原因很简单:头插法每次都要移动已有字符串的内存或重新分配字符串对象,当转换的层数只有七八层时感受不到,但一旦字符集缩小、数字变大,或者你要处理几万个数,差距就出来了。

更重要的是,先收集后逆序的逻辑最贴近“先低位后高位”的数学推导,不容易出错。你在草稿纸上手算的时候也是先得 Q 再得 Y 再得 B,最后反向读,代码当然可以照抄这个思路。

4. 反向转换:字符串怎么变回数字

4.1 按权展开的算法

正向转换是把数字掰成一位一位的字母,反向转换就是把字母重新累加成数字。算法类比十进制转数字的“按权展开”:

def name_to_year(s: str) -> int: res = 0 for ch in s: res = res * 26 + (ord(ch) - ord('A') + 1) return res

用 BYQ 验证一下。B 对应 2,res=2。Y 对应 25,res=226+25=77。Q 对应 17,res=7726+17=2019。完美还原。

这里有个值得注意的点:反向转换不需要减一。为什么?因为正向转换时,每处理一位数字,都要减一才映射到字符;而反向转换是从字符直接还原位值,A 取 1,B 取 2,这是天然正确的。偏移只体现在转换过程中,不在字符编码本身。

4.2 用它做正向算法的验证

我写这类题有个习惯:写一个循环,从 1 到 10000 全部正向再反向,检查结果是否一致:

def check(): for i in range(1, 10000): if name_to_year(year_to_name(i)) != i: print('error:', i) return False return True

这个笨办法能一次性抓出偏移量错误、循环边界错误、逆序遗漏等几乎所有问题。项目开发中如果写了 Excel 列号转换工具,也可以用同样的思路做单元测试覆盖,1 到几千的边界值全跑一遍,比任何静态审查都可靠。

5. 我替大家踩过的五个坑:边界、顺序和缓冲区

坑 1:忘掉 n-- 导致 26 变成了 BA

这个错误最常见。如果直接tmp[len++] = 'A' + n % 26,那么 n=26 时:

  • 26 % 26 = 0,对应 A
  • 26 / 26 = 1
  • 1 % 26 = 1,对应 B

最后得到 BA,而正确答案是 Z。这个例子特别适合用来记忆知识点:只要映射规则里没有 0,就必须在取模之前减一。

坑 2:循环条件写错导致丢最高位

有人会把while (n > 0)写成while (n / 26 > 0)或while (n >= 26)。对 2019 来说,第一次循环 n=2019 进入,第二次 n=77 进入,第三次 n=2 时2 / 26 = 0退出,最高位 B 就丢了,输出变成 YQ。

正确写法只有一个:while (n > 0)。因为 n 经过多轮减一和整除后,最终一定会归零,此时所有位都处理完了。

坑 3:忘记逆序导致结果倒着

临时数组里收集到的是从低位到高位的字符,2019 得到的是 QYB。不逆序直接输出,结果就是反的。我见过有人把这个问题归到“字符串逆序输出”这个考点,确实,这道题如果你先在纸上画出低位在高位右侧,逆序就是必不可少的一步。

坑 4:C 语言缓冲区开太小

有些题解用char out[8],计算一下确实够:int 最大值 2147483647,转成 26 字符表示大概是 7 位,加上结尾符 8 字节。但实战里我强烈不建议这么压线。原因不是数学算错,而是 C 语言里字符数组越界后很难排查,可能污染栈上其他变量,看起来像玄学崩溃。开 32 字节或 64 字节不增加任何成本,却能把越界风险压到最低。

坑 5:JavaScript 的除法和 0 的边界

JavaScript 的n / 26得到浮点数,比如2019 / 26 = 77.653...。如果循环里写n = n / 26,n 永远不会变成整数,程序输出会越来越诡异,甚至死循环。用Math.floor(n / 26)是标配。

另外注意 n=0。题目一般从正整数开始,但如果你把这段代码封装成工具函数,调用方传了 0,while 循环直接跳过,返回空字符串。这在某些业务里可能不是你想要的。我的建议是在函数入口显式判断:

if n <= 0: raise ValueError('n must be positive')

明确报错比静默返回空字符串更能暴露问题,反向转换时也建议对空字符串做同样处理。

6. 从蓝桥杯到Excel列号:这道题的现实身份

6.1 Excel 列号就是同一个系统

很多人第一次遇到这个映射其实是在 Excel 里。Excel 的列号 A、B、……、Z、AA、AB、……、AZ、BA,和年号字串完全一致。比如 Excel 的第 26 列是 Z,第 27 列是 AA,第 52 列是 AZ,第 53 列是 BA。

如果你用 Python 做过 Excel 导入导出,大概率用过 openpyxl 里的get_column_letter和column_index_from_string。这两个函数背后就是这个算法。自己实现一遍之后,再看库源码会非常亲切。

6.2 短链接与邀请码的进制变体

把字符集从 26 个字母扩展到 36 个或 62 个,就是短链接和优惠码的常见实现。这里有一个关键分支:如果字符集包含 0,那么就是标准 n 进制;如果字符集刻意去掉了 0,比如为了避免用户把 0 和 O、1 和 I 混淆,很多邀请码字符集只有 1-9 加部分字母,那又是一个“年号字串”,只需要把代码里的 26 替换成字符集长度。

我记得有一次做活动邀请码生成,看到“去掉了 0/O/1/I 四个字符”的需求,第一反应就是这个题。字符集去零之后,正向转换依然要先减一取模,反向转换依然直接按权展开,完全一致。所以这道题不只是竞赛题目,它是一整类编码问题的通用解法。

6.3 这类题真正的考点

如果面试官考这道题,他想看到的不是你会背模板,而是三个能力:能不能从“A=1”这个映射意识到普通进制写法的偏差;能不能把 26 和偏移量两个变量分离,写出可扩展代码;能不能把边界情况处理干净。

我的建议是做题前先写出映射规则,比如“数字减一后取模,映射到 A 到 Z”,再翻译成代码。很多出错都发生在“凭感觉写”而不是“按规则写”。这和工作中做设计是一样的:方案先于代码,规则先于手感。

最后分享一个小技巧

后来我换了一种写法,用递归把逆序问题直接绕开:

def year_to_name_recursive(n: int) -> str: if n <= 0: return '' n -= 1 return year_to_name_recursive(n // 26) + chr(ord('A') + n % 26)

调用year_to_name_recursive(2019),递归先处理高位,返回时低位拼接在右侧,天然就是正确顺序。这个思路和“先收集后逆序”殊途同归,但代码更短,也可以用来和迭代写法互相验证。

我自己最初做这道题也走了弯路,后来把它当成 Excel 列号来理解后就再也没忘过。如果你后面遇到类似的、看起来像进制又不是标准进制的题,记住一句话:先找映射规则里有没有 0,再动手写代码。

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

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

立即咨询