力扣刷题日常(7-8)
第7题: 整数反转(难度: 中等)
原题:
给你一个 32 位的有符号整数 x
,返回将 x
中的数字部分反转后的结果.
如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1]
,就返回 0.
假设环境不允许存储 64 位整数(有符号或无符号).
示例 1:
输入:x = 123
输出:321
示例 2:
输入:x = -123
输出:-321
示例 3:
输入:x = 120
输出:21
示例 4:
输入:x = 0
输出:0
提示:
-231 <= x <= 231 - 1
开始解题:
首先,这道题虽然是中等难度,但它考察的重点——整数溢出处理,是编程中一个非常重要且实用的概念.
我们先来分析这道题的实现逻辑
算法实现逻辑
我们先从数学的角度思考,如何把一个数字 123
,颠倒过来变成 321
?
核心思想是数学方法,通过循环不断"剥离"原数字的末尾,然后"构建"新数字的开头
我们以 123
为栗子,来走一步这个流程
-
初始化: 我们需要一个变量来存储反转后的结果,命名为
reversed_x
,初始时reversed_x = 0
-
第一轮循环:
- 取末位: 如何得到最后一位呢----取余(%)运算.
123 % 10
的结果就是3
.我们将这个数字定为dight
. - 构建新数: 把这个
dight
添加到reversed_x
中.reversed_x = reversed_x * 10 + dight
. 经过计算,现在reversed_x
的值为0 * 10 + 3 = 3
- "砍掉"原数的末位: 如何砍掉原数的末位呢----整除(/)运算.
123 / 10
的结果就是12
.现在x
变成了12
.
- 取末位: 如何得到最后一位呢----取余(%)运算.
-
第二轮循环:
-
此时
x = 12
,reversed_x = 3
. -
取末位:
12 % 10
得到2
.(digit = 2
) -
构建新数:
reversed_x = 3 * 10 + 2
,结果为32
. -
“砍掉”原数的末位:
12 / 10
得到1
.现在x
变成了1
.
-
-
第三轮循环:
-
此时
x = 1
,reversed_x = 32
. -
取末位:
1 % 10
得到1
.(digit = 1
) -
构建新数:
reversed_x = 32 * 10 + 1
,结果为321
. -
“砍掉”原数的末位:
1 / 10
得到0
.现在x
变成了0
.
-
-
循环结束: 我们的循环条件是
x != 0
.现在x
已经是0
了,所以循环停止.最终的结果reversed_x
就是321
.
如何处理特殊情况?
- 负数 (如 -123): 这个数学方法同样适用
-123 % 10
结果是-3
.-123 / 10
结果是-12
.- 整个过程下来,符号会被自然而然地保留,最终得到
-321
.
- 末尾是0 (如 120):
- 第一轮,
120 % 10
得到0
,reversed_x
变成0
.x
变成12
. - 后续步骤和
12
的反转一样,最终得到21
.前导零被自动消除了.
- 第一轮,
本题最大的难点: 处理整数溢出
题目要求环境是32位有符号整数,范围是 [-2^31, 2^31 - 1]
,也就是 [-2147483648, 2147483647]
.
在我们的核心步骤 reversed_x = reversed_x * 10 + digit
中,reversed_x * 10
的结果可能会超出这个范围.由于题目不允许使用64位整数(long)来临时存储,我们不能等溢出发生了再去判断.我们必须在它发生之前就预判到.
如何进行预判:
我们来分析一下正数溢出的情况.溢出即将发生在 reversed_x
乘以 10 的过程中
int
的最大值为 int.MaxValue
(即 2147483647
)
- 一个粗略的检查: 如果
reversed_x
大于int.MaxValue / 10
(即214748364
),那么它再乘以10,无论digit
是多少,都必然会溢出. - 一个精确的检查: 如果
reversed_x
正好等于int.MaxValue / 10
(即214748364
),那么能否溢出就取决于digit
了.int.MaxValue
的末尾是7
.- 如果
digit
大于7
,那么214748364 * 10 + digit
就会大于2147483647
,导致溢出. - 如果
digit
小于等于7
,就不会溢出.
所以,正数溢出的完整判断条件是:
if (reversed_x > int.MaxValue / 10 || (reversed_x == int.MaxValue / 10 && digit > 7))
同理,负数溢出的判断条件(int.MinValue
是 -2147483648
):
if (reversed_x < int.MinValue / 10 || (reversed_x == int.MinValue / 10 && digit < -8))
一旦检测到即将溢出,我们就按题目要求返回 0
.
简单总结:
- 初始化
reversed_x = 0
. - 使用
while
循环,条件是x != 0
. - 在循环内部:
a. 通过x % 10
取出x
的末位数字digit
.
b. 在执行乘法前,进行溢出检查.如果检查到即将溢出,立即return 0
.
c. 如果安全,则更新reversed_x = reversed_x * 10 + digit
.
d. 通过x / 10
“砍掉”x
的末位. - 循环结束后,返回
reversed_x
.
算法代码:
public class Solution
{public int Reverse(int x) {// 用于存储反转后的结果int reversed = 0;// 循环直到x的所有位都被处理while (x != 0) {// 1. 取出x的末尾数字// C#的取余(%)运算会自动处理符号,例如 -123 % 10 的结果是 -3int digit = x % 10;// 2. “砍掉”x的末尾数字// C#的整除(/)运算同样会自动处理符号,例如 -123 / 10 的结果是 -12x /= 10;// 3. 【核心】在构建新数之前,检查是否会溢出// 检查正数溢出:// a) reversed > 214748364 (int.MaxValue / 10)// 如果成立,那么 reversed * 10 必然大于 int.MaxValue,溢出.// b) reversed == 214748364 && digit > 7// 如果成立,那么 reversed * 10 + digit 必然大于 2147483647,溢出.if (reversed > int.MaxValue / 10 || (reversed == int.MaxValue / 10 && digit > 7)) {return 0; // 发生正向溢出,返回0}// 检查负数溢出:// a) reversed < -214748364 (int.MinValue / 10)// 如果成立,那么 reversed * 10 必然小于 int.MinValue,溢出.// b) reversed == -214748364 && digit < -8// 如果成立,那么 reversed * 10 + digit 必然小于 -2147483648,溢出.if (reversed < int.MinValue / 10 || (reversed == int.MinValue / 10 && digit < -8)) {return 0; // 发生负向溢出,返回0}// 4. 安全,构建新数// 将取出的末位数字拼接到reversed的末尾reversed = reversed * 10 + digit;}// 循环结束,返回最终结果return reversed;}
}
知识点总结:
-
数值溢出:一个“沉默的杀手”
- 核心思想:任何有固定大小的数据类型(如
byte
,short
,int
,long
)都有其表示范围.当计算结果超出这个范围时,就会发生“溢出”.在C#中,整数溢出默认是不抛出异常的,它会“环绕”(wrap around),例如int.MaxValue + 1
会变成int.MinValue
.这种不报错的错误行为非常隐蔽,是很多逻辑bug的根源. - 通用原则:在进行任何可能导致结果急剧增大的运算(尤其是乘法和累加)时,都要有意识地思考:“我的结果会超出数据类型的边界吗?”
- 核心思想:任何有固定大小的数据类型(如
-
防御性编程:在错误发生前预判
- 核心思想:不要等到溢出发生后再去处理(因为你可能根本察觉不到),而是在执行运算之前,就检查这次运算是否安全.
- 本题范例:
if (reversed > int.MaxValue / 10 ...)
就是一个完美的防御性编程范例.我们通过逆向思维(从int.MaxValue
反推),为即将执行的reversed * 10
操作建立了一个“安全护栏”. - 通用原则:对于关键运算,先检查其操作数是否在安全范围内,再执行运算.这比“事后补救”要健壮得多.
-
数学方法处理数字:高效且独立
- 核心思想:使用取余 (
%
) 和整除 (/
) 是一套“组合拳”,可以逐位拆解任何整数,而无需将其转换为字符串. - 优点:这种纯数学处理通常比字符串转换和操作(如
ToString()
,ToCharArray()
,int.Parse()
)的性能要高得多,并且不产生额外的内存分配(GC),这在对性能敏感的UnityUpdate
循环中尤其重要. - 通用原则:当需要处理数字的各个位时,优先考虑
%
和/
的数学方法.
- 核心思想:使用取余 (
练习题:
选择题
1. 在本题的溢出判断 if (reversed > int.MaxValue / 10 || ...)
中,为什么我们用 int.MaxValue / 10
进行比较,而不是直接写 if (reversed * 10 > int.MaxValue)
?
A. 因为 reversed * 10
可能会先于比较操作发生溢出,导致 if
判断的条件本身就是基于一个错误(已环绕)的值,从而判断失效.
B. 这是一种性能优化,除法比乘法快.
C. 因为 int.MaxValue
不能被10整除,所以必须先做除法.
D. 为了让代码更易读.
2. 如果你需要获取一个正整数 n
(例如 n = 54321
) 的百位数(也就是 3
),以下哪个表达式是正确的?
A. n % 100
B. n / 100
C. (n / 100) % 10
D. n % 1000 / 100
简答题
3. 假设你需要反转一个 short
类型的整数(范围: -32768 到 32767).请参照本题的思路,写出针对 short
类型的正数溢出检查的 if
条件语句.
参考答案:
选择题答案
1. 在本题的溢出判断 if (reversed > int.MaxValue / 10 || ...)
中,为什么我们用 int.MaxValue / 10
进行比较,而不是直接写 if (reversed * 10 > int.MaxValue)
?
正确答案:A
解析:
- A. 因为
reversed * 10
可能会先于比较操作发生溢出,导致if
判断的条件本身就是基于一个错误(已环绕)的值,从而判断失效.- 这是最关键的原因.在C#中,
if (A > B)
这个表达式会先计算A
的值,再计算B
的值,最后进行比较.如果reversed
的值已经很大(例如300000000
),那么在执行reversed * 10
时,结果就已经超出了int
的最大值,发生了溢出并“环绕”成了一个负数.此时,if
语句就变成了if (一个负数 > int.MaxValue)
,这个条件永远是false
,导致我们错过了真正的溢出,程序会继续使用这个错误的负数进行计算.而reversed > int.MaxValue / 10
这种写法,将运算转移到了不易溢出的一侧,从而安全地预判了风险.
- 这是最关键的原因.在C#中,
- B. 这是一种性能优化,除法比乘法快.
- 这个说法是错误的.在大多数现代处理器上,整数乘法通常比整数除法要快.
- C. 因为
int.MaxValue
不能被10整除,所以必须先做除法.int.MaxValue
能否被10整除与判断逻辑的正确性无关.
- D. 为了让代码更易读.
- 虽然代码可读性很重要,但在这里,最主要的原因是为了保证逻辑的正确性,防止因运算顺序导致溢出,从而使判断失效.
2. 如果你需要获取一个正整数 n
(例如 n = 54321
) 的百位数(也就是 3
),以下哪个表达式是正确的?
正确答案:C (选项 D 在数学上也是正确的,但 C 是更通用和标准的方法)
解析:
- 我们的目标是隔离出百位上的
3
. - 第一步:去掉百位右边的所有数字. 我们可以通过整除
100
来实现.
54321 / 100
的结果是543
.现在,我们想要的目标数字3
成为了结果的个位数. - 第二步:从新结果中取出个位数. 我们可以通过对
10
取余来实现.
543 % 10
的结果是3
. - 所以,组合起来就是
(n / 100) % 10
. - 为什么 D
n % 1000 / 100
也可以?54321 % 1000
的结果是321
(取出了后三位).321 / 100
的结果是3
(对后三位数取百位).- 虽然结果正确,但选项 C 的思路(“移位”再“取末位”)更具有普适性.例如,要取任意第
k
位的数字,通用的公式就是(n / 10^k) % 10
.
简答题答案:
// short.MaxValue 是 32767// short.MaxValue / 10 是 3276// short.MaxValue % 10 是 7// 假设 digit 是当前取出的末位数,rev 是 short 类型的反转结果if (rev > short.MaxValue / 10 || (rev == short.MaxValue / 10 && digit > 7)){// 即将发生正向溢出return 0; }
实际应用场景:
-
游戏经济系统(金币、资源)
- 场景:玩家的货币数量通常用
int
或long
存储.如果一个玩家拥有接近int.MaxValue
的金币,此时他完成一个任务获得了少量金币,若不检查溢出,他的金币数可能会瞬间从一个巨大的正数变成负数,导致经济系统崩溃. - 应用:在增加玩家货币的函数中,必须加入溢出检查.例如
if (playerGold > int.MaxValue - rewardAmount)
,如果成立,则直接将玩家金币设为int.MaxValue
,而不是执行加法.
- 场景:玩家的货币数量通常用
-
计分与伤害计算
- 场景:在一个伤害可以无限叠加的“割草”游戏中,或者一个分数可以滚得极高的街机游戏中,总伤害或总分数很容易超过
int
的上限. - 应用:使用
long
类型来存储分数/伤害值.或者,在每次累加伤害前,进行防御性检查,防止数值环绕.
- 场景:在一个伤害可以无限叠加的“割草”游戏中,或者一个分数可以滚得极高的街机游戏中,总伤害或总分数很容易超过
-
UI显示和格式化
- 场景:你需要将一个分数
12345
显示成带有闪烁效果的单个数字1
,2
,3
,4
,5
. - 应用:可以使用
%
和/
的技巧,循环取出每个数字,然后为每个数字实例化一个UI组件并应用动画,而无需进行字符串操作.
- 场景:你需要将一个分数
-
程序化生成(Procedural Generation)
- 场景:使用一个整数种子(Seed)来生成关卡.你可以通过拆解这个种子的各位数字来决定不同的生成规则.
- 应用:例如,
seed % 10
的结果决定地图主题(0=森林, 1=沙漠…),(seed / 10) % 10
的结果决定敌人密度,以此类推.这让一个简单的整数种子可以控制多个生成维度.
第8题: 字符串转换整数 (atoi) (难度: 中等)
原题(这部分粘贴存在问题,可自行去力扣查看):
请你来实现一个 myAtoi(string s)
函数,使其能将字符串转换成一个 32 位有符号整数.
函数 myAtoi(string s)
的算法如下:
- 空格: 读入字符串并丢弃无用的前导空格(
" "
) - 符号: 检查下一个字符(假设还未到字符末尾)为
'-'
还是'+'
.如果两者都不存在,则假定结果为正. - 转换: 通过跳过前置零来读取该整数,直到遇到非数字字符或到达字符串的结尾.如果没有读取数字,则结果为0.
- 舍入: 如果整数数超过 32 位有符号整数范围
[−231, 231 − 1]
,需要截断这个整数,使其保持在这个范围内.具体来说,小于−231
的整数应该被舍入为−231
,大于231 − 1
的整数应该被舍入为231 − 1
.
返回整数作为最终结果.
示例 1:
输入: s = “42”
输出: 42
解释: 加粗的字符串为已经读入的字符,插入符号是当前读取的字符.
带下划线线的字符是所读的内容,插入符号是当前读入位置.
第 1 步:"42"(当前没有读入字符,因为没有前导空格)^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')^
第 3 步:"42"(读入 "42")^
示例 2:
输入: s = " -042"
输出:-42
解释:
第 1 步:" -042"(读入前导空格,但忽视掉)^
第 2 步:" -042"(读入 '-' 字符,所以结果应该是负数)^
第 3 步:" -042"(读入 "042",在结果中忽略前导零)^
示例 3:
输入: s = “1337c0d3”
输出: 1337
解释:
第 1 步:"1337c0d3"(当前没有读入字符,因为没有前导空格)^
第 2 步:"1337c0d3"(当前没有读入字符,因为这里不存在 '-' 或者 '+')^
第 3 步:"1337c0d3"(读入 "1337";由于下一个字符不是一个数字,所以读入停止)^
示例 4:
输入: s = “0-1”
输出: 0
解释:
第 1 步:"0-1" (当前没有读入字符,因为没有前导空格)^
第 2 步:"0-1" (当前没有读入字符,因为这里不存在 '-' 或者 '+')^
第 3 步:"0-1" (读入 "0";由于下一个字符不是一个数字,所以读入停止)^
示例 5:
输入: s = “words and 987”
输出: 0
解释:
读取在第一个非数字字符“w”处停止.
提示:
0 <= s.length <= 200
s
由英文字母(大写和小写)、数字(0-9
)、' '
、'+'
、'-'
和'.'
组成
开始解题:
那么这道题的本质是模拟一个字符串解析器. 我们可以把它想象成一个机器人,它会从左到右扫描一个字符串,并根据一套严格的规定来决定自己该做什么.如果我们将这个过程分解,就会发现它非常符合逻辑,就像在Unity中编写一个角色的AI状态机一样.
我们可以把整个过程分为几个连续的状态或步骤:
第一步: 初始状态 -> "寻找数字"状态
目标: 跳过所有无关紧要的前导空格.
- 机器人行为: 我们的机器人从字符串的第一个字符(索引
0
)开始.它会问自己:“这个字符是空格吗?”- 如果是,它会简单的向前走一步(索引
+1
),然后继续问同样的问题. - 如果不是,或者已经走到了字符串的尽头,这个阶段就结束了.
- 如果是,它会简单的向前走一步(索引
- 关键点: 这个阶段是"清理"阶段,为后续真正的解析做准备.如果整个字符串都是空格,那么机器人走到头也找不到任何有用的东西,最终就应该返回
0
.
第二步: "寻找数字"状态 -> "确认符号"状态
目标: 在找到第一个非空格字符后,判断数字的正负.
-
机器人行为: 机器人停在了第一个非空格字符上.它会检查这个字符:
- 是
'-'
吗?如果是,它就在自己的小本本上记下“这是个负数”,然后向前走一步,准备读取数字. - 是
'+'
吗?如果是,它记下“这是个正数”(或者什么都不记,因为默认就是正数),然后也向前走一步. - 如果既不是
'-'
也不是'+'
,机器人就认为这是一个正数,并且停在原地不动,因为这个字符可能就是数字本身.
- 是
-
关键点: 符号只在数字的最前面出现才有效.例如,对于
" -42"
,这个阶段会记录负号;但对于" 4-2"
,这个阶段会认为数字是正数,因为第一个非空格字符是'4'
.
第三步: "确认符号"状态 -> "转换数字"状态
目标: 连续读取所有数字字符,并将它们组合成一个整数.
-
机器人行为: 现在机器人已经准备好读取核心数字了.它会继续向前扫描:
- “当前字符是数字(
'0'
到'9'
)吗?” - 如果是,它就执行一个核心操作:
当前结果 = 当前结果 * 10 + 这个数字的值
.- 例如,如果当前结果是
4
,读到了新数字'2'
,那么新结果就是4 * 10 + 2 = 42
. - 如果当前结果是
42
,读到了新数字'3'
,那么新结果就是42 * 10 + 3 = 423
.
- 例如,如果当前结果是
- 如果当前字符不是数字,或者走到了字符串的尽头,这个阶段就立刻结束.
- “当前字符是数字(
-
一个巨大的陷阱(核心难点):整数溢出(第7题)
- C# 的
int
类型有范围限制,即[-2147483648, 2147483647]
. - 在执行
当前结果 = 当前结果 * 10 + 这个数字的值
之前,我们必须预判这次计算是否会导致结果超出范围. - 如何预判?
- 假设我们正在构建一个正数.
int
的最大值是2147483647
. - 在乘以10之前,如果
当前结果
已经大于int.MaxValue / 10
(即214748364
),那么再乘以10必然溢出. - 或者,如果
当前结果
正好等于int.MaxValue / 10
(即214748364
),那么我们就要看下一个数字了.如果这个数字大于int.MaxValue % 10
(即7
),那么加法操作就会导致溢出.
- 假设我们正在构建一个正数.
- 如果预判到会溢出,就不能再继续计算了,必须立即停止并返回该方向的极值(正数返回
int.MaxValue
,负数返回int.MinValue
).
- C# 的
第四步: "转换数字"状态 -> "最终返回"状态
目标: 结合符号和计算出的数值,得到最终结果.
-
机器人行为: 数字读取循环结束后,我们得到了一个数值(在计算过程中我们一直把它当作正数处理).
- 现在,机器人拿出它的小本本,看看在第二步记录的符号是什么.
- 如果是“负数”,就把计算出的数值乘以
-1
. - 如果是“正数”,就保持原样.
- 最后,返回这个最终的整数.
-
特殊情况: 如果从头到尾都没有读到任何一个数字(例如
"words and 987"
或者" -abc"
),那么“转换数字”这个阶段根本就不会执行,我们计算出的数值会保持初始值0
.最终返回0
,完全符合题意.
总结一下逻辑:
开始
-> 循环跳过空格
-> 判断并记录符号
-> 循环读取数字并处理溢出
-> 结合符号返回结果
代码与讲解:
public class Solution {public int MyAtoi(string s) {// 1. 初始化变量int i = 0; // 我们的“机器人”或指针,从字符串开头出发int sign = 1; // 符号,默认为正long result = 0; // 使用 long 类型来存储中间结果,方便检测溢出int n = s.Length;// 2. 步骤一:丢弃前导空格while (i < n && s[i] == ' ') {i++;}// 3. 步骤二:检查符号if (i < n && (s[i] == '+' || s[i] == '-')) {sign = (s[i] == '-') ? -1 : 1;i++;}// 4. 步骤三:转换数字并处理溢出while (i < n && char.IsDigit(s[i])) {// 将字符转换为数字int digit = s[i] - '0';// 核心:构建数字result = result * 10 + digit;// 核心:在构建过程中检查是否溢出if (sign * result > int.MaxValue) {return int.MaxValue;}if (sign * result < int.MinValue) {return int.MinValue;}i++;}// 5. 步骤四:结合符号并返回最终结果// 将 long 类型的结果乘以符号,并强制转换为 intreturn (int)(sign * result);}
}
代码讲解:
-
long
数据类型- 是什么:
long
是一个64位有符号整数类型,它的范围比32位的int
大得多(大约是 -9 x 10^18 到 9 x 10^18). - 为什么用在这里: 这是处理整数溢出问题的一个绝佳技巧.题目要求我们检测结果是否会超出
int
的范围.如果我们直接用int
来累加,一旦溢出,它就会变成一个意想不到的负数(或正数),我们就丢失了溢出前的信息.通过使用long
这个“更大的容器”来存放计算结果,我们可以安全地进行result = result * 10 + digit
操作.然后,在每一步都检查这个long
类型的结果是否已经超出了int
的范围.这让溢出判断变得非常简单和清晰.
- 是什么:
-
*
int.MaxValue
和int.MinValue
- 是什么: 它们是
System.Int32
结构体中定义的两个公共静态常量(public static const
).int.MaxValue
的值是2,147,483,647
,int.MinValue
的值是-2,147,483,648
. - 如何使用: 它们为我们提供了
int
类型的精确边界,是进行范围判断和“截断”(Clamping)操作的权威标准.在代码中,我们用if (sign * result > int.MaxValue)
来判断是否超过了正数边界.这比自己手写2147483647
要更具可读性,也更不容易出错.在Unity中,我们常用Mathf.Infinity
,这与int.MaxValue
在概念上是类似的,都代表了某种类型的边界.
- 是什么: 它们是
-
char.IsDigit(char c)
- 是什么: 这是一个非常方便的静态方法,用于判断一个给定的字符是否是十进制数字(‘0’ 到 ‘9’).
- 为什么用它: 在C#中,我们当然可以写
s[i] >= '0' && s[i] <= '9'
.但这有几个小缺点:可读性稍差,且它隐含了字符编码是连续的假设(虽然对于数字来说总是如此).使用char.IsDigit()
是更推荐的“C#风格”写法,它意图明确,代码更干净.
-
字符的算术运算:
s[i] - '0'
- 是什么: 这是C#(以及C/C++/Java)中一个非常经典和高效的技巧.在内部,
char
类型实际上是存储为一个数字(其Unicode编码值).幸运的是,所有数字字符'0', '1', '2', ... '9'
的编码是连续的. - 如何工作: 因此,用一个数字字符的编码值减去
'0'
的编码值,得到的结果恰好就是这个数字的整数值.例如,'5' - '0'
的结果就是整数5
.这比int.Parse(s[i].ToString())
这种先转成字符串再解析的方式要快得多,是性能敏感场景下的首选.
- 是什么: 这是C#(以及C/C++/Java)中一个非常经典和高效的技巧.在内部,
-
三元运算符:
condition ? value_if_true : value_if_false
-
是什么: 这是一个紧凑的
if-else
语句的简写形式. -
在代码中的应用:
sign = (s[i] == '-') ? -1 : 1;
这行代码等价于:if (s[i] == '-') {sign = -1; } else {sign = 1; }
对于这种简单的赋值逻辑,三元运算符能让代码更简洁.
-
知识点总结:
1. 边界条件与防御性编程
- 核心思想: 永远不要相信输入.一个健壮的程序必须能处理各种预料之外的、不规范的、甚至是恶意的输入.
- 在本题中的体现:
- 空字符串或纯空格:
s = ""
或s = " "
. - 非数字开头:
s = "words and 987"
. - 符号位置不正确:
s = " + 42"
vss = "42+"
. - 溢出:
s = "2147483648"
(比int.MaxValue
大1). - 混合输入:
s = " -042a123"
.
- 空字符串或纯空格:
- 通用启示: 在编写任何函数或方法时,都要先思考:“最坏的输入情况是什么?”.在函数开头处理这些边缘情况(如检查
null
或空集合),可以让我们的主逻辑更清晰、更安全.
2. 使用“更大”的数据类型处理溢出
- 核心思想: 当一个计算过程有可能超出某个数值类型的范围时,使用一个范围更大的类型作为“临时计算器”是一种简单而有效的策略.
- 在本题中的体现: 我们使用
long
(64位) 来计算一个最终要存入int
(32位) 的结果.这使得我们可以在不丢失信息的情况下,轻松地将中间结果与int.MaxValue
和int.MinValue
进行比较. - 通用启示: 这个技巧不限于
int
和long
.例如,在处理需要高精度的小数运算时,我们可能会选择使用decimal
类型而不是float
或double
来避免精度损失.在处理图形学或物理计算时,有时为了中间步骤的精度,会使用double
进行计算,最后再将结果转回float
.
3. 状态机思想 (State Machine)
- 核心思想: 将一个复杂的过程分解为一系列离散的、定义清晰的“状态”,以及在这些状态之间转换的“规则”.
- 在本题中的体现: 我们的解析过程可以看作一个简单的状态机:
State_Skipping_Whitespace
(跳过空格状态)State_Determining_Sign
(确定符号状态)State_Reading_Digits
(读取数字状态)State_Finished
(完成状态)
程序根据当前字符,从一个状态转换到下一个状态.
- 通用启示: 状态机是解决许多问题的强大模型,尤其是在游戏开发中.角色的AI(站立、行走、攻击、防御)、UI的交互流程(主菜单、设置、游戏中)、网络协议的握手过程等,都可以用状态机来清晰地建模和实现.
练习题:
选择题
1. 在一个需要返回 int
的函数中,你正在累加一个可能很大的数字.为了防止计算过程中发生溢出,以下哪个是最佳实践?
A. 在每次加法后,检查结果是否变成了负数,如果是,说明溢出了.
B. 使用 try-catch
块包裹加法运算,捕捉 OverflowException
异常.
C. 将累加器变量声明为 long
类型,每次累加后,检查该 long
值是否超出了 int
的范围.
D. 在进行加法前,使用 int.MaxValue - currentValue < numberToAdd
的方式进行预判.
简答题
2. 假设你需要编写一个函数 ParseVector2(string s)
,它能将形如 "(1.5, -2.0)"
的字符串解析为一个 Unity 的 Vector2
对象.请简要描述你会如何运用“状态机”的思想来设计这个函数的解析逻辑?(不需要写代码,描述步骤即可)
参考答案
-
答案:C 或 D 都是优秀的实践,但 C 更为通用和简单.
解析:- A 是不可靠的.对于正数溢出,结果会变成负数,但对于负数下溢,结果可能变成正数,逻辑复杂且容易出错.
- B 在默认的 C# 编译设置下是行不通的.整数溢出默认不会抛出异常(为了性能).你需要使用
checked
关键字才能让它抛出异常,这在性能敏感的循环中可能不是最佳选择. - C (本题解法) 是最直观和简单的方法.它将问题从“如何检测溢出”转变为“如何比较大小”,逻辑清晰.
- D (预判法) 也是一种非常好的、不依赖更大类型的方法,性能很高.它直接在操作前判断本次操作是否会导致溢出.在某些不能使用更大类型的场景下,这是标准做法.
-
答案:
我们可以将解析过程设计为以下几个状态:- 寻找左括号状态: 从头开始,跳过所有空格,直到找到第一个
'('
.如果没找到,则格式错误. - 解析X值状态: 从
'('
之后开始,解析一个浮点数(这本身就可以复用 atoi 的逻辑,但要处理小数点).直到遇到','
字符.将解析出的值存为 X. - 寻找逗号状态: 在解析完 X 后,跳过可能的空格,寻找
','
.如果没找到,则格式错误. - 解析Y值状态: 从
','
之后开始,用与解析 X 值相同的方法解析第二个浮点数.直到遇到')'
字符.将解析出的值存为 Y. - 寻找右括号状态: 在解析完 Y 后,跳过可能的空格,寻找
')'
.如果没找到,则格式错误. - 完成状态: 成功找到所有部分,用解析出的 X 和 Y 构建并返回
new Vector2(x, y)
.
在任何一步失败(比如找不到预期的字符,或数字格式错误),函数都应立即停止并返回一个错误或默认值.
- 寻找左括号状态: 从头开始,跳过所有空格,直到找到第一个
可能的实际应用:
-
配置文件解析: 游戏中的配置文件(
.ini
,.txt
,.json
,.xml
)经常包含需要被读取为数值的设置,如音量大小、图形质量等级、玩家初始生命值等.一个健壮的解析器是读取这些配置的基础. -
自定义编辑器工具: 在Unity Editor中,我们可能会创建一些工具窗口,让策划或美术在输入框里填写数值(例如,批量修改一堆怪物的血量).我们需要将输入框中的字符串安全地转换为整数或浮点数,
atoi
的逻辑是这个过程的核心. -
网络通信: 从服务器接收的数据包可能是以文本协议(如HTTP)传输的.解析协议头或消息体中的数字字段时,就需要用到类似
atoi
的健壮的字符串到数字的转换逻辑. -
调试控制台/GM命令: 游戏内通常会有一个调试控制台,允许开发者输入命令,如
set_player_hp 1000
.解析这个命令中的参数1000
,就需要一个能处理各种输入的atoi
函数.