数据结构与算法之深入解析“有效数字”的求解思路与算法示例
Posted Serendipity·y
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“有效数字”的求解思路与算法示例相关的知识,希望对你有一定的参考价值。
一、题目要求
- 有效数字(按顺序)可以分成以下几个部分:
-
- 一个小数或者整数;
-
- (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个 整数;
- 小数(按顺序)可以分成以下几个部分:
-
- (可选)一个符号字符(’+’ 或 ‘-’);
-
- 下述格式之一:
-
-
- 至少一位数字,后面跟着一个点 ‘.’;
-
-
-
- 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字;
-
-
-
- 一个点 ‘.’ ,后面跟着至少一位数字。
-
- 整数(按顺序)可以分成以下几个部分:
-
- (可选)一个符号字符(’+’ 或 ‘-’);
-
- 至少一位数字;
- 部分有效数字列举如下:
["2", "0089", "-0.1", "+3.14", "4.", "-.9", "2e10", "-90E3", "3e+7", "+6e-1", "53.5e93", "-123.456e789"]
- 部分无效数字列举如下:
["abc", "1a", "1e", "e3", "99e2.5", "--6", "-+3", "95a54e53"]
- 给你一个字符串 s ,如果 s 是一个 有效数字 ,请返回 true。
- 示例 1:
输入:s = "0"
输出:true
- 示例 2:
输入:s = "e"
输出:false
- 示例 3:
输入:s = "."
输出:false
- 示例 4:
输入:s = ".1"
输出:true
- 提示:
-
- 1 <= s.length <= 20;
-
- s 仅含英文字母(大写和小写),数字(0-9),加号 ‘+’ ,减号 ‘-’ ,或者点 ‘.’ 。
二、“有限状态自动机”简介
- 确定有限状态自动机(以下简称「自动机」)是一类计算模型。它包含一系列状态,这些状态中:
-
- 有一个特殊的状态,被称作「初始状态」。
-
- 还有一系列状态被称为「接受状态」,它们组成了一个特殊的集合。其中,一个状态可能既是「初始状态」,也是「接受状态」。
- 起初,这个自动机处于「初始状态」。随后,它顺序地读取字符串中的每一个字符,并根据当前状态和读入的字符,按照某个事先约定好的「转移规则」,从当前状态转移到下一个状态;当状态转移完成后,它就读取下一个字符。当字符串全部读取完毕后,如果自动机处于某个「接受状态」,则判定该字符串「被接受」;否则,判定该字符串「被拒绝」。
- 注意:如果输入的过程中某一步转移失败了,即不存在对应的「转移规则」,此时计算将提前中止。在这种情况下也判定该字符串「被拒绝」。
- 一个自动机,总能够回答某种形式的「对于给定的输入字符串 S,判断其是否满足条件 P」的问题。在本题中,条件 P 即为「构成合法的表示数值的字符串」。
- 自动机驱动的编程,可以被看做一种暴力枚举方法的延伸:它穷尽了在任何一种情况下,对应任何的输入,需要做的事情。
- 自动机在计算机科学领域有着广泛的应用。在算法领域,它与大名鼎鼎的字符串查找算法「KMP 算法」有着密切的关联;在工程领域,它是实现「正则表达式」的基础。
- 在 C++文档 中,描述了一个合法的数值字符串应当具有的格式。具体而言,它包含以下部分:
-
- 符号位,即 +、− 两种符号;
-
- 整数部分,即由若干字符 0−9 组成的字符串;
-
- 小数点;
-
- 小数部分,其构成与整数部分相同;
-
- 指数部分,其中包含开头的字符 e(大写小写均可)、可选的符号位,和整数部分;
- 在上面描述的五个部分中,每个部分都不是必需的,但也受一些额外规则的制约,如:
-
- 如果符号位存在,其后面必须跟着数字或小数点;
-
- 小数点的前后两侧,至少有一侧是数字。
三、求解算法
① 有限状态机(DFA)
- 构造一个 DFA 并实现,构造方法可以先写正则表达式,然后转为 DFA,也可以直接写,虽然大概率不会是最简结构,不过不影响解题。DFA 作为确定的有限状态机,比 NFA 更加实用,因为对于每一个状态接收的下一个字符,DFA 能确定唯一一条转换路径,所以使用简单的表驱动的一些方法就可以实现,并且只需要读一遍输入流,比起 NFA 需要回读在速度上会有所提升。
- 构建出来的状态机如封面图片所示(红色为 终止状态,蓝色为 中间状态),DFA 从状态 0 接受串 s 作为输入,当 s 耗尽的时候如果当前状态处于中间状态,则拒绝;如果到达终止状态,则接受:
- 然后,根据 DFA 列出如下的状态跳转表,之后就可以采用“表驱动法”进行编程实现。需要注意的是,这里面多了一个状态 8,是用于处理串后面的若干个多余空格的。所以,所有的终止态都要跟上一个状态 8。其中,有一些状态标识为 -1,是表示遇到了一些意外的字符,可以直接停止后续的计算。状态跳转表如下:
- Java 示例:
class Solution
public int make(char c)
switch(c)
case ' ': return 0;
case '+':
case '-': return 1;
case '.': return 3;
case 'e': return 4;
default:
if(c >= 48 && c <= 57) return 2;
return -1;
public boolean isNumber(String s)
int state = 0;
int finals = 0b101101000;
int[][] transfer = new int[][] 0, 1, 6, 2,-1,
-1,-1, 6, 2,-1,
-1,-1, 3,-1,-1,
8,-1, 3,-1, 4,
-1, 7, 5,-1,-1,
8,-1, 5,-1,-1,
8,-1, 6, 3, 4,
-1,-1, 5,-1,-1,
8,-1,-1,-1,-1;
char[] ss = s.toCharArray();
for(int i=0; i < ss.length; ++i)
int id = make(ss[i]);
if (id < 0) return false;
state = transfer[state][id];
if (state < 0) return false;
return (finals & (1 << state)) > 0;
② 确定有限状态自动机(LeetCode 官方解法)
- 根据上面的描述,现在可以定义自动机的「状态集合」了,那么怎么挖掘出所有可能的状态呢?一个常用的技巧是,用「当前处理到字符串的哪个部分」当作状态的表述。根据这一技巧,不难挖掘出所有状态:
-
- 初始状态;
-
- 符号位;
-
- 整数部分;
-
- 左侧有整数的小数点;
-
- 左侧无整数的小数点(根据前面的第二条额外规则,需要对左侧有无整数的两种小数点做区分);
-
- 小数部分;
-
- 字符 e;
-
- 指数部分的符号位;
-
- 指数部分的整数部分;
- 下一步是找出「初始状态」和「接受状态」的集合。根据题意,「初始状态」应当为状态 0,而「接受状态」的集合则为状态 2、状态 3、状态 5 以及状态 8。换言之,字符串的末尾要么是空格,要么是数字,要么是小数点,但前提是小数点的前面有数字。
- 最后,需要定义「转移规则」,结合数值字符串应当具备的格式,将自动机转移的过程以图解的方式表示出来:
- 比较上图与上文中中对自动机的描述,可以看出有一点不同:我们没有单独地考虑每种字符,而是划分为若干类。由于全部 10 个数字字符彼此之间都等价,因此只需定义一种统一的「数字」类型即可。对于正负号也是同理。
- 在实际代码中,我们需要处理转移失败的情况。为了处理这种情况,可以创建一个特殊的拒绝状态,如果当前状态下没有对应读入字符的「转移规则」,就转移到这个特殊的拒绝状态;一旦自动机转移到这个特殊状态,就可以立即判定该字符串不「被接受」。
- Java 示例:
class Solution
public boolean isNumber(String s)
Map<State, Map<CharType, State>> transfer = new HashMap<State, Map<CharType, State>>();
Map<CharType, State> initialMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
put(CharType.CHAR_SIGN, State.STATE_INT_SIGN);
;
transfer.put(State.STATE_INITIAL, initialMap);
Map<CharType, State> intSignMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
;
transfer.put(State.STATE_INT_SIGN, intSignMap);
Map<CharType, State> integerMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_POINT, State.STATE_POINT);
;
transfer.put(State.STATE_INTEGER, integerMap);
Map<CharType, State> pointMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
;
transfer.put(State.STATE_POINT, pointMap);
Map<CharType, State> pointWithoutIntMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
;
transfer.put(State.STATE_POINT_WITHOUT_INT, pointWithoutIntMap);
Map<CharType, State> fractionMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
;
transfer.put(State.STATE_FRACTION, fractionMap);
Map<CharType, State> expMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
put(CharType.CHAR_SIGN, State.STATE_EXP_SIGN);
;
transfer.put(State.STATE_EXP, expMap);
Map<CharType, State> expSignMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
;
transfer.put(State.STATE_EXP_SIGN, expSignMap);
Map<CharType, State> expNumberMap = new HashMap<CharType, State>()
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
;
transfer.put(State.STATE_EXP_NUMBER, expNumberMap);
int length = s.length();
State state = State.STATE_INITIAL;
for (int i = 0; i < length; i++)
CharType type = toCharType(s.charAt(i));
if (!transfer.get(state).containsKey(type))
return false;
else
state = transfer.get(state).get(type);
return state == State.STATE_INTEGER || state == State.STATE_POINT || state == State.STATE_FRACTION || state == State.STATE_EXP_NUMBER || state == State.STATE_END;
public CharType toCharType(char ch)
if (ch >= '0' && ch <= '9')
return CharType.CHAR_NUMBER;
else if (ch == 'e' || ch == 'E')
return CharType.CHAR_EXP;
else if (ch == '.')
return CharType.CHAR_POINT;
else if (ch == '+' || ch == '-')
return CharType.CHAR_SIGN;
else
return CharType.CHAR_ILLEGAL;
enum State
STATE_INITIAL,
STATE_INT_SIGN,
STATE_INTEGER,
STATE_POINT,
STATE_POINT_WITHOUT_INT,
STATE_FRACTION,
STATE_EXP,
STATE_EXP_SIGN,
STATE_EXP_NUMBER,
STATE_END
enum CharType
CHAR_NUMBER,
CHAR_EXP,
CHAR_POINT,
CHAR_SIGN,
CHAR_ILLEGAL
- C++ 示例:
class Solution
public:
enum State
STATE_INITIAL,
STATE_INT_SIGN,
STATE_INTEGER,
STATE_POINT,
STATE_POINT_WITHOUT_INT,
STATE_FRACTION,
STATE_EXP,
STATE_EXP_SIGN,
STATE_EXP_NUMBER,
STATE_END
;
enum CharType
CHAR_NUMBER,
CHAR_EXP,
CHAR_POINT,
CHAR_SIGN,
CHAR_ILLEGAL
;
CharType toCharType(char ch)
if (ch >= '0' && ch <= '9')
return CHAR_NUMBER;
else if (ch == 'e' || ch == 'E')
return CHAR_EXP;
else if (ch == '.')
return CHAR_POINT;
else if (ch == '+' || ch == '-')
return CHAR_SIGN;
else
return CHAR_ILLEGAL;
bool isNumber(string s)
unordered_map<State, unordered_map<CharType, State>> transfer
STATE_INITIAL,
CHAR_NUMBER, STATE_INTEGER,
CHAR_POINT, STATE_POINT_WITHOUT_INT,
CHAR_SIGN, STATE_INT_SIGN
,
STATE_INT_SIGN,
CHAR_NUMBER, STATE_INTEGER,
CHAR_POINT, STATE_POINT_WITHOUT_INT
,
STATE_INTEGER,
CHAR_NUMBER, STATE_INTEGER,
CHAR_EXP, STATE_EXP,
CHAR_POINT, STATE_POINT
,
STATE_POINT,
CHAR_NUMBER, STATE_FRACTION,
CHAR_EXP, STATE_EXP
,
STATE_POINT_WITHOUT_INT,
CHAR_NUMBER, STATE_FRACTION
,
STATE_FRACTION,
CHAR_NUMBER, STATE_FRACTION,
CHAR_EXP, STATE_EXP
,
STATE_EXP,
CHAR_NUMBER, STATE_EXP_NUMBER,
CHAR_SIGN, STATE_EXP_SIGN
,
STATE_EXP_SIGN,
CHAR_NUMBER, STATE_EXP_NUMBER
,
STATE_EXP_NUMBER,
CHAR_NUMBER, STATE_EXP_NUMBER
;
int len = s.length();
State st = STATE_INITIAL;
for (int i = 0数据结构与算法之深入解析“股票的最大利润”的求解思路与算法示例