编译原理没好好听(不如说脑子里没有那种思维),现在要遭重啦!
题目介绍 这道题 要求实现 atoi 函数,它要求将一个字符串转成整型,并且有以下要求——
如果转换失败,返回 0
忽略不限数量的前置零
能判断正负
有效的整数后可能有非法字符,应当能够处理所有有效整数
如果整型溢出,则返回整型的最大值或最小值
比如,对输入“ -42”,返回-42,对输入“ +43hello”,返回 43,对”he is a 5”,返回 0
我的实现——一万个条件判断 这里是我之前写的代码——
首先检查输入,如果输入为空,或者长度为 1,则直接返回结果。
然后排除前置零,找到第一个非零元素。判断该元素是否是+,-或数字。
然后找到数字的第一个有效位置,从此处开始找到数字的最后一个有效位置。
此时,数字的符号,开始位置和结束位置已经决定,下面就不言自明了。
关于溢出的判断,可以看到每次递增都是 res = res * 10 + temp,在计算前对 res 进行判断。
对于正数,需要看是否大于 MAX_VALUE/10,如果大于,则必定越界。如果等于,则判断 temp 是否小于 7(整型的上界的个位是 7),如果 temp 小于 7 则不越界,如果其小于 MAX_VALUE/10 则也不越界。
负数遵循相似的判断方法,如果 temp 小于-8(整型的下界的个位是 8),则必定越界。
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 int myAtoi (String s) { if (s.isEmpty()) return 0 ; else if (s.length()==1 ) { int temp = s.charAt(0 ) - '0' ; if (temp>=0 && temp<=9 ) return temp; return 0 ; } int pointer = 0 ; while (s.charAt(pointer) == ' ' ) { pointer++; if (pointer == s.length()) return 0 ; } char first = s.charAt(pointer); if (!(Character.isDigit(first) || first == '-' || first == '+' )) return 0 ; boolean positive = true ; if (first == '-' ) { positive = false ; pointer++; } else if (first == '+' ) pointer++; if (!Character.isDigit(s.charAt(pointer))) return 0 ; int end; for (end = pointer; end < s.length() && Character.isDigit(s.charAt(end)); end++); end--; int res = 0 ; for (int i = pointer; i <= end; i++) { int temp = s.charAt(i) - '0' ; if (!positive) temp = -temp; if (res > Integer.MAX_VALUE/10 || res == Integer.MAX_VALUE/10 && temp > 7 ) return Integer.MAX_VALUE; if (res < Integer.MIN_VALUE/10 || res == Integer.MIN_VALUE/10 && temp < -8 ) return Integer.MIN_VALUE; res = res * 10 + temp; } return res; }
可以看到,这个实现是非常……臃肿的,代码并不好修改(也不好看),if 和 return 到处都是。虽然我 no bug AC 了,但是这绝对是机缘巧合。这样多分支的玩意出现 bug 轻而易举。
于是去翻了下题解,卧槽?DFA?还有这种操作?原来这种流程复杂的玩意可以用自动机优雅地解决……我是没想到编译原理里的东西可以用到这里……这必须得学习一波了。
DFA 的实现 状态转移图如下,start 为初始,signed 为判断正负,in_number 则是正在处理有效数字,end 为转换失败或转换完成
然后贴上状态转移表。这个表描述任意条件下遇到各种字符的处理方法。比如在 start 条件下,遇到 number 则跳转到 in_number 下
‘ ‘
+/-
number
other
start
start
signed
in_number
end
signed
end
end
in_number
end
in_number
end
end
in_number
end
end
end
end
end
end
总之先贴上题解的 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 INT_MAX = 2 ** 31 - 1 INT_MIN = -2 ** 31 class Automaton : def __init__ (self ): self.state = 'start' // 代表当前状态 self.sign = 1 // 代表符号 self.ans = 0 // 代表结果 self.table = { // 状态转移表 'start' : ['start' , 'signed' , 'in_number' , 'end' ], 'signed' : ['end' , 'end' , 'in_number' , 'end' ], 'in_number' : ['end' , 'end' , 'in_number' , 'end' ], 'end' : ['end' , 'end' , 'end' , 'end' ], } def get_col (self, c ): // 根据字符来获取状态转移表的列 if c.isspace(): return 0 if c == '+' or c == '-' : return 1 if c.isdigit(): return 2 return 3 def get (self, c ): // 根据字符进行操作。注意,是先更改状态,再进行操作 self.state = self.table[self.state][self.get_col(c)] if self.state == 'in_number' : self.ans = self.ans * 10 + int (c) self.ans = min (self.ans, INT_MAX) if self.sign == 1 else min (self.ans, -INT_MIN) elif self.state == 'signed' : self.sign = 1 if c == '+' else -1 class Solution : def myAtoi (self, str : str ) -> int : automaton = Automaton() for c in str : // 遍历字符串 automaton.get(c) return automaton.sign * automaton.ans
下面默写一次 java 版本!
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 class Automation { enum State { START, SIGNED, IN_NUMBER, END } int sign = 1 ; public int res = 0 ; State state; HashMap<State, State[]> table; public Automation () { state = State.START; table = new HashMap <State, State[]>(); table.put(State.START, new State [] { State.START, State.SIGNED, State.IN_NUMBER, State.END }); table.put(State.SIGNED, new State [] {State.END, State.END, State.IN_NUMBER, State.END}); table.put(State.IN_NUMBER, new State [] {State.END, State.END, State.IN_NUMBER, State.END}); table.put(State.END, new State [] { State.END, State.END, State.END, State.END }); } private int get_col (char c) { if (c == ' ' ) return 0 ; if (c == '+' || c == '-' ) return 1 ; if (Character.isDigit(c)) return 2 ; return 3 ; } public void get (char c) { state = table.get(state)[get_col(c)]; if (state == State.SIGNED) sign = c=='+' ? 1 : -1 ; if (state == State.IN_NUMBER) { int temp = (c - '0' ) * sign; if (res > Integer.MAX_VALUE/10 || res == Integer.MAX_VALUE/10 && temp > 7 ) { res = Integer.MAX_VALUE; state = State.END; return ; } if (res < Integer.MIN_VALUE/10 || res == Integer.MIN_VALUE/10 && temp < -8 ) { res = Integer.MIN_VALUE; state = State.END; return ; } res = res * 10 + temp; } } }public class Solution { public static int myAtoi (String s) { Automation automation = new Automation (); for (char c: s.toCharArray()) automation.get(c); return automation.res; } }
总之学习了!
举一反三——isValidNumber 不实践,如何敢说自己掌握了?
这一题 ,要求判断一个数字字符串是否为合法数字。合法字符串应当只包括如下内容(以及前置后置空格)——
数字 0 到 9
指数 e (科学计数法的那个玩意)
正负号-/+
小数点。
数字可以分为三个部分——数字,指数符号 e,指数,其中数字可以有正负和小数点,如-43.2。指数是可选的,但是如果有 e,则必须要有指数。其中指数必须为整数,可以有正负号,比如-43.2e-10…
然而题目告诉我,像。1 这样的也合法,wtf?
于是,容易画出状态转移图——
不画了,有点麻烦(没有舒服的软件可用)……草稿纸上手画后直接写状态转移表(这个有问题)——
WHITESPACE
+/-
number
.
e
other
START
START
SIGN
INTEGER
DECIMAL_POINT
ERROR
ERROR
SIGN
ERROR
ERROR
INTEGER
DECIMAL_POINT
ERROR
ERROR
INTEGER
END
ERROR
INTEGER
DECIMAL_POINT
E
ERROR
DECIMAL_POINT
END
ERROR
DECIMAL
ERROR
E
ERROR
DECIMAL
END
ERROR
DECIMAL
ERROR
E
ERROR
E
ERROR
E_SIGN
E_NUMBER
ERROR
ERROR
ERROR
E_SIGN
ERROR
ERROR
E_NUMBER
ERROR
ERROR
ERROR
E_NUMBER
END
ERROR
E_NUMBER
ERROR
ERROR
ERROR
END
END
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
这里需要注意的是,-.1e10 是合法的,-1.e10 是合法的,但是-.e10 是不合法的,1. 是合法的,.1 是合法的,但。是不合法的,因此显然需要保存是否已经检测到数字……
不过可以定两个小数点的状态,让 start->dot->digit 和 start->digit->dot 两条路径不经过同一个 dot 状态,就如下面官方题解给出的状态转移表……这样也不需要做多余判定了。两个状态可以叫做 POINT_WITH_INT 和 POINT_WITHOUT_INT
下面是实现——
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 class Automation { enum State { START, SIGN, INTEGER, DECIMAL_POINT, DECIMAL, E, E_SIGN, E_NUMBER, END, ERROR } boolean flag; State state; HashMap<State, State[]> table; public Automation () { state = State.START; flag = false ; table = new HashMap <State, State[]>(); table.put(State.START, new State [] { State.START, State.SIGN, State.INTEGER, State.DECIMAL_POINT, State.ERROR, State.ERROR }); table.put(State.SIGN, new State [] { State.ERROR, State.ERROR, State.INTEGER, State.DECIMAL_POINT, State.ERROR, State.ERROR }); table.put(State.INTEGER, new State [] { State.END, State.ERROR, State.INTEGER, State.DECIMAL_POINT, State.E, State.ERROR }); table.put(State.DECIMAL_POINT, new State [] { State.END, State.ERROR, State.DECIMAL, State.ERROR, State.E, State.ERROR }); table.put(State.DECIMAL, new State [] { State.END, State.ERROR, State.DECIMAL, State.ERROR, State.E, State.ERROR }); table.put(State.E, new State [] { State.ERROR, State.E_SIGN, State.E_NUMBER, State.ERROR, State.ERROR, State.ERROR }); table.put(State.E_SIGN, new State [] { State.ERROR, State.ERROR, State.E_NUMBER, State.ERROR, State.ERROR, State.ERROR }); table.put(State.E_NUMBER, new State [] { State.END, State.ERROR, State.E_NUMBER, State.ERROR, State.ERROR, State.ERROR }); table.put(State.END, new State [] { State.END, State.ERROR, State.ERROR, State.ERROR, State.ERROR, State.ERROR }); table.put(State.ERROR, new State [] { State.ERROR, State.ERROR, State.ERROR, State.ERROR, State.ERROR, State.ERROR }); } private int get_col (char c) { if (c == ' ' ) return 0 ; if (c == '+' || c == '-' ) return 1 ; if (Character.isDigit(c)) return 2 ; if (c == '.' ) return 3 ; if (c == 'e' ) return 4 ; return 5 ; } public void get (char c) { state = table.get(state)[get_col(c)]; if (state == State.INTEGER || state == State.DECIMAL) flag = true ; } public boolean isValid () { return flag && ( state == State.INTEGER || state == State.DECIMAL_POINT || state == State.DECIMAL || state == State.E_NUMBER || state == State.END); } }class Solution { public boolean isNumber (String s) { Automation automation = new Automation (); for (char c : s.toCharArray()) automation.get(c); return automation.isValid(); } }
然后看题解,wtf?路漫漫其修远兮啊……
舒服多了,也不需要一个额外的 bool 值来让代码变丑,最终也只需要判断最后是不是状态 3,5,6,8(8 由状态 3,5,6 跳转,负责判断有没有后置 0)……
下面重写一次状态转移表,就不再实现了。
space
+/-
number
.
e
other
START
START
SIGN
NUMBER
DOT_WITHOUT_INT
ERROR
ERROR
SIGN
ERROR
ERROR
NUMBER
DOT_WITHOUT_INT
ERROR
ERROR
NUMBER
END
END
NUMBER
DOT_WITH_INT
E
ERROR
DOT_WITH_INT
END
ERROR
NUMBER
ERROR
E
ERROR
DOT_WITHOUT_INT
ERROR
ERROR
NUMBER
ERROR
ERROR
ERROR
E
ERROR
E_SIGN
E_NUMBER
ERROR
ERROR
ERROR
E_SIGN
ERROR
ERROR
E_NUMBER
ERROR
ERROR
ERROR
E_NUMBER
END
ERROR
E_NUMBER
ERROR
ERROR
ERROR
END
END
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
ERROR
NUMBER,DOT_WITHOUT_INT,E_NUMBER 和 END 是终结状态,如果最终自动机处于这几个状态,则识别成功。