实现一个 atoi:跳过前导空格,读一个可选的正负号,再读连续的数字,遇到第一个非数字字符就停;结果超出 32 位有符号整数范围时,截断到边界值;没有读到任何数字时返回 0。规则很简单,坑全在细节里:哪些字符算"空格"、数字是哪几个字符、溢出怎么在不使用 64 位整数的前提下判断。本文实测直接使用语言自带的 int() 会在哪些输入上和题目规则不一致,给出五种语言的实现,并把三个阶段做成分步演示。
完整题目见官方页面(链接在文末)。前几组基准用例和结果由一个独立的做法算出(用正则表达式取出"空格 符号 数字",转成大整数后再截断)。一共 21 组,覆盖:前导空格、符号、前导零、非数字结尾、空串、只有符号、+- 连续符号、32 位上下界及刚好越界一位、远超 64 位的数、制表符:
输入 ["42"] 输出 42 输入 [" -042"] 输出 -42 输入 ["1337c0d3"] 输出 1337 输入 ["0-1"] 输出 0 输入 ["words and 987"] 输出 0 输入 [""] 输出 0
Python 的 int() 看起来能省掉所有工作,但它的规则和题目不一样。下面是 int(s.strip()) 与按题目规则实现的解法在几个输入上的真实结果:
# 直接用 int(s.strip()) 处理,和题目规则对比 '42' : int(s.strip()) -> 42 按题目规则 -> 42 ' -042' : int(s.strip()) -> -42 按题目规则 -> -42 '1337c0d3' : int(s.strip()) -> 抛出 ValueError: invalid literal for int() with base 10: '1337c0d3' 按题目规则 -> 1337 '\t42' : int(s.strip()) -> 42 按题目规则 -> 0 '1_000' : int(s.strip()) -> 1000 按题目规则 -> 1 '²' : int(s.strip()) -> 抛出 ValueError: invalid literal for int() with base 10: '²' 按题目规则 -> 0 '²'.isdigit() = True (isdigit 不等于 0~9 这十个字符)
四处不一致:1337c0d3 里有非数字字符,int() 直接抛出异常,而题目要求读到 1337 就停;\t42 前面是制表符,strip() 会把它去掉,得到 42,但题目只把空格字符当作可跳过的空白,所以答案是 0;1_000 里的下划线在 Python 的数字字面量里合法,int() 得到 1000,题目规则读到下划线就停,答案是 1;²(上标 2)的 isdigit() 返回 True,但它不是 0 到 9 这十个字符之一,int() 会报错。所以判断数字要写成字符范围比较 '0' <= c <= '9',不能用 isdigit()。
数字位数不限,结果却必须落在 32 位范围内。和第 7 题一样,在 res = res * 10 + d 之前先判断这一步会不会越界:res 已经大于 INT_MAX / 10,或者等于它并且 d 大于 7,就直接返回上界或下界(由符号决定)。这样 res 永远不会真的超出 32 位,Java、Go、C++ 里也不需要借助 64 位整数。用例里的 2147483648、-2147483649、9223372036854775808、21474836460 都是针对这个判断挑的。
按顺序做三件事:跳过空格、读一个可选的符号、读连续的数字。Python、Java、Go、JavaScript、C++ 五种语言都是真实编译运行的。JavaScript 版本最后返回 sign * res + 0,因为 -1 * 0 在 JavaScript 里得到的是 -0,加 0 才会变成 0(第 7 题里也遇到过同样的现象)。
Python · 解法代码def solve(s): INT_MAX, INT_MIN = 2**31 - 1, -(2**31) i, n = 0, len(s) while i < n and s[i] == " ": # 1. 跳过前导空格(只有空格字符,不含制表符) i += 1 sign = 1 if i < n and s[i] in "+-": # 2. 可选的一个符号 sign = -1 if s[i] == "-" else 1 i += 1 res = 0 while i < n and "0" <= s[i] <= "9": # 3. 读连续的数字,遇到第一个非数字就停 d = ord(s[i]) - ord("0") # 乘 10 之前先判断是否越界,越界直接截断到边界值 if res > INT_MAX // 10 or (res == INT_MAX // 10 and d > 7): return INT_MAX if sign == 1 else INT_MIN res = res * 10 + d i += 1 return sign * res
$ python3 solution.py # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON ["42"] → 42 [" -042"] → -42 ["1337c0d3"] → 1337 ["0-1"] → 0 ["words and 987"] → 0 [""] → 0
Java · 解法代码class Solution { public int myAtoi(String s) { int i = 0, n = s.length(); while (i < n && s.charAt(i) == ' ') i++; // 1. 跳过前导空格(只有空格字符) int sign = 1; if (i < n && (s.charAt(i) == '+' || s.charAt(i) == '-')) { // 2. 可选的一个符号 sign = s.charAt(i) == '-' ? -1 : 1; i++; } int res = 0; while (i < n && s.charAt(i) >= '0' && s.charAt(i) <= '9') { // 3. 读连续的数字 int d = s.charAt(i) - '0'; // 乘 10 之前先判断是否越界,越界直接截断到边界值 if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && d > 7)) return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE; res = res * 10 + d; i++; } return sign * res; } }
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl ["42"] → 42 [" -042"] → -42 ["1337c0d3"] → 1337 ["0-1"] → 0 ["words and 987"] → 0 [""] → 0
Go · 解法代码func myAtoi(s string) int { const maxInt32, minInt32 = 1<<31 - 1, -(1 << 31) i, n := 0, len(s) for i < n && s[i] == ' ' { // 1. 跳过前导空格(只有空格字符) i++ } sign := 1 if i < n && (s[i] == '+' || s[i] == '-') { // 2. 可选的一个符号 if s[i] == '-' { sign = -1 } i++ } res := 0 for i < n && s[i] >= '0' && s[i] <= '9' { // 3. 读连续的数字 d := int(s[i] - '0') // 乘 10 之前先判断是否越界,越界直接截断到边界值 if res > maxInt32/10 || (res == maxInt32/10 && d > 7) { if sign == 1 { return maxInt32 } return minInt32 } res = res*10 + d i++ } return sign * res }
$ go build -o bin . && ./bin < cases.jsonl ["42"] → 42 [" -042"] → -42 ["1337c0d3"] → 1337 ["0-1"] → 0 ["words and 987"] → 0 [""] → 0
JavaScript · 解法代码function myAtoi(s) { const INT_MAX = 2 ** 31 - 1, INT_MIN = -(2 ** 31); let i = 0; const n = s.length; while (i < n && s[i] === ' ') i++; // 1. 跳过前导空格(只有空格字符) let sign = 1; if (i < n && (s[i] === '+' || s[i] === '-')) { // 2. 可选的一个符号 sign = s[i] === '-' ? -1 : 1; i++; } let res = 0; while (i < n && s[i] >= '0' && s[i] <= '9') { // 3. 读连续的数字 const d = s.charCodeAt(i) - 48; // 乘 10 之前先判断是否越界,越界直接截断到边界值 if (res > Math.floor(INT_MAX / 10) || (res === Math.floor(INT_MAX / 10) && d > 7)) return sign === 1 ? INT_MAX : INT_MIN; res = res * 10 + d; i++; } return sign * res + 0; // + 0 把 -0 变成 0 }
$ node run.js < cases.jsonl ["42"] → 42 [" -042"] → -42 ["1337c0d3"] → 1337 ["0-1"] → 0 ["words and 987"] → 0 [""] → 0
C++ · 解法代码#include <climits> #include <string> using namespace std; class Solution { public: int myAtoi(string s) { int i = 0, n = s.size(); while (i < n && s[i] == ' ') i++; // 1. 跳过前导空格(只有空格字符) int sign = 1; if (i < n && (s[i] == '+' || s[i] == '-')) { // 2. 可选的一个符号 sign = s[i] == '-' ? -1 : 1; i++; } int res = 0; while (i < n && s[i] >= '0' && s[i] <= '9') { // 3. 读连续的数字 int d = s[i] - '0'; // 乘 10 之前先判断是否越界,越界直接截断到边界值 if (res > INT_MAX / 10 || (res == INT_MAX / 10 && d > 7)) return sign == 1 ? INT_MAX : INT_MIN; res = res * 10 + d; i++; } return sign * res; } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl ["42"] → 42 [" -042"] → -42 ["1337c0d3"] → 1337 ["0-1"] → 0 ["words and 987"] → 0 [""] → 0
共 21 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 21/21 Java 通过 21/21 Go 通过 21/21 JavaScript 通过 21/21 C++ 通过 21/21
Python 版本另外和暴力解(正则)做了随机对拍:
随机对拍 20000 组(前导空格 0 到 3 个、随机符号(包括 +-、字母这类非法开头)、最多 12 位数字、随机的非数字尾巴),solve 与 brute 的结果全部一致。
另外四种语言只在上面 21 组固定用例上验证过,没有做大规模随机对拍。
下面的演示用 s = " -042abc":三个空格、一个负号、042、然后是字母。每一帧都是把上面的解法在这个字符串上真实执行记录下来的,运行器还检查了演示最终得到的结果与 solve 的返回值一致。