本文首发于 欧雷流。由于我会时不时对文章进行补充、修正和润色,为了保证所看到的是最新版本,请阅读 原文。
昨晚,一个小伙伴甩给我一道算法题,由于当时正躺在床上跟老婆一起看《权力的游戏 第七季》,就没去想,今早起床后打开电脑第二件事就是去做一下那道题。
题目
这道题是如此描述的——
完成一个转换函数,将数字转成对应的大写字母,满足下面的对应关系:
1 => A
2 => B
3 => C
...
26 => Z
27 => AA
28 => AB
29 => AC
...
52 => AZ
53 => BA
54 => BB
...
题解
从描述中初步分析可以知道,需要做的事情大体为两步:将给定数字按照一定规则转换为大写字母;拼接转换后的大写字母为一个完整的字符串。
从「1」开始最初的连续 26 个整数与大写字母之间的对应关系很明确,让人觉得这是一道普通的十进制转二十六进制或二十七进制的进制转换题。然而,进一步观察就会发现,这里缺少了对于「0」的相关描述,所以在进位时肯定要做点特殊处理。
将数字转为大写字母,最直觉的方式,就是使用字符串数组:
const LETTER_ARR = [
'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z'
];
function getLetterByNum(num) {
return LETTER_ARR[num - 1];
}
在 JS 中通过 String.prototype.charCodeAt()
方法能够获取到字符所对应的 Unicode 的编码单元,并且 26 个大写字母是连续的,所以可以改造为:
function getLetterByNum(num) {
return String.fromCharCode('A'.charCodeAt(0) - 1 + num);
}
在做进制转换时,需要在源和目标之间不断地进行求商和余数。结合上面的转换代码,用循环实现为:
function convert(num) {
const result = [];
let divided;
do {
const dividedNum = divided ? divided[0] : num;
// 将数字处理成 `[商, 余数]` 的格式
divided = isNaN(dividedNum) || dividedNum < 1 ? [-1, -1] : [Math.floor(dividedNum / 26), dividedNum % 26];
// 进位的特殊处理
if (divided[1] === 0) {
divided = [divided[0] - 1, 26];
}
if (divided[0] !== -1) {
result.unshift(String.fromCharCode('A'.charCodeAt(0) - 1 + divided[1]));
}
} while (divided[0] / 26 > 0);
return result.join('');
}
其中,Math.floor(dividedNum / 26)
可以用 ~~(dividedNum / 26)
替代,用递归实现为:
function convert(num) {
return num <= 26 ? String.fromCharCode('A'.charCodeAt(0) - 1 + num) : convert(~~((num - 1) / 26)) + convert(num % 26 || 26);
}
另外,LeetCode 上有同款算法题:https://leetcode-cn.com/problems/excel-sheet-column-title/。
欢迎关注微信公众号【Coding as Hobby】(微信中搜「coding-as-hobby」)以及时阅读最新的技术文章~ ;-)