简介:本文围绕“将字符串转换为32位有符号整数”这一经典算法问题展开,系统拆解前导空格过滤、符号识别、数字累积与越界截断四个解析阶段,梳理典型边界条件与易错点,重点推导“乘加前预判溢出”的安全技巧,并配套参考实现、测试对照表与复杂度分析,帮助读者建立字符串解析类问题的通用求解框架。
“字符串转整数”(atoi,即 ASCII to Integer 的缩写)是算法题库与工程实践中都极为经典的一类解析问题。其目标是实现函数 myAtoi(string s):按既定规则把输入字符串 s 解析为一个 32 位有符号整数,取值范围为 [−2^31, 2^31 − 1],即 [−2147483648, 2147483647]。
这类问题代码量很小,却能同时考查三项基本功:
值得注意的是,myAtoi 采用的是“尽力而为 + 越界截断”的容错语义:遇到非法字符不抛异常,而是终止解析并返回已累积的值;结果越界时也不报错,而是收敛到 32 位边界值。理解这一语义设定,是后续所有实现决策的前提。
整个解析过程可以抽象为“单指针、单向前进、阶段推进”的线性扫描,指针从不回溯。
指针从头开始,跳过所有连续的空格字符。需要注意两点:其一,题目通常只要求跳过空格本身,制表符、换行符等其他空白字符不在此列;其二,空格只在“最前面”有效——一旦进入符号识别或数字累积阶段,再遇到空格就属于“非数字字符”,会直接终止解析。
空格之后,若下一个字符是 ‘+’ 或 ‘-‘,记录符号并让指针前进一步;若不存在符号字符,默认结果为正。符号位有三条约束:最多出现一次;必须出现在数字之前;连续出现两个符号(如 “+-“)意味着第二个符号会被当作非法字符,解析终止且结果为 0。
从当前位置开始逐个读取字符:只要落在 ‘0’ ~ ‘9’ 区间内,就将其并入当前结果;遇到任何非数字字符或抵达串尾,立即停止。前导零不需要单独处理——它们在“乘 10 加 digit”的累积过程中会被自然吸收。若本阶段一个数字都没有读到(例如输入 “words and 987”),最终结果为 0。
若累积出的整数超出 32 位有符号范围,则收敛到边界:小于 −2^31 的取 −2^31,大于 2^31 − 1 的取 2^31 − 1。这一步的实现位置非常关键——正确做法是在累积过程中“预判”,而不是算完再裁剪,原因见第四节。
一个帮助理解的关键认知:四个阶段是严格顺序、不可回溯的。一旦进入数字累积阶段,后续再出现的符号、空格等一律视为终止信号,这正是 “0-1” 输出 0、”3.14159” 输出 3 的根本原因。
解析类问题的规则分支极多,建议在写代码之前先把边界条件整理成“输入—期望输出”对照表,编码时逐一对照,编码后再转成自动化断言:
| 输入 | 输出 | 考查点 |
|---|---|---|
| “42” | 42 | 最简正例 |
| “ -042” | -42 | 前导空格、负号、前导零三者叠加 |
| “1337c0d3” | 1337 | 数字段中遇非法字符即截断 |
| “0-1” | 0 | 只读取第一段连续数字 |
| “words and 987” | 0 | 首字符即非法,未读到任何数字 |
| “+-12” | 0 | 符号只允许出现一次 |
| “” / “ “ | 0 | 空串或纯空格串 |
| “ + 413” | 0 | 符号与数字之间的空格触发终止 |
| “3.14159” | 3 | 小数点是非数字字符 |
| “2147483648” | 2147483647 | 正向越界截断 |
| “-91283472332” | -2147483648 | 负向越界截断 |
| “2147483647” / “-2147483648” | 原值 | 恰好落在边界上的合法值 |
许多初学者的直觉写法是:先执行 res = res 10 + digit,再检查 res 是否越界。问题在于,这一步乘加本身可能已经超出 32 位整数的表示能力。在 C、C++、Java 这类定宽整数语言中,有符号整数溢出要么是未定义行为,要么发生静默回绕——溢出发生之后 res 的值已经不可信,再拿它去比较毫无意义。因此,越界检查必须*前移到乘加操作之前。
设当前非负累积值为 res,即将并入的数字为 digit,上界 INT_MAX = 2^31 − 1。本次并入安全的充要条件是:
res * 10 + digit ≤ INT_MAX
对不等式做整除变换,得到等价的预判条件:
res ≤ (INT_MAX − digit) / 10 (整数除法)
于是,在执行乘加之前检查:一旦发现 res > (INT_MAX − digit) / 10,说明本次并入必然越界,立即按符号返回对应边界值。整个判断只涉及一次减法与一次整除,任何中间量都不会溢出。
细心的读者会问:负方向的下界是 −2^31,其绝对值比 INT_MAX 大 1,用上界做预判是否会在输入 “-2147483648” 时误判?分析可知:此时预判条件确实会被触发(res = 214748364、digit = 8 时越界),走截断分支并返回 −2^31——而这恰好就是正确答案。也就是说,“按上界预判 + 越界即截断”的策略与题目“越界收敛到边界值”的语义天然自洽,负方向多出的那 1 个表示空间被截断逻辑精确覆盖。
此外还有一种“全程以负数累积”的变体(res = res * 10 − digit,最后再统一应用符号),它利用负方向表示空间更大这一特性,让预判条件写起来更对称。两种写法本质等价,掌握其一、理解推导过程即可。
对于 Python 这类原生支持任意精度整数的语言,数值本身不会溢出,可以先累积再统一收敛;但预判式写法具有跨语言可移植性,也更贴近真实工程中的安全编码要求,建议优先掌握。
以下给出两个版本,逻辑完全一致,均采用“乘加前预判”的安全写法。
版本一(Python,便于阅读与验证):
def myAtoi(s: str) -> int:INT_MAX, INT_MIN = 2 ** 31 - 1, -2 ** 31i, n = 0, len(s)# 阶段一:跳过前导空格while i < n and s[i] == ' ':i += 1# 阶段二:识别符号位(至多一个)sign = 1if i < n and s[i] in '+-':if s[i] == '-':sign = -1i += 1# 阶段三:累积数字;阶段四:乘加前预判溢出res = 0while i < n and '0' <= s[i] <= '9':digit = ord(s[i]) - ord('0')if res > (INT_MAX - digit) // 10:return INT_MIN if sign == -1 else INT_MAXres = res * 10 + digiti += 1return sign * res
版本二(C++,定宽整数下的安全写法):
#include <climits>#include <string>int myAtoi(const std::string& s) {int i = 0, n = static_cast<int>(s.size());while (i < n && s[i] == ' ') ++i; // 1. 跳过前导空格int sign = 1;if (i < n && (s[i] == '+' || s[i] == '-')) { // 2. 识别符号if (s[i] == '-') sign = -1;++i;}int res = 0;while (i < n && s[i] >= '0' && s[i] <= '9') { // 3. 累积数字int digit = s[i] - '0';if (res > (INT_MAX - digit) / 10) { // 4. 溢出预判return sign == 1 ? INT_MAX : INT_MIN;}res = res * 10 + digit;++i;}return sign * res;}
Java 等其他定宽整数语言的写法与版本二同理,仅需替换边界常量的表达方式。
测试策略:将第三节的对照表改写为参数化断言,覆盖“空格 / 符号 / 前导零 / 非法字符 / 边界值 / 越界值”六个维度。解析类问题的回归测试成本极低、收益极高——规则一旦写错,往往是成批用例同时失败,表驱动测试能最快定位是哪一条规则被破坏。
复杂度分析:
本算法本质上是一个四状态的确定性有限状态机(DFA)。定义状态集合 {起始态, 符号态, 数值态, 终止态},按字符类别(空格 / 符号 / 数字 / 其他)给出转移表:
| 当前状态 \ 输入字符 | 空格 | 符号 | 数字 | 其他 |
|---|---|---|---|---|
| 起始态 | 起始态 | 符号态 | 数值态 | 终止态 |
| 符号态 | 终止态 | 终止态 | 数值态 | 终止态 |
| 数值态 | 终止态 | 终止态 | 数值态 | 终止态 |
| 终止态 | 终止态 | 终止态 | 终止态 | 终止态 |
进入数值态时执行累积与溢出预判,离开数值态或扫描结束时应用符号并做最终裁剪。状态机视角的价值在于可扩展性:当解析规则升级为“允许科学计数法”“允许千分位逗号”“允许多进制前缀”时,在转移表上增补行列,远比在嵌套条件分支中打补丁更清晰、更不易引入回归缺陷。
字符串转整数是一道“小身材、大容量”的经典题目:它以几十行代码的体量,浓缩了状态控制的扫描流程、覆盖完备的边界用例设计,以及防患于未然的溢出预判三项基本功。掌握“跳空格 → 读符号 → 累数字 → 截越界”的四段式框架后,浮点数字面量解析、带千分位的金额字符串、简单表达式的前置词法分析等同类问题,都可以在同一副骨架上按需扩展。