bigywater.tech
算法与数据结构 · LeetCode 8

LeetCode 8. 字符串转换整数 (atoi)

实现一个 atoi:跳过前导空格,读一个可选的正负号,再读连续的数字,遇到第一个非数字字符就停;结果超出 32 位有符号整数范围时,截断到边界值;没有读到任何数字时返回 0。规则很简单,坑全在细节里:哪些字符算"空格"、数字是哪几个字符、溢出怎么在不使用 64 位整数的前提下判断。本文实测直接使用语言自带的 int() 会在哪些输入上和题目规则不一致,给出五种语言的实现,并把三个阶段做成分步演示。

20,000
组随机对拍,与暴力解的结果全部一致
10 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。前几组基准用例和结果由一个独立的做法算出(用正则表达式取出"空格 符号 数字",转成大整数后再截断)。一共 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()。

陷阱二:溢出要在乘 10 之前判断

数字位数不限,结果却必须落在 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
真实实测 · Python · 前几组用例的输出
$ 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; } }
真实实测 · Java · 前几组用例的输出
$ 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 · 前几组用例的输出
$ 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 }
真实实测 · JavaScript · 前几组用例的输出
$ 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; } };
真实实测 · C++ · 前几组用例的输出
$ 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 的返回值一致。

字符串转整数:s = " -042abc"
正在读的字符已经消耗的字符

    参考与说明

    • 官方题目页:LeetCode 8. 字符串转换整数 (atoi)。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:Python 里"只处理十进制"之外的写法(int(s, 16) 等)与本题无关;含多字节字符的输入按字节处理时的表现。
    京ICP备2026067502号-1京公网安备11010502063487号