很早之前碰到过这个题了,当时是学校的一个 ACM 比赛,很遗憾在赛场上时没有解出来。不过这个题还是蛮有意思的,一次偶然无聊的时候,又想起了这个题,当时莫名就有思路了,很快写出了相应的代码实现。不过思路才是最重要的。
题目
先了解一下这个题是什么意思。用过 Excel 都知道,它是由行和列来组织数据的,行号是从 1 开始的数字,列号则是大写字母,如图 1 所示。
那么众所周知,英文字母只有 26 个,当列数超过 26 列时怎么表示呢?
从上图可以看出,第 27 列是由 AA 表示的,也就是说,Z 的下一位,前面增加一个 A,而最后一位回到 A。为什么这么说呢?因为当位数不够时,只有增加位数才能表示更大的值,读者不妨先试想一下 AZ 的下一位为什么是 BA,而 ZZ 的下一位为什么是 AAA。在了解这个字母序列之后,读者应该明白了,所谓的 Excel 列号转数字不过就是几个大写字母列号转换为数字列号,即得到它是第几列,比如 AB 是 28。反之亦然,数字转 Excel 列号也不言而喻了。
一开始,我以为可以用进制来进行转化。不过,很明显有许多地方是不符合进制的特点的,因此关于进制这里不作介绍,主要讲讲 Excel 列号的特点。为了便于叙述,后文将 Excel 列号简称为字母列号。
思路分析
笔者的分析思路是将不同长度的字母列号分开分析。也就是分为以下几种情况:
(1)当只有一位时,A、B、C...Z,很明显,只有 26 个,序号就是从 1 到 26。我们可以称它是一维的或是一条“线”。
(2)当有两位时,AA、AB、AC...AZ,还有 BA、BB、BC...BZ,以及 CA...CZ,DA...DZ,一直到 ZA...ZZ。这时,从 AA 到 AZ 有 26 个,BA 到 BZ 有 26 个,而第一位的字母从 A 到 Z 有 26 个,也就是总共有 26*26 = 676 个,这可以称为二维或“面”。但是序号是从只有一位的情况开始的,也就是说这里需要加上之前的那一条“线”,即加上 26,那么从 AA 到 ZZ 的序号即是从 27(即1+26) 到 702(即 676+26)。
(3)当有三位时,AAA、AAB... ...ZAA... ... ZZA...ZZZ。即有 26*26*26 = 17576 个,即三维或“体”。同时序号需要加上前面的一维“线”和二维“面”,即从 703(1+26+676)开始的。
... ...
(n)当有 n 位时,则有 26^n 个,即 n 维。同时序号需要加上前面的所有维度的和,即前 n-1 维的和。
字母列号转数字
那么,字母列号转数字就非常明确了,根据字母位数就可以知道它处于第几维。计算出它当前维度所在的具体位置,再加上前面的维度总和即是它的序号了。举一个三位字母(BCF)的例子:
根据上文可知,先看“体”,从下往上数,第 1 层表示第一位为 A,第 2 层表示第一位为 B;再看“面”,第 1 行表示第二位为 A,第 3 行则表示第二位为 C;最后看“线”,第 1 列表示第三位为 A,第 6 列表示第三位为 F,综合可知图中 BCF 的具体位置。
首先,因为它有三位字母,所以它是处于第三维度中,那么前两个维度肯定是完整的,直接加上 702(26^1+26^2),剩下的就是计算它在第三维度中所处的具体位置了,因为它的第三维并不是完整的。
那么,图 6 清晰明了,在 BCF 之前,有 1(B - 1)个完整的“面”(1*26^2),还有 2(C - 1)个完整的“线”(2*26^1),最后剩下的就是 6(F)了。(这里把 A 看成 1,那么 B - 1 = 2 - 1 = 1,C - 1 = 3 - 1 = 2)
那么由此可以看出,BCF 的序号就是 (B - 1) * 26^2 + (C - 1) * 26^1 + F 再加上 26^2 加上 26^1,即 B * 26^2 + C * 26^1 + F。
这个式子还可以写成 B * 26^2 + C * 26^1 + F * 26^0。它所代表的意义就是该字母序号具有 B 个完整的“面”,C 个完整的“线”,和剩下的 F 了(读者也可以理解为剩下的 F 个“点”)。
这个式子恰好是 26 进制转 10 进制的公式,只不过它并不是真正意义上的 26 进制罢了(因为 26 进制的数字范围是 0~25,这里的范围却是 1~26)。那么对应的 Java 代码也很简单:
数字转字母列号
同样,也是先确定该序号所处的维度,那么,数字如何确定它所处的维度呢?再使用上面那个例子,序数为 1436,先算出第一维度的大小(26),与 1436 作比较,发现 1436 是大于 26 的,说明 1436 不处于第一维度(废话嘛),再算出第二维度(26^2),加上第一维度(26)得到 702,再与 1436 做比较,那么 1436 仍然大于 702,所以 1436 也不处于第二维度,继续算出第三维度(26^3),加上前面的维度和(702)得到 18278,这时 1436 是小于等于 18278 的,所以 1436 是处于第三维度中的。这才得出 1436 是处于第三维度的结论。(至于为什么是小于等于,那是因为当等于的时候,恰好是第三维度中最后的那个,即 ZZZ)
现在知道了 1436 所处的维度,也就相当于知道了字母序号的位数(为3),所以它至少是以 AAA 起步的。根据前面的公式可以知道各位字母所表示的具体的含义,所以接下来就是要找出它具有多少个完整的“面”,多少个完整的“线”,然后剩下的就是“点”了。假设算出它具有 2 个完整的“面”,那么第一位字母就是 B,具有 3 个完整的“线”,第二位字母就是 C,剩下 6 个“点”就代表第三位字母是 F。关于如何计算有多少个完整的“面”,只需整除以 26^2 就可以了;然后把“面”的那部分数量减去,再整除以 26^1 就是完整的“线”的数量。关于什么是整除,这边建议您出门左拐复习下小学数学......简单说就是正常做除法,除完以后只取整数,小数部分直接扔掉,也不要四舍五入,直接无视小数就是整除了,比如 7 整除以 3 的结果是 2,为啥呢,因为小数部分扔了啊。这个例子也说明了 7 只有 2 个完整的 3,不可能有第三个 3 了。
那么对应的 Java 代码如下:
到这里就结束了吗?当然不是,上面这个代码是有问题的,BCF 具有 2 个完整的“面”,3 个完整的“线”,和剩下的 6 个“点”,但若是恰好有 2 个完整的“面”,4 个完整的“线”呢?没有多余的任何“点”。这时通过推理可以知道,在 BCF 后面把空位补齐使余出的“点”凑成一条完整的“线”,就有了 4 个完整的“线”,对应的字母应该是 BCZ。看下程序运行结果:
结果显然是不正确的。分析下其中的原理,乍一看 BCZ 具有 B 个完整的“面”,C 个完整的“线”,和一条 Z 个“点”凑成的完整的“线”,也就是说其实它是把最后一条“线”分离出来作为 Z 个“点”了,那为什么不直接让 C 加 1 变成 D 呢?那样就是 BD 了,只有两位,维度为 2,也就是上图中的结果,很明显是错误的,因为维度为 3,所以它必须分出一条“线”放在第三位上作为 Z 个“点”。
所以需要增加一些判断,如果在做除法的时候,发现它被整除后没有余数,说明需要留出一条完整的“线”作为下个维度的“点”,这样想貌似没有问题,但应对 BZZ 时会出问题,BZZ 表示 B 个完整的“面”,Z 个“线”,Z 个“点”,问题出在 Z 个“线”也是一个“面”,Z 个“点”也是一个“线”,即其实是有 C 个“面”,A 个“线”,在对“面”做除法的时候余数是 A 个线,余数不为 0,结果也是不正确的。
整除取余失效了,那么怎么才能解出数字对应的字母呢?读者不妨换个角度想想,既然得出了维度为 3 的结论,那么就一定存在完整的二维和完整的一维,先把这两部分减去,剩下的就是第三维度中的具体位置了,可以对照图 5 和图 6 来理解。这样就可以方便地求整除和余数了。
当然,以上均为个人思路,不见得空间时间上能有多大的效率。很久之前的题了,今天整理一下。
代码
文末贴上代码,方便复制。对于特别大的值,需要注意整型的溢出问题,不然会出现无限循环。
/**
* Excel 列号转数字
*
* @param excelNum Excel 列号
* @return 数字
*/
public static int excelNum2Digit(String excelNum) {
char[] chs = excelNum.toCharArray();
int digit = 0;
/*
* B*26^2 + C*26^1 + F*26^0
* = ((0*26 + B)*26 + C)*26 + F
*/
for (char ch : chs) {
digit = digit * 26 + (ch - 'A' + 1);
}
return digit;
}
/**
* 数字转 Excel 列号
*
* @param digit 数字
* @return Excel 列号
*/
public static String digit2ExcelNum(int digit) {
/*
* 找到 digit 所处的维度 len, 它同时表示字母的位数
* power 表示 26^n, 这里 n 分别等于 1, 2, 3
* pre 表示 前 n 个维度的总和, 即 26^1 + 26^2 + 26^3
*/
int len = 0, power = 1, pre = 0;
for (; pre < digit; pre += power) {
power *= 26;
len++;
}
// 确定字母位数
char[] excelNum = new char[len];
/*
* pre 包含 digit 所处的维度
* pre - power 则是 digit 前面的维度总和
* digit 先减去前面维度和
*/
digit -= pre - power;
/*
* 比较难以理解的是这里为什么要自减 1
* 其实是相对 (digit / power + 'A') 这句代码来的
* 本应该是 (digit / power + 'A' - 1),
* digit / power 的结果是完整的维度个数, 它加上 'A' - 1 后需要再加一
* 当最后剩下的 6 个加上 'A' - 1 是应当的, 不需要做修改
* 而当 (digit / power + 'A') 中没有减 1 后,
* digit / power 的结果不需要再加一了
* 相对于 digit / power 的结果, 最后剩下的 6 需要减 1
*/
digit--;
for (int i = 0; i < len; i++) {
power /= 26;
excelNum[i] = (char) (digit / power + 'A');
digit %= power;
}
return String.valueOf(excelNum);
}