基于编译原理的有限状态机思想和正则匹配都可以解这道题,如果要用《剑指offer》的方法解是无法通过LeetCode设计的那些杂七杂八的特殊测试样例的。常规解法解这个题,就是在没事找事做!
题目
验证给定的字符串是否可以解释为十进制数字。
说明: 我们有意将问题陈述地比较模糊。在实现代码之前,你应当事先思考所有可能的情况。这里给出一份可能存在于有效十进制数字中的字符列表:
数字 0-9
指数 - "e"
正/负号 - "+"/"-"
小数点 - "."
当然,在输入中,这些字符的上下文也很重要。
测试用例
- 功能测试(正数或者负数;包含或者不包含整数部分的数值;包含或者不包含小数部分的数值;包含或者不包含指数部分的数值;各种不能表达有效数值的字符串)。
- 特殊测试(输入字符串和模式字符串是
nullptr
、空字符串)。
考点
考查应聘者对字符串的编程能力。 考查应聘者分析问题的能力。面试官希望应聘者能够从不同类型的数值中分析出规律。 考查应聘者思维的全面性。应聘者要全面考虑数值整数、小数、指数部分的特点,比如哪些部分可以出现正负号,而哪些部分不能出现。在应聘者写完代码之后,面试官会期待应聘者能够完备地测试用例来验证自己的代码。
示例
例如: | "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
|
考察知识点
字符串、基于编译原理的有限状态机
核心思想
直接判断
剑指offer的思路 可以把符合要求的数字归纳为 A[.[B]][e|E C] 的模式 A是整数部分,非必须,是以+/-开头的 '0' 到 '9' 的字符串。 B是小数部分,是无符号的 '0' 到 '9' 的字符串。 C是指数部分,是以+/-开头的 '0' 到 '9' 的字符串。 按照上述思路,按照整数部分、小数部分、指数部分依次处理即可。
LeetCode设置测的特殊测试样例很多,如果按照剑指offer上的做法写,通过不了,还是要做很多修改,详见代码中的测试部分和逻辑部分,添加了很多特判。
限状态机
思想
从 0 开始总共有 9 个状态,橙色代表可接受状态,也就是表示此时是合法数字,橙色代表终止状态。总共有四大类输入,数字,小数点,e 和 正负号。DFA 从状态 0 接受串 s 作为输入。当s耗尽的时候如果当前状态处于中间状态,则拒绝;如果到达终止状态,则接受。我们只需要将这个图实现就够了(下图和代码不匹配,仅供参考)。
76e2e607a9a9ea2f10b21da715ac09ea7a3c7ca6a571e75b0fab71deb204bef4-image.png
代码:
- 画出 状态转移表 ,结构为 states[n]
存储 n
个状态; - states[i]
为一个哈希表,表示从当前状态允许跳转到的下一个状态。 - 主循环中遍历字符串,通过状态转移表判断结构是否成立: - 若中途遇到无法跳转的状态,直接返回 False
; - 若成功遍历完字符串,要判断结束状态是否在允许的结束状态内,代码实现为 [2, 3, 7, 8]
。
正则匹配
Python的正则表达式
^[+-]?(\.\d+|\d+\.?\d*)([eE][+-]?\d+)?$
Python版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| from typing import List
class Solution: def isNumber(self, s: str) -> bool: s = s.strip() ascii_a = ord('a') ascii_z = ord('z') ascii_A = ord('A') ascii_Z = ord('Z') ascii_e = ord('e') ascii_0 = ord('0') ascii_9 = ord('9') has_number = False point_count = 0 e_count = 0 for index in range(len(s)): char = s[index] if ascii_0 <= ord(char) <= ascii_9: has_number = True continue if char == ' ': return False if ord(char) == ascii_e: e_count += 1 if index == 0 or index == len(s)-1 or "." in s[index:]: return False if index+1 == len(s)-1 and (s[index+1] == "+" or s[index+1] == "-"): return False if ascii_a <= ord(char) <= ascii_z and ord(char) != ascii_e: return False if ascii_A <= ord(char) <= ascii_Z: return False if char == '-' or char == '+': if index == len(s)-1: return False if 'e' not in s and index != 0: return False if index+2 <= len(s) and (s[index:index+2] == '--' or s[index:index+2] == '++' or s[index:index+2] == '+-' or s[index:index+2] == '-+' or s[index:index+2] == '-e' or s[index:index+2] == '+e'): return False if char == '.': point_count += 1 if index+2 <= len(s) and s[index:index+2] == '..': return False if index+2 <= len(s) and s[index:index+2] == '.e' and index == 0: return False
if (point_count != 0 and point_count != 1) or (e_count != 0 and e_count!= 1): return False
return True if has_number else False
print("leet code accept!!!") Input = ["459277e38+", "6ee69", "5-e95", "4e+", "46.e3", "6+1", "Re7", ".1.", ".e1", "..2", ".", "0", " 0.1 ", "abc", "1 a", "2e10", " -90e3 ", " 1e", "e3", " 6e-1", " 99e2.5 ", "53.5e93", " --6 ", "-+3", "95a54e53"] Input1 = [2, 3] Answer = [False, False, False, False, True, False, False, False, False, False, False, True, True, False, False, True, True, False, False, True, False, True, False, False, False]
if __name__ == "__main__": solution = Solution() for i in range(len(Input)): print("-"*50) reslut = solution.isNumber(Input[i]) if reslut == Answer[i]: print(True) else: print(False) print(Input[i])
|
时间复杂度:\(O(n)\) n为输入字符串的长度
空间复杂度:\(O(1)\) 没有使用额外的空间
执行用时 :52 ms, 在所有 Python3 提交中击败了23.31%的用户
内存消耗 :13.4 MB, 在所有 Python3 提交中击败了5.56%的用户
一种更加简洁的写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution: def isNumber(self, s: str) -> bool: s = s.strip() if not s : return False else: if s[0] in ['+', '-']: s = s[1:] if 'e' in s: temp_list = s.split('e') if len(temp_list) > 2: return False temp_list[0] = temp_list[0].replace('.', '', 1) if len(temp_list[1]) > 0 and temp_list[1][0] in ['+', '-']: temp_list[1] = temp_list[1].replace(temp_list[1][0], '', 1) if temp_list[0].isnumeric() and temp_list[1].isnumeric(): return True return False else: s = s.replace('.', '', 1) if s.isnumeric(): return True return False
|
执行用时 :40 ms, 在所有 Python3 提交中击败了61.85%的用户
内存消耗 :13.4 MB, 在所有 Python3 提交中击败了5.56%的用户
剑指offer思路的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| import unittest from typing import List
class Solution: def isNumber(self, s: str) -> bool: s = s.strip(' ')
if not s: return False if s == '.': return False _str = [char.lower() for char in s] if 'e' in _str: index_e = _str.index('e') integer = _str[:index_e] exponent = _str[index_e+1:] if '.' in exponent or 'e' in exponent or len(integer) == 0 or len(exponent) == 0: return False if len(exponent) == 1 and exponent[0] in ['+', '-']: return False else: integer = _str exponent = ''
if '.' in integer: index_dot = integer.index('.') decimals = integer[index_dot+1:] integer = integer[:index_dot] if '.' in decimals: return False else: decimals = ''
is_valid = False if integer: is_valid = self.scan_digit(integer) if decimals: is_valid = (is_valid or self.scan_unsgin_digit(decimals)) if len(integer) == 0 else (is_valid and self.scan_unsgin_digit(decimals)) else: if len(integer) == 1 and integer[0] in ['+', '-']: return False
if is_valid and exponent: is_valid = is_valid and self.scan_digit(exponent)
return is_valid
def scan_digit(self, number): if number[0] in ['+', '-']: number = number[1:] return self.scan_unsgin_digit(number)
def scan_unsgin_digit(self, number): dot_count = 0 allow_values = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] for i in range(len(number)): if number[i] not in allow_values: return False if number[i] == '.': dot_count += 1 if number[i] in ['+', '-'] and i != 0: return False if dot_count > 1: return False return True
class TestSolution(unittest.TestCase): def setUp(self): self.test_class = Solution()
def test_solution(self): inputs = [" -.", "+.8", "4e+", ".1", "i.1", "+-.", "6ee", ".e1", "..", "0e", "1 "] answers = [ False, True, False, True, False, False, False, False, False, False, True ] res = [] for i in range(len(inputs)): res.append(self.test_class.isNumber(inputs[i])) self.assertEqual(res, answers) def tearDown(self): del self.test_class
if __name__ == "__main__": unittest.main()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution: def isNumber(self, s: str) -> bool: states = [ { 'b': 0, 's': 1, 'd': 2, '.': 4 }, { 'd': 2, '.': 4 } , { 'd': 2, '.': 3, 'e': 5, 'b': 8 }, { 'd': 3, 'e': 5, 'b': 8 }, { 'd': 3 }, { 's': 6, 'd': 7 }, { 'd': 7 }, { 'd': 7, 'b': 8 }, { 'b': 8 } ] p = 0 for c in s: if '0' <= c <= '9': typ = 'd' elif c == ' ': typ = 'b' elif c == '.': typ = '.' elif c == 'e': typ = 'e' elif c in "+-": typ = 's' else: typ = '?' if typ not in states[p]: return False p = states[p][typ] return p in [2, 3, 7, 8]
|
时间复杂度:\(O(n)\)
执行用时 :48 ms, 在所有 Python3 提交中击败了30.86%的用户
内存消耗 :13.4 MB, 在所有 Python3 提交中击败了5.56%的用户
| import re class Solution: p = re.compile(r'^[+-]?(\.\d+|\d+\.?\d*)([eE][+-]?\d+)?$') def isNumber(self, s: str) -> bool: return bool(self.p.match(s.strip()))
|
执行用时 :32 ms, 在所有 Python3 提交中击败了93.62%的用户
内存消耗 :13.6 MB, 在所有 Python3 提交中击败了5.56%的用户
| class Solution: def isNumber(self, s: str) -> bool: try: key=float(s) return True except: return False
|
执行用时 :36 ms, 在所有 Python3 提交中击败了82.16%的用户
内存消耗 :13.6 MB, 在所有 Python3 提交中击败了5.56%的用户
参考连接
力扣(LeetCode)jyd
力扣(LeetCode)erik_chen