本文始發於個人公眾號:TechFlow,原創不易,求個關注
今天是LeetCode專題的第38篇文章,我們一起來看看第65題,Valid Number。
曾經我們聊到過算法當中的一個類別——模擬題。所謂的模擬題就是題面非常簡單,也不涉及任何復雜的算法,但是要實現的功能比較麻煩,非常考驗人思維的縝密程度,很難寫出bug-free的代碼來。今天要說的65題可以說是其中的典范,它的題面非常簡單,簡單到只有一句話,但是要實現非常麻煩,比較鍛煉人的耐心,以至於反對的人是點贊的9倍,我們一起來看看。
題面
給定一個字符串,判斷它是否是一個合法的浮點數。
從題面來看只有一句話,似乎非常簡單。但是實際上如果你仔細研究一下樣例和提示,會發現事情可能和你想的不太一樣。
樣例
"0" => true
" 0.1 " => true
"abc" => false
"1 a" => false
"2e10" => true
" -90e3 " => true
" 1e" => false
"e3" => false
" 6e-1" => true
" 99e2.5 " => false
"53.5e93" => true
" --6 " => false
"-+3" => false
"95a54e53" => false
提示
我們有意將問題陳述地比較模糊。在實現代碼之前,你應當事先思考所有可能的情況。這里給出一份可能存在於有效十進制數字中的字符列表:
數字 0-9
指數 - "e"
正/負號 - "+"/"-"
小數點 - "."
當然,在輸入中,這些字符的上下文也很重要。
不知道大家感受到了沒有,這其中的情況不少,涉及的符號就很多。除了基本的數字之外,還有小數點、空格、正負號、e。這些符號帶來的合法和非法的情況都很多,更何況這些符號之間還可以互相組合,又會引申出新的情況。
更坑爹的一點是,這題簡單粗暴,只有這一個解法,我們別無選擇,只有覆蓋所有的情況才能通過。所以如果你試着去想清楚所有的這些情況,你會發現這是非常困難的一件事,甚至可能會越想情況越多,覺得怎么也理不清楚,即使你理出了很多情況,也不知道是否有遺漏,很容易讓人抓狂並且心煩氣躁,明明很簡單的問題做不出來,感受肯定不好。先別着急,這正是模擬題考驗人的地方,它考驗的不僅是思維,也是心態。
所以,先深吸一口氣,冷靜下來,我們仔細地分析一下這些情況。
解法
我們從列舉所有的情況入手是非常困難的,因為符號之間互相組合的情況實在是很多,一一列舉全並且用代碼實現的代價很大。我們先把這些煩人的情況放在一邊,我們先來思考一下問題所在。
這個問題的核心就是判斷浮點數是否合法,合法的浮點數的情況也是不少的,但這些情況雖然多,彼此之間是有聯系的。最起碼我們可以發現符號之間的順序,如果我們把一個合法的浮點數進行拆分,它大概可以分成以下幾個部分。
首先是符號位,表示這個數是正數還是負數。題目當中沒有明說,但是我們可以猜測出來,正數用正號表示也是合法的。
第二個部分是科學記數法的前半部分,它可以是一個小數。
第三個部分是e,即科學記數法當中的e。
最后一個部分是整數部分,表示e的指數,根據科學記數法的定義,必然是一個整數。但是可以是負數。
當我們把這四個部分列舉出來之后,再來進行判斷就容易多了。因為這四個部分是有順序的,我們只需要判斷它們順序的合理性就可以了。根據順序的合理性,我們可以進一步推測出每一個符號允許出現的位置,所有和預期位置不符的符號都是非法的。
根據這一點我們可以推導出一些結論:
-
空格只能出現在首尾,出現在中間一定是非法的。 -
正負號只能出現在兩個地方,第一個地方是數字的最前面,表示符號。第二個位置是e后面,表示指數的正負。如果出現在其他的位置一定也是非法的。 -
數字,數字沒有特別的判斷,本題當中沒有前導0的問題。 -
e只能出現一次,並且e之后一定要有數字才是合法的,123e這種也是非法的。 -
小數點,由於e之后的指數一定是整數,所以小數點最多只能出現一次,並且一定要在e之前。所以如果之前出現過小數點或者是e,再次出現小數點就是非法的。
當我們把每一個符號合法的情況梳理清楚之后,會發現其實也沒有那么復雜,情況也沒有那么多。這其實也是常用套路,我們把互相耦合的一些變量拆分開了,彼此互不影響。這樣我們就可以單獨考慮這其中的每個零件,而不用面對它們互相耦合的復雜情況了。
我們把剛才梳理出來的全部用代碼實現就可以通過這題了:
class Solution:
def isNumber(self, s: str) -> bool: s = s.strip() numbers = [str(i) for i in range(10)] n = len(s) # 用4個標記記錄e和小數點以及數字和e之后的數字有無出現過 e_show_up, dot_show_up, num_show_up, num_after_e = False, False, False, False for i in range(n): c = s[i] # 如果是數字,則將數字和e后出現的數字都標記為true # 沒有e的浮點數也認為e之后出現過數字 if c in numbers: num_show_up = True num_after_e = True # 如果是正負號,只有出現在首位或者是e后面才是合法 elif c in ('+', '-'): if i > 0 and s[i-1] != 'e': return False # 如果是小數點,那么必須保證e和小數點都沒有出現過 elif c == '.': if dot_show_up or e_show_up: return False dot_show_up = True # 如果是e,要保證已經有數字出現,並且e沒有出現過 elif c == 'e': if e_show_up or not num_show_up: return False e_show_up = True num_show_up = False # 其他情況都視為非法 else: return False return num_show_up and num_after_e
總結
這題我們看代碼好像也不復雜,但是想要把這么多條件都梳理清楚,寫出這樣簡單的代碼也不是一件容易的事情。必須建立在我們對問題有了充分的思考的基礎上,其實我們的代碼還疏漏了一個條件就是前導零的情況。如果0出現在數字最前面其實也是非法的,不過這題當中沒有針對這樣的case,但實際上我們是應該考慮的,這里也是我偷懶了。
很多人很討厭模擬題,包括我在內,原因就是情況太多很惡心人,經常會中招遺漏一些情況。看看這題的評分也能看得出來,點贊的只有678,反對的卻又4572,可見一斑。但其實模擬題也是一種對思維的鍛煉,需要我們有冷靜的思維和理智的分析,這也是一個優秀的選手必不可少的。希望大家都能攻克這道難關。
今天的文章就到這里,原創不易,掃碼關注我,獲取更多精彩文章。