力扣刷題日常(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
函數.