我第一次在在线题库的基础篇里看到这道题时,题面里“Attitude 对应数字之和为 100”这句话特别扎眼。很多人把它当成一道靠“查字典”硬算的送分题,但真正动手实现后会发现,里面藏着字符串处理和字符映射的核心套路:把英文 26 个字母 A~Z 或 a~z 对应到整数 1~26,然后把给定字符串的所有字符值累加,非英文字母一律按 0 处理。掌握这道题,你不只是会算“Attitude=100”,还能顺手打通后续的Excel列号转换、罗马数字累加、词频权重分析这类相似问题。我按自己的实战习惯,把这道经典题完整拆解一遍。
1. 题干拆解:Attitude等于100背后的“字母权值”规则
1.1 这道题到底在考什么
从表面看,只需要做一次逐字符遍历:拿一个循环扫过字符串,把每个字母转成它在字母表中的位置,然后累加。但题目故意用 Attitude 这个单词作为诱饵,实际上考察的是三层能力:
- 字符串遍历:无论用 for、while 还是 foreach,你都得能拿到每个字符;
- 字符与整数的转换:知道字符编码的连续性,才能用减法代替查表;
- 分支判断与边界处理:大小写要分开处理,非字母字符要返回 0,不能跳过,也不能终止循环。
很多初学者第一次写这题,会试图建一个哈希表,把 26 个大写字母和 26 个小写字母一一列出来,再查表累加。这个思路没有错,也能跑对,但它掩盖了题目更想让你掌握的规律:因为 ASCII 码连续,‘A’到‘Z’的码点 65~90,‘a’到‘z’的码点 97~122,所以完全可以直接用偏移量计算,根本不需要哈希表。
1.2 大写、小写与非字母的三类处理分支
在任意一个字符上,我们需要判断它落进以下三种情况中的哪一种:
- 大写字母:ASCII 码 65~90,对应值
ch - 'A' + 1; - 小写字母:ASCII 码 97~122,对应值
ch - 'a' + 1; - 其他字符:数字、空格、标点、中文、emoji、控制字符等,对应值 0。
分支的先后顺序没有强制要求,只要保证每个字符只会被计算一次。C/C++ 里可以直接用ch >= 'A' && ch <= 'Z'这类区间判断,因为字符会被当作整数参与比较。需要特别注意的是字符串结尾的'\0',它的 ASCII 码是 0,不在任何一个字母区间内,所以天然对应 0,不需要额外处理。
这个规则里最容易忽视的是“非英文字母对应 0”这个措辞。它与“跳过非字母”有细微差别:跳过意味着遇到空格就不参与求和,对应 0 也意味着不参与求和,表面上结果一致,但如果你的循环因为看到空格而提前 break,那就会漏掉空格后面的所有字母,这才是真正的坑。后面我会专门讲这个踩坑案例。
1.3 手算一遍:为什么 Attitude 恰好是 100
先手动算一遍题面给的示例,这既能帮我们确认映射规则,也是一个天然测试用例。拿Attitude逐字母拆开:
| 字符 | A | t | t | i | t | u | d | e |
|---|---|---|---|---|---|---|---|---|
| 映射值 | 1 | 20 | 20 | 9 | 20 | 21 | 4 | 5 |
累加:1 + 20 + 20 + 9 + 20 + 21 + 4 + 5 = 100。
有意思的是,全小写attitude的和也是 100,全大写ATTITUDE还是 100。因为大小写字母的映射值完全一致,只是匹配分支不同。出题人偏爱 Attitude,不只是因为“Attitude is everything”这句英文鸡汤,更因为这个单词天然演示了规则里“大小写都对应同一数字”的设定。
2. ASCII码的连续性是破题关键:从字符到数字的换算
2.1 英文字母在字符编码中的排布规律
所有常见编程语言里,字符都不是“图形”,而是整数。ASCII 表中大写字母从 65 到 90 连续排布,小写字母从 97 到 122 连续排布。正是这个连续性,让“A 对应 1,B 对应 2……”变成了一条数学规律,而不是一份需要背下来的对照表。
在具体语言里,获取字符码点的方式略有不同:C/C++ 的char本身就是整数,可以直接拿来做减法;Python 使用ord()函数;Java 的char是 16 位 Unicode 码元,可以直接参与算术;JavaScript 用charCodeAt();Go 的 rune 是 Unicode 码点,同样可以参与算术。无论哪种语言,核心思想都是一样的:取字符码点,减去对应区间的起始码点,再加回 1。
2.2 偏移公式的推导思路:距离加一
为什么公式是ch - 'A' + 1,而不是ch - 'A'?因为题面规定 A 对应 1,不是 0。ch - 'A'得到的是“当前字符离 A 有多远”,B 离 A 是 1,但 B 在字母表里的位置是 2,所以还要加 1。
用小写字母举例:'t'的 ASCII 码是 116,小写区间起点'a'是 97,距离是 19,再加 1 得到 20,正好是字母表第 20 个字母。我习惯在说课时用“距离再补一”来解释这个公式,比死记硬背 64、96 这类魔法数字容易得多。
2.3 边界条件:空字符串与任意非英文字符
空字符串的求和结果是 0。你不需要在函数开头单独判断空串,因为循环一次都不会执行,累加变量保持初始值 0,直接返回即可。
非英文字符范围广得超乎想象:空格、回车、数字、标点、中文、日文假名、emoji,统统都是 0。在多字节编码里,比如 UTF-8 中文由多个字节组成,如果按字节遍历,每个字节本身也不会落在A~Z或a~z区间,所以结果依然是 0。但如果你所用的语言字符串是 Unicode 序列(比如 Python 的 str、Java 的 String),按字符遍历更清晰,判断也更容易。
3. 五种主流语言实现:从Python到Go的完整代码
3.1 Python:最简洁但小心isalpha的坑
Python 版本可以写得非常干净:
def letter_sum(s: str) -> int: total = 0 for ch in s: if 'A' <= ch <= 'Z': total += ord(ch) - ord('A') + 1 elif 'a' <= ch <= 'z': total += ord(ch) - ord('a') + 1 return total这里有句恨重要的劝告:不要用isalpha()。在 Python 3 中,'中'.isalpha()返回 True,'α'.isalpha()也返回 True,但题目要的是“英文字母”,不是“Unicode 字母”。如果盲目用isalpha(),一句中文会被当作 26 个字母以外的东西?不,中文会被当作真,然后你拿它的码点减去ord('a')得到一个毫无意义的负数或大数,最终结果完全错乱。所以必须限定 ASCII 区间。
3.2 C/C++:用ASCII区间判断最可靠
C 语言实现:
int letter_sum(const char *s) { int total = 0; while (*s) { if (*s >= 'A' && *s <= 'Z') total += *s - 'A' + 1; else if (*s >= 'a' && *s <= 'z') total += *s - 'a' + 1; ++s; } return total; }用while遍历指针直到'\0'结束。这段代码默认字符串是 ASCII 编码,处理普通英文输入没有任何问题。严格一点的做法,是把*s先转成unsigned char再比较,避免某些平台上char是有符号类型时,扩展 ASCII 字节被符号扩展为负数,虽然这些负数也不会进字母区间,但转一下能让行为更可控。
3.3 Java:Character.isLetter并非总是你想要的
Java 实现:
public static int letterSum(String s) { int total = 0; for (char c : s.toCharArray()) { if (c >= 'A' && c <= 'Z') total += c - 'A' + 1; else if (c >= 'a' && c <= 'z') total += c - 'a' + 1; } return total; }很多 Java 新手会写Character.isLetter(c),同样掉进 Unicode 字母的坑。Character.isLetter对中文字符返回 true,所以用它判断会导致非英文字母被计入求和。这题的正确做法是老老实实做两次区间比较,或者用正则表达式[a-zA-Z]先过滤再计算。
3.4 JavaScript:正则表达式与大写转换的取舍
JavaScript 实现:
function letterSum(s) { let total = 0; for (const ch of s) { if (/[a-zA-Z]/.test(ch)) { const code = ch.charCodeAt(0); const base = ch >= 'a' ? 96 : 64; total += code - base; } } return total; }这里我用了 96 和 64 这两个魔法数字,其实就是'a'的码点减 1、'A'的码点减 1。为了避免魔法数字,更推荐写成:
const code = ch.charCodeAt(0); const base = ch >= 'a' ? 'a'.charCodeAt(0) - 1 : 'A'.charCodeAt(0) - 1; total += code - base;还有种常见做法是先把字符转成大写,然后统一用大写区间计算,因为大小写值相同。但那样要先赋值临时变量,多一次字符串操作,性能略低,代码也不见得更好读。
3.5 Go:按rune遍历能自然避开多字节陷阱
Go 实现:
func letterSum(s string) int { total := 0 for _, r := range s { if r >= 'A' && r <= 'Z' { total += int(r - 'A' + 1) } else if r >= 'a' && r <= 'z' { total += int(r - 'a' + 1) } } return total }Go 的for range配合字符串,会按 Unicode 码点遍历,中文字符会作为一个 rune 出现,不会被拆成多个字节。rune 是 int32 类型,参与累加前要转成 int。如果你用for i := 0; i < len(s); i++按字节遍历,在 UTF-8 字符串上会得到字节序列,虽然本题结果通常也正确,但一旦你误以为某个中文字节等于某个字母,就会出问题。直接用for range是更稳的选择。
3.6 各语言代码对比
| 语言 | 判断方式 | 需要注意的坑 |
|---|---|---|
| Python | 'A' <= ch <= 'Z' | isalpha会误判中文 |
| C/C++ | 字符区间比较 | 指针遍历时别写错++s |
| Java | 字符区间比较 | Character.isLetter会误判中文 |
| JavaScript | 正则或区间判断 | 魔法数字建议用charCodeAt计算 |
| Go | rune 区间比较 | 按字节遍历会踩 UTF-8 的坑 |
4. 测试用例设计与复杂度分析
4.1 用Attitude作为天然验算样例
代码写完以后,第一个测试当然是用题面给出的Attitude,期望值是 100。我建议同时测试三种情况:Attitude(首字母大写)、attitude(全小写)、ATTITUDE(全大写)。因为大小写映射值相同,三个输入都应该返回 100。这三组用例不仅能验证大写分支,还能验证小写分支,一次顶三次。
如果你用形参传入的是字符串而非数组,记得也要测空字符串,期望 0。空串是最容易遗漏的边界,很多在线评测会专门放一个空串用例,用来揪出那些试图读s[0]的函数。
4.2 边界用例:空格、数字、中文与emoji
构造一组更有代表性的用例:
| 输入 | 期望输出 | 说明 |
|---|---|---|
"" | 0 | 空字符串 |
"12345" | 0 | 纯数字 |
"a1b2" | 3 | 数字按 0,a=1,b=2 |
"Hello World!" | 124 | 空格与感叹号都是 0 |
"你好" | 0 | 中文字符非英文字母 |
"αβγ" | 0 | 希腊字母也非英文字母 |
"😀abc" | 6 | emoji 是 0,a=1,b=2,c=3 |
这里"Hello World!"的 124 从哪来:H=8,e=5,l=12,l=12,o=15,空格 0,W=23,o=15,r=18,l=12,d=4,! 0,合计 124。我在本地跑的时候,特意把这些用例写成了一个表驱动测试,方便一次性看结果。
4.3 复杂度结论:所有语言都是O(n)时间与O(1)空间
算法每个字符访问一次,做常数次比较和一两次加减,时间复杂度是 O(n),其中 n 是输入字符串长度。辅助变量只有几个整数,空间复杂度 O(1)。这个复杂度已经是最优的,因为无论如何你都得把每个字符读一遍才能知道它是什么。
在竞赛或面试中,当题目数据范围很大时,O(n) 就是标准答案。如果字符串长度超过 1 亿,累加结果可能接近 26 亿,32 位 int 会溢出,这时需要把累加器声明为 64 位整数,或者按题目要求取模。原题通常不会给那么大的数据,但养成用long long或int64的习惯没坏处。
4.4 随机模糊测试:用几百条随机串验证逻辑
除了手写边界用例,我还会用一段小脚本做随机模糊测试:生成包含大小写字母、数字、空格、中文的随机字符串,先按“预期公式”逐个字符累加算出期望结果,再调用自己实现的函数对比。几百条随机用例跑下来,很多自以为写对的边角逻辑会立刻现出原形。这个习惯同样适用于其他字符串算法题,比单纯靠几个手工用例踏实得多。
5. 踩坑实录:我在这道题上犯过的几个错误
5.1 只写了大写分支,小写字母全部阵亡
我第一次写 C 语言版本时,只判断了*s >= 'A' && *s <= 'Z',测试Attitude直接返回 75。原因很简单:首字母大写 A 加 1,后面小写的 t、t、i、t、u、d、e 全被忽略,而它们加起来应该是 99。这个错误非常隐蔽,因为你测试"ABC"全大写时会通过,一测混合大小写就露馅。排查办法是打印每个字符的中间累加值,或者把大小写混在一个字符串里作为固定测试用例。
5.2 把空格当成分隔符,结果提前跳出循环
另一个经典错误出现在处理“Hello World”这类带空格的字符串时。有人习惯性地认为“看到空格就说明单词结束了”,于是写成了if (*s == ' ') break;。结果字符串后半段World完全没被计算。请记住,这道题里的空格、数字、标点只代表“对应值 0”,它们不承担终止符角色。唯一合法的终止位置是字符串末尾,不是任意空白字符。
5.3 用Unicode字母判断引入非英文字母
Python 的isalpha()和 Java 的Character.isLetter(),是我见过翻车率最高的两个写法。用它们判断时,中文、希腊字母、甚至一些特殊符号都会返回 true,然后被带进偏移公式,产生一个巨大的无意义数字。如果你只是刷题,测试数据里没有中文,靠那些标准用例也许能侥幸通过;但一旦你的函数被其他模块调用,输入里混进一句中文,结果就莫名漂移。为了避免这个隐患,最佳实践就是使用 ASCII 区间比较,不要依赖语言自带的“字母”语义。
5.4 过度压缩代码导致的可读性问题
我还见过一些追求“一行流”的写法,比如在 C 语言里把大小写判断和累加全部挤进一个表达式,用短路运算符、位移甚至位操作凑成一行。这种代码拿到在线评测平台上确实能通过,但它几乎无法维护。以后你自己回头读,或者同事接手,都会非常痛苦。工程代码是写给人看的,其次才是给机器执行。这道题逻辑简单,更应该用清晰的分支表达。
6. 从简单求和到真实场景:这道题的扩展与变体
6.1 单词权值排序与“名字打分”小工具
搞清楚字母求和后,你可以立刻做一个单词权值计算器:输入一组昵称或单词,输出按权值排序的列表。我在实际工作中写过一个简单的“昵称趣味打分”工具,正是把letter_sum封装后对每个昵称求值,再按分数从高到低排序。这样做成本很低,却能给运营活动增加一个互动点。
实现时有个小技巧:如果单词数量很大,先一次性把每个单词的权值算好存进数组,再做排序,不要每次比较时重新算一遍。排序算法调用比较函数的次数约 O(m log m),如果比较时现算权值,整体复杂度会高出不少。
6.2 用预计算表加速批量查询
如果同一个长字符串会被反复查询“某个子串的字母权值之和”,你可以用前缀和优化。定义prefix[i]表示字符串前 i 个字符的权值和,则prefix[i] = prefix[i-1] + letterValue(s[i-1])。任意子串[l, r)的求和都能用prefix[r] - prefix[l]在 O(1) 时间内算出,而预处理只要 O(n)。
前缀和是区间查询问题里的基础武器。虽然这道题本身是单串整体求和,但稍加变化就变成“多次区间查询”,这时候再用逐次遍历就会浪费大量时间。我最初学这题时只觉得它简单,后来发现它其实是理解前缀和的绝佳起点。
6.3 相似题目对照:Excel列号与罗马数字
有一道经典题和它非常像:电子表格的列标转数字,A 是 1,B 是 2,Z 是 26,AA 是 27。但它的规则不是简单求和,而是类似 26 进制的“按位加权求和”,'AB'表示 1×26 + 2。所以它和本题的区别在于“每个字符是独立贡献,还是位置权重叠加”。掌握本题的独立贡献模型,再遇到列号转换时,你会更容易看出“这里多了乘 26 的一步”。
罗马数字转整数也是同一族问题:每个字符映射到一个数值,再根据前后大小关系决定是加还是减。比如 IV 表示 5 减 1。它比本题多了规则约束,但第一步仍然是“字符到数值的映射”。建议把这几个题放在一起刷,你能明显感觉到自己的字符处理基本功扎实了一层。
6.4 把字母映射思想迁移到简单数据指纹
更远处讲,这种“按字符权值累加”的思路可以用于最简单的数据指纹:把一个字符串压成一个整数,用来快速比较两段文本是否可能相同。当然,严谨的哈希算法要处理碰撞、分布均匀性等一堆问题,但初学者可以先从这种朴素的加权和开始理解“字符串降维成整数”是怎么回事。
我后来写一个小型的昵称查重工具时,就是先用这种加权和作为粗筛条件,过滤掉权值明显不同的昵称,再对候选做精确比较。这样做虽然不如布隆过滤器那么专业,但对数据量不大的内部工具来说,简单、直观、够用。所有的复杂应用,追根溯源都离不开今天这个最朴素的字母映射逻辑。
从第一次在这道题上栽跟头,到现在把这个思路复用到各种工具里,我最大的体会是:基础题不要急着求快,先手动算一遍示例,再写循环,最后把空格、数字、中文这类边界用例全部喂一遍。养成这个顺序之后,再去刷更难的问题,你会发现自己排查 bug 的路径清晰了很多。