ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

年号字串与Excel列号转换:深入理解无零的伪26进制算法

年号字串与Excel列号转换:深入理解无零的伪26进制算法 这题名字听着挺唬人P605 年号字串说白了就是把一个正整数变成一串字母1 对应 A2 对应 B26 对应 Z27 对应 AA2019 对应 BYQ。我第一次做这道题的时候第一反应就是“这不就是个 26 进制转换”结果按普通进制写法写完样例全挂对着输出发呆了半小时。后来才想明白它和真正的 26 进制差了关键一步这个系统里没有 0每一位都从 1 开始。这篇文章适合三类人准备算法竞赛的学生、要写 Excel 导出功能的老哥还有面试前突击进制转换的求职者。我会把这类题的数学本质、C/Python/JavaScript 实现以及我踩过的边界坑一次性讲透。看完你不仅能秒杀这道题还能顺手解决 Excel 列号转换这类实际问题。1. 题目到底在考什么一个没有零的“伪26进制”1.1 先看清题目不是简单进制转换题目本身很短把正整数映射成字母串映射规则是 A1B2……Z26AA27AB28依此类推。这个规则一旦写出来你应该马上能意识到它和普通 26 进制不一样——普通 26 进制里每一位的取值范围是 0 到 25而这里每一位的取值范围是 1 到 26。这个差异就是整道题的题眼。很多人在这一步栽了跟头因为他们把 1 到 26 的映射看成 0 到 25 的映射直接在循环里A n % 26结果 26 输出成了 BA 而不是 Z27 输出成了 BB 而不是 AA。不是代码写错了是数学模型没对上。1.2 用三个数字验证差异光说“不一样”不够直接看一组对照就能彻底明白。我按“本题正确结果”和“普通 26 进制 0A 的结果”列了一张表数字年号字串正确结果普通26进制0A的结果问题出在哪25YZ普通进制下 25 对应 Z但本题 25 对应 Y26ZBA普通进制下 26 需要进位但本题 26 还是单字符 Z27AABB普通进制下 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我们需要先确定它的最低位。如果 n26最低位显然是 Z没有进位。但26 % 26 0如果你用 0 对应 A 的做法最低位就变成了 A这显然是错的。解决办法是先把 n 减 1变成 25再取模25 % 26 2525 对应 Z正好。这里减 1 的本质是调整映射偏移量原来数字 1 到 26 对应字母 A 到 Z而取模结果的范围是 0 到 25。为了让两个区间重合把数字整体平移一格1 变 02 变 1……26 变 25。2.2 下一步的 n 应该怎么更新这个坑比第一步更隐蔽。很多人以为处理完一位后直接n / 26就行但正确做法是“先减一再整除”。用数学语言说设 m n - 1m 可以写成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举个例子n26如果直接商是 1意味着还要再输出一个高位字符但正确答案就是 Z不应该有第二位。用(26-1)//26 0商为 0循环结束输出只有一个 Z。2.3 2019 完整手算过程用代码逻辑手算 2019非常直观第一次迭代n2019先执行 n2018。2018 % 26 16对应 QA0所以 16 是第 17 个字母 Q。2018 // 26 77。第二次迭代n77先执行 n76。76 % 26 24对应 Y。76 // 26 2。第三次迭代n2先执行 n1。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 对应 2res2。Y 对应 25res2262577。Q 对应 17res7726172019。完美还原。这里有个值得注意的点反向转换不需要减一。为什么因为正向转换时每处理一位数字都要减一才映射到字符而反向转换是从字符直接还原位值A 取 1B 取 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那么 n26 时26 % 26 0对应 A26 / 26 11 % 26 1对应 B最后得到 BA而正确答案是 Z。这个例子特别适合用来记忆知识点只要映射规则里没有 0就必须在取模之前减一。坑 2循环条件写错导致丢最高位有人会把while (n 0)写成while (n / 26 0)或while (n 26)。对 2019 来说第一次循环 n2019 进入第二次 n77 进入第三次 n2 时2 / 26 0退出最高位 B 就丢了输出变成 YQ。正确写法只有一个while (n 0)。因为 n 经过多轮减一和整除后最终一定会归零此时所有位都处理完了。坑 3忘记逆序导致结果倒着临时数组里收集到的是从低位到高位的字符2019 得到的是 QYB。不逆序直接输出结果就是反的。我见过有人把这个问题归到“字符串逆序输出”这个考点确实这道题如果你先在纸上画出低位在高位右侧逆序就是必不可少的一步。坑 4C 语言缓冲区开太小有些题解用char out[8]计算一下确实够int 最大值 2147483647转成 26 字符表示大概是 7 位加上结尾符 8 字节。但实战里我强烈不建议这么压线。原因不是数学算错而是 C 语言里字符数组越界后很难排查可能污染栈上其他变量看起来像玄学崩溃。开 32 字节或 64 字节不增加任何成本却能把越界风险压到最低。坑 5JavaScript 的除法和 0 的边界JavaScript 的n / 26得到浮点数比如2019 / 26 77.653...。如果循环里写n n / 26n 永远不会变成整数程序输出会越来越诡异甚至死循环。用Math.floor(n / 26)是标配。另外注意 n0。题目一般从正整数开始但如果你把这段代码封装成工具函数调用方传了 0while 循环直接跳过返回空字符串。这在某些业务里可能不是你想要的。我的建议是在函数入口显式判断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 这类题真正的考点如果面试官考这道题他想看到的不是你会背模板而是三个能力能不能从“A1”这个映射意识到普通进制写法的偏差能不能把 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再动手写代码。
返回列表