☰
200分找数字题:四种语言实现与边界条件全面复盘
2026/10/21 19:37:35 网站建设 项目流程

看到标题里赫然写着(200分),很多人下意识会觉得:找数字?这不就是把字符串里的数字抠出来吗,有什么好说的。但我要说一个自己栽过跟头的结论:这类题恰恰是机考和在线评测里最能拉开差距的题目之一。它同时要求用 Java、JavaScript、Python、C 四种语言给出解法,说明出题人不是想考某个冷门 API,而是要用同一道题检验不同语言背景的程序员,对字符串遍历、状态标记、类型边界这些基本功掌握得够不够扎实。这篇文章就把找数字这道题从题面分析、核心思路、四语言实现到边界条件完整复盘一遍,给准备机考或刷题的新手一份可以直接对照的参考答案。

1. 拿到题先别急着写:200分题目的真实考点拆解

1.1 我这次遇到的题面版本

先交代题面。某个在线评测平台上给出的版本大致是:输入一个字符串,里面混着大小写英文字母、数字字符以及其他可见符号,要求把所有由连续数字字符构成的子串当作一个整数提取出来,再输出这些整数的总和。

举几个最简单的例子:

  • 输入 "a1b23c456",数字子串是 1、23、456,输出 480。
  • 输入 "abc",没有数字,输出 0。
  • 输入 "123",整体就是一个数字,输出 123。
  • 输入 "12a34b56",输出 12 + 34 + 56 = 102。

题面就这么几句话,看起来没有任何难度。但正是这种"看起来简单"的题,才是评测系统里放倒一片人的经典款。

1.2 看起来很简单,为什么值200分

分值是200分,说明它不是一个纯送分题。早期测试点可能只给"a1b2c3"这种规规矩矩的输入,循环里判断一下字符是不是数字,顺手累加就行。但后面一定藏着边界测试组:字符串以数字结尾、数字超长、连续多个分隔符、前导零、空字符串等等。

这些边界点单独拎出来都不难,难的是你在写第一版代码时,有没有把"所有情况"都当成一等公民对待。很多人写题的习惯是先把主流程跑通,然后测两个样例就交了,结果一提交,隐藏测试点里挂掉一片。

做过在线评测的人都知道,这种题往往不是"能不能做出来"的问题,而是"会不会在不该丢分的地方丢分"的问题。能拿满分的人,未必比你能多写多少行代码,差别就在边界意识和类型意识。

1.3 这个考点在真实代码里长什么样

别觉得这是纯应试技巧。找数字这套逻辑,在真实开发里到处都是。

比如解析日志文件时,要从一行混杂信息的文本里提取请求耗时并求平均值;比如爬虫清洗数据时,要把商品描述里的价格数字全部捞出来;再比如从用户输入的半结构化文本中抽取订单号、手机号、验证码——底层都是同一个动作:按字符扫描,判断是不是数字,把连续数字拼起来,遇到非数字就结算一次。

这四种语言同时被点名也很有现实味道。很多团队里,后端服务是 Java 写的,脚本工具用 Python,前端页面跑着 JavaScript,底层嵌入式或性能敏感模块还是 C。同一道题,你至少要能在这四种语言里顺畅表达同一种思路,才谈得上在不同技术栈之间切换工作。

2. 核心思路:把连续数字“攒”起来的单次扫描状态机

2.1 三个变量一个循环:状态机的核心

这道题的精髓不是"找数字"三个字,而是"连续"两个字。你不能看到一个数字字符就立刻把它加到总和里,因为你不知道后面还会不会接着来一个数字。

比如输入 "a12b",当你扫到字符 '1' 时,你只知道它属于一个数字,但不确定这个数字是 1 还是 12。要等扫到 '2' 才知道数字还在继续。所以正确的做法是:先"攒"着,等到碰见非数字字符,说明当前这个数字结束了,再一次性结算。

攒的过程需要三个变量:

  • current:正在拼接的当前数字。每遇到一个数字,就把旧的 current 乘以 10,再加上新的数字位。比如已经攒了 12,又遇到 '3',current 就变成 123。
  • total:已经结算完成的数字的总和。
  • in_num:一个布尔标记,表示当前是否正处于一个数字子串内部。

in_num看起来多余,其实很关键。它解决的是"连续多个非数字字符"的情况。比如 "a,,1",两个逗号中间没有数字,如果没有这个标记,你可能会在第二个逗号处再次把零散的 current 加进 total,导致重复计算。有了 in_num,只有"从数字状态切换到非数字状态"的那一刻才做结算。

整个算法的时间复杂度是 O(n),只需要遍历字符串一次;额外空间是 O(1),不管字符串多长,只占用固定几个变量。

2.2 手动走一遍:把“a12b3”算给小白看

光说变量太抽象,拿 "a12b3" 手动跑一遍,每一步都列出来:

  1. 读 'a':非数字,in_num 为 false,什么都不做。
  2. 读 '1':是数字,in_num 变成 true,current = 0 * 10 + 1 = 1。
  3. 读 '2':是数字,current = 1 * 10 + 2 = 12。
  4. 读 'b':非数字,且当前 in_num 是 true,说明数字 '12' 结束,执行 total += 12,然后 current 清零,in_num 置 false。
  5. 读 '3':是数字,in_num 变 true,current = 3。
  6. 字符串遍历结束。此时 current 里还装着 3,而循环已经走完了,必须再做一次 total += 3。

最后 total = 15,结果正确。

你看,整个逻辑就是一台微型状态机:在"数字状态"和"非数字状态"之间切换,只有从数字状态跳出去的那一刻才结算。

2.3 末尾结算:最容易漏掉的一行

上面第 6 步是重灾区。很多人写完循环就收工了,完全没意识到,如果字符串最后一个字符是数字,那么最后一个数字子串在循环体内永远不会触发结算——因为循环结束后没有"非数字字符"来逼你结算。

我印象很深的一次翻车就是输在 "abc123" 这种用例上。循环跑到最后,current 里存着 123,所有测试示例都跟"以数字结尾"无关,我就没写循环外的结算,结果一提交,针对这个边界的测试点全部阵亡。

规则很简单:循环结束后,再检查一次 in_num 是否为 true,是的话就把 current 加进 total。这一个 if 语句,就是满分和部分分的分界线之一。

2.4 为什么我不用正则和 split

我知道有人会说:Python 里re.findall(r'\d+', s)一行就搞定了,Java 里也有正则,干嘛费劲手写循环?

原因有三。

第一,C 语言没有内置正则,POSIX 正则用起来麻烦,而且这种小任务不值得引入额外复杂度。既然要写一套能在四种语言里通用、能互相迁移的思路,手写扫描是交集最大的方案。

第二,split 按非数字分割的思路有坑。Python 的re.split(r'\D+', s)会产生空字符串,你得额外过滤;Java 的split对结尾空串有特殊处理规则,不同语言行为还不一样。你是来解题的,不是来跟各语言的 split 语义作斗争的。

第三,正则和 split 通常会把匹配结果先存成字符串数组或列表,再转成整数求和,时间和内存上多出一些开销。手写扫描全程在原字符串上走一遍,不需要任何额外存储,在大字符串上性能更稳。评测系统里有时候有超长字符串用例,O(n) 和 O(n) 虽然复杂度一样,但常数项和分配开销差很多。

手写扫描在四种语言里语义完全一致,这才是最省心的选择。

3. 四种语言现场实现:同一套逻辑的不同表达

3.1 Python:写起来最快,int 又不会溢出

Python 版本是最轻松的,语法简洁,字符可以直接和 '0'、'9' 比较:

def sum_of_numbers(s: str) -> int: total = 0 current = 0 in_num = False for ch in s: if '0' <= ch <= '9': current = current * 10 + (ord(ch) - ord('0')) in_num = True else: if in_num: total += current current = 0 in_num = False if in_num: total += current return total

两个细节值得说。

一是判断数字用'0' <= ch <= '9',Python 的字符串重载了比较运算符,对单个 ASCII 字符来说,这种区间比较是完全可靠的。

二是把数字字符转成数值时,我用ord(ch) - ord('0')而不是int(ch)。int(ch)当然也能跑,但它内部是一个函数调用、要构造新对象,在每层循环里对每个数字字符都执行一次,积少成多。ord相减是纯算术运算,语义也更直白:字符 '7' 的编码减去字符 '0' 的编码,结果就是 7。

还有一点是 Python 特有的运气加成:int 是无上限的大整数,不管测试数据里出现多长的数字,Python 都不会溢出。但这不意味着你可以对其他语言也这么放心。

3.2 Java:字符遍历与 long 的选择

Java 版本的核心差别在于字符串不能像 Python 那样直接 for-in 出字符,要用charAt(i)按下标访问:

public static long sumOfNumbers(String s) { long total = 0; long current = 0; boolean inNum = false; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c >= '0' && c <= '9') { current = current * 10 + (c - '0'); inNum = true; } else { if (inNum) { total += current; current = 0; inNum = false; } } } if (inNum) { total += current; } return total; }

这里有一个很多 Java 新手会犯的错误:把 total 和 current 声明成 int。Java 的 int 是 32 位的,最大值只有 21 亿多一点。如果测试数据里出现一个较长的数字,或者多个大数字累加,int 直接溢出,变成负数,评测结果当然是错的。

我建议一律用 long。64 位的 long 能容纳约 9.2 乘以 10 的 18 次方,覆盖绝大多数题目的数据范围。如果题目明确说数字可能有几十位,那就要上BigInteger了,但那种题通常会把分值标得更高,不会是这道 200 分的定位。

Java 的 char 是无符号 16 位类型,直接拿字符和字面量'0'、'9'比较完全安全,不用像 C 那样担心符号位问题。

3.3 JavaScript:charCodeAt 与安全整数范围

JavaScript 版本的写法比较考验基本功,因为s[i]拿到的是一个长度为 1 的字符串,而不是字符类型。你能拿它和'0'、'9'做字符串比较,在 ASCII 数字范围内结果是可靠的,但我更推荐用charCodeAt拿字符编码来比较,意图更明确:

function sumOfNumbers(s) { let total = 0; let current = 0; let inNum = false; for (let i = 0; i < s.length; i++) { const code = s.charCodeAt(i); if (code >= 48 && code <= 57) { current = current * 10 + (code - 48); inNum = true; } else { if (inNum) { total += current; current = 0; inNum = false; } } } if (inNum) { total += current; } return total; }

字符 '0' 的 Unicode 编码是 48,'9' 是 57,所以用编码区间判断数字,语义上和c >= '0' && c <= '9'一模一样,还不受字符串比较规则干扰。

JavaScript 里有个隐蔽的坑:Number 类型是双精度浮点数,它能精确表示的整数范围只有 2 的 53 次方,也就是约 9000 万亿。如果题目数据超过了这个范围,普通运算符算出来的结果会丢精度。真遇到这种情况,得把 current 和 total 换成 BigInt,代码里所有数字字面量都要加后缀,比别的语言多一层功夫。好在常规评测数据很少为难 JS 到这个程度,知道有这回事就行。

另外强调一点:JavaScript 里单引号、双引号、反引号都能当字符用,但代码里数字判断最好统一用单字符字面量或编码,别把 '0' 写成 "0"。字符串比较虽然也能过,但会让人觉得你对类型不够敏感。

3.4 C:指针遍历、isdigit 的坑和 long long

C 版本最贴近底层,也最需要操心类型:

#include <stdio.h> #include <ctype.h> #include <limits.h> long long sum_of_numbers(const char *s) { long long total = 0; long long current = 0; int in_num = 0; for (const char *p = s; *p != '\0'; p++) { if (isdigit((unsigned char)*p)) { current = current * 10 + (*p - '0'); in_num = 1; } else { if (in_num) { total += current; current = 0; in_num = 0; } } } if (in_num) { total += current; } return total; }

这里藏着一个特别阴的坑:isdigit函数的形参是 int,但标准明确规定,传入的值必须是 unsigned char 或者 EOF,否则是未定义行为。什么意思呢?如果你的输入字符串里含有中文或其他非 ASCII 字符,在常见的 UTF-8 编码下,一个汉字占用多个字节,每个字节的最高位都是 1,转成 char 就是负数。直接把负数 char 传给isdigit,行为不可预测,可能在你的机器上没事,在评测服务器上就崩了。

解决办法就是强制转换:isdigit((unsigned char)*p)。这一行看着不起眼,不知道能救多少人。

类型选择上,我建议 total 和 current 一律用long long,至少 64 位。在 32 位系统上,int 只有 4 个字节,随便来一个十位数的数字就溢出了。如果还担心超过 long long 的范围,可以在累加前做一次判断:

if (current > (LLONG_MAX - (*p - '0')) / 10) { // 溢出,按题目要求处理,比如钳制在某个上限 }

这种防御式写法在考试里未必用得着,但能体现出你对整数边界的理解。

3.5 四种写法的横向对比

放一张对照表,方便你快速回忆:

语言逐字符访问方式数字判断方式建议类型溢出风险
Pythonfor ch in s'0' <= ch <= '9'int 本身不限长无
Javas.charAt(i)c >= '0' && c <= '9'long超长数字仍溢出
JavaScripts.charCodeAt(i)code >= 48 && code <= 57Number 默认,极限用 BigInt超过 2^53 丢精度
C指针 *pisdigit((unsigned char)*p)long longint 必溢出,long long 也可能溢出

核心逻辑完全一致,差别只在语言层面的表示方式。我把这张表默记在心里,换语言写的时候,只需要替换"怎么拿字符""怎么判断数字""用什么类型"三处,其余骨架原样搬。

4. 边界条件才是分水岭:逐项排查最容易丢分的细节

4.1 空串、纯字母、纯数字:三个送分测试

写完之后,先拿三组最基础的输入过一遍:

  • 空字符串 "":循环一次都不进,in_num 是 false,最终输出 0。
  • 纯字母 "abc":所有字符都是非数字,current 永远是 0,in_num 永远是 false,输出 0。
  • 纯数字 "12345":全程 in_num 都是 true,循环结束后靠那个 if 结算,输出 12345。

这三组用例能同时验证你的基础流程和末尾结算逻辑。很多人不测纯数字,结果就是"以数字开头且以数字结尾"这类用例出问题。

4.2 以数字结尾:漏掉 flush 就前功尽弃

再强调一次,因为这是全题最高频的丢分点。输入 "a12b345",扫描到 '5' 时,current 已经累加成了 345,in_num 是 true,此时字符串也到头了。如果不在循环外补上结算,345 就永远躺在 current 里,最后输出只有 12,丢一个测试点。

我的习惯是把循环外的结算代码和循环写在一起,形成肌肉记忆:

扫描完字符串后 如果 in_num 为真: total 累加 current

无论字符串以什么结尾,这段代码都不会产生副作用,因为 in_num 为 false 时它什么都不做。所以哪怕你对边界没把握,把这段加上也绝不会错。

4.3 前导零:007 当 7 还是当 “007”

输入 "a007b",用累加式写法,current 的演变是 0、0、7,最终结算成整数 7。从数学角度看完全正确:007 作为整数就是 7。

但要注意,有些找数字的变体题不是让你求整数和,而是让你计数,或者把数字子串原样输出。那种题里"007"是一个独立的数字子串,必须保留前导零。也就是说,007 到底算 7 还是算 "007",取决于题目问的是"整数值"还是"原始子串"。

我建议你每次做题前先花十秒钟读清楚样例输出。题目给了结果,你就能反推出它期望的是哪种语义。这算是一个读题技巧:样例输出会说话。

4.4 超长数字与溢出:四语言各自的防线

如果测试数据里出现一个超长数字,四种语言的防线完全不同。

Python 最省心,int 自动扩容,怎么加都不会溢出。Java 的 long 是 64 位,如果题目说数字最长可能有 20 位以上,long 也扛不住,得上BigInteger,代价是代码变啰嗦。JavaScript 的 Number 在超过 2 的 53 次方后就开始丢精度,极端情况要用 BigInt。C 的 long long 上限约 9.2 乘以 10 的 18 次方,如果数字可能超过这个范围,要么用前面给的溢出判断提前拦截,要么考虑用无符号类型扩大正数范围,但输入可能是负数的话又要另外处理。

最实用的策略是:先看题目给的约束范围。绝大多数这种分值的题,数字长度和求和结果都设计在 64 位范围内,你用 long 和 long long 就是安全的。如果你发现题面没说范围,别猜,直接用最宽的方案,或者干脆在代码里做好溢出保护。

4.5 输入读取:别让空格把字符串截断

题面说字符串里可能混着各种符号,但没说有没有空格。这种模糊描述是最坑人的。

C 语言里如果你用scanf("%s", buf)读输入,遇到空格就停,一个含空格的字符串会被读成两段,后面那段全部丢失。应该用fgets读整行。Java 里Scanner.next()同样按空白分割,得用nextLine()。Python 的input()和 JavaScript 的readline默认读整行,问题不大。

一个真实的教训:有人用scanf读,本地测试都过了,因为本地输入文件里字符串没有空格。提交到评测系统,有一个测试点的字符串里带着空格,直接读断,整道题白做。所以只要题面出现"包含各种符号"这种说法,我第一反应就是检查读取方式。

4.6 负数、小数、全角数字:没提就别自作主张

如果题面只说"数字字符",那默认就是 '0' 到 '9' 这十个 ASCII 字符。遇到 '-' 就当作普通分隔符,遇到 '.' 也一样。有人看到 "-12" 就下意识觉得要按负数处理,把 -12 加进总和——这属于自己给自己加戏。除非题面明确说"数字可带正负号",否则一律不处理符号。

全角数字也类似。比如 "123" 这种全角字符,它们的 Unicode 编码不在 48 到 57 区间,判断自然跳过,输出也不会被污染。这实际上是好事,说明我们的编码区间判断天然免疫了非 ASCII 数字。

我把这条总结成一个原则:题目说什么,就做什么;题目没说的,一律不发挥。

5. 题目稍微变形:从“求和”到“计数、取最大、保留原串”

5.1 改成求个数、最大值、乘积

同一个状态机骨架,改几个变量就能应对不同问法。

  • 求个数:把total += current换成count += 1,结算逻辑不变。
  • 求最大值:加一个max_num,每次结算时比较更新。初始值要注意,如果数字全为正整数,初始化为 -1 即可;如果可能包含 0,初始化成 -1 仍然安全,因为总和或单个数字至少是 0。
  • 求乘积:把total初始化成 1,结算时product *= current。但如果字符串里一个数字都没有,题目如果要求输出 0 还是输出 1,得看清楚。

核心状态机一行都不用改,变的只是"结算那一刻做什么"。这就是把骨架刻在脑子里的价值,遇到变体题,你省下的全是思考时间。

5.2 改成保留原始数字串

如果题目要求把数字子串原样输出,累加式 current 就不够用了,因为它在拼接过程中丢掉了前导零的信息。正确做法是换一个字符串缓冲区,遇到数字字符就 append,遇到非数字字符就把缓冲区内容作为一个结果输出,然后清空缓冲区。

在 Java 里用StringBuilder,在 Python 里用列表再''.join(),在 JavaScript 里用数组 push 再 join,在 C 里用一个字符数组手动管理。这里有一个性能提醒:尽量不要在循环里做result = result + c这种字符串拼接,Python 和 Java 的字符串是不可变对象,每次拼接都创建新对象,长字符串下会变成 O(n^2)。用可变缓冲区或数组收集字符,是正确且高效的做法。

5.3 加上过滤条件

有些变体题会问:只求大于某个阈值的数字之和,或者只统计偶数数字,又或者只计算每个包含特定数字位的子串。

以"只统计偶数"为例,判断时机有两个选择:一是在结算时检查 current 是否为偶数,二是整个数字都拼完了才知道奇偶,因为偶数的判断依赖最后一位,而最后一位在结算前才确定。所以正确位置一定是在结算处:

如果 in_num: 如果 current % 2 == 0: total += current

这个例子说明一个通用思路:过滤条件只要能放在结算点统一判断,就不要在扫描过程中提前处理。扫描过程越简单,越不容易出错。

5.4 从一维字符串到二维矩阵

如果你在其他题库里看到"找数字"出现在二维矩阵语境下,比如在一个 m 乘 n 的网格里找数字路径,那题目的复杂度就上了一个台阶,不再是单次扫描能搞定的。这种变体通常是搜索题,要用到 DFS 或者 BFS,状态机思路依然是基础,但要加上方向遍历、去重访问等逻辑。

遇到这种题别慌,先把一维版本的字符判断逻辑保留,再往上面叠加搜索框架。一维的找数字骨架是地基,二维题目是在地基上盖楼。

5.5 语言迁移时的检查清单

在线评测经常要求你用指定语言提交,而且有时候只能提交一次,没有试错机会。我给自己固定了一个五分钟检查清单:

  1. 怎么逐字符遍历字符串?C 用下标或指针,Java 用 charAt,JS 用 charCodeAt,Python 直接 for-in。
  2. 怎么判断数字字符?能用编码区间就用编码区间,C 里注意 unsigned char 转换。
  3. 用什么整数类型?默认选最宽的:Java 和 C 用 64 位,JS 考虑精度范围,Python 无所谓。
  4. 怎么读输入?确认读整行而不是按空白分割。
  5. 循环结束后的结算写了吗?没写的话先补上。

每换一种语言,就在心里把这五条过一遍。熟练之后,整个过程用不了三分钟。

6. 复盘与心得:考场上我踩过的坑和留下的习惯

6.1 我第一次只拿了一半分

说点真实经历。我第一次做这类题,用的是 C 语言版本,代码逻辑完全正确,本地测试也全过,但提交之后只过了一半测试点。排查了很久才发现两个问题:int 溢出,以及isdigit没做 unsigned char 转换。当时输入里有个测试串是含中文的,一个汉字拆成多个字节后,正好有一个字节落在 ASCII 数字区间附近,转换没做好,isdigit的返回结果变成了未知数,导致一个本来不该被识别的字符被当成了数字。

那次翻车之后我明白了一个道理:拿不到满分的题,通常不是输在算法上,而是输在语言细节上。这道题同时要求四种语言,某种程度上就是在逼你把每种语言的细节都摸清楚。

6.2 三分钟默写一个万能骨架

我现在可以把找数字的骨架在任意一种语言里默写出来,整个过程不用思考:

遍历字符串每个字符 如果 是数字字符: current = current * 10 + 数字值 in_num = true 否则: 如果 in_num: 结算 current current = 0 in_num = false 遍历结束后 如果 in_num: 结算 current

结算动作可以是累加、计数、比较大小、拼接输出串,根据题目要求替换就行。我会在考试前把这个骨架默写三遍,确保在紧张状态下也不会写错位置。

这个骨架的价值在于它不依赖任何语言特性,从汇编到脚本都能实现。你把它练成肌肉记忆,市面上八成"字符串里提取数字"的题都能覆盖。

6.3 每次提交前过一遍边界小抄

我现在提交前会固定做一遍脑内测试,按这个顺序走:

  • 空字符串和纯字母:应该输出 0 或空结果。
  • 纯数字:验证末尾结算。
  • 以数字结尾:验证 flush。
  • 以数字开头:验证 first number 正常进入。
  • 连续多个分隔符:验证不会重复结算。
  • 前导零数字:确认题目期望的语义。
  • 超长数字:确认类型不会溢出。

这道题最多也就两分钟能把这几个用例在脑子里跑完。评测系统不会给你第二次机会的题,这两分钟花得非常值。

6.4 一个小提醒:别在循环里反复拼接字符串

最后分享一个和找数字关系不大、但经常在同类题里出现的性能陷阱。如果你在实现"保留原始数字串"的变体时,用了类似result += s[i]这种写法,注意 Python、Java 的字符串是不可变对象,循环里每拼接一次就创建一次新字符串。在一个长字符串上做几千次拼接,时间开销会明显变大,甚至导致超时。

正确做法是:Python 用列表收集字符,最后 join;Java 用 StringBuilder;JavaScript 用数组 push 后 join;C 用字符数组自己管理位置。这是我在好几个项目里踩过实的教训,写在这里给后来者提个醒。

找数字这道题本身不算难,但它像一面镜子,能照出你对字符串、类型边界和边界条件的掌握程度。把这套思路吃透,再用四种语言各写一遍,你收获的不仅仅是一道 200 分的题,而是一个能迁移到日志解析、数据清洗、文本处理日常场景里的核心技能。

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

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

立即咨询