写个解析器组合子解决 Valid Number Problem

LeetCode 有一道叫 Valid Number 的题,题目很简单,就是判断一个字符串是不是合法数字。

Valid Number

有趣的是,这道题是 Hard 级别,总的提交次数有70多万次,最后通过的只有9万,12.9%的通过率。谷歌搜了一下,网上对这道题骂声一片。

因为数字的规则实在比想象中复杂太多了,不仅要考虑 + - 号,考虑小数点,还要考虑科学记数法的规则,例如 1.2e−4 里面幂的处理。复杂如下,这是Grandyang 调试测出来的规则。

string s1 = "0"; // True
string s2 = " 0.1 "; // True
string s3 = "abc"; // False
string s4 = "1 a"; // False
string s5 = "2e10"; // True

string s6 = "-e10"; // False
string s7 = " 2e-9 "; // True
string s8 = "+e1"; // False
string s9 = "1+e"; // False
string s10 = " "; // False

string s11 = "e9"; // False
string s12 = "4e+"; // False
string s13 = " -."; // False
string s14 = "+.8"; // True
string s15 = " 005047e+6"; // True

string s16 = ".e1"; // False
string s17 = "3.e"; // False
string s18 = "3.e1"; // True
string s19 = "+1.e+5"; // True
string s20 = " -54.53061"; // True

string s21 = ". 1"; // False

能搜到的有两种解答方式,要么用 while 循环硬扛,要么如下面的有限状态机示意图,这两种方法都极其考验人类的耐心,实在敬佩用这两种方法答题还能通过的人。

通过循环判断,并且添加各种条件判断的问题在于,复杂度太高了,人脑处理这么多规则根本处理不过来,这是非常糟糕的做法,不易维护,可读性差。这是 Grandyang 的解法,丧心病狂的复杂。

class Solution {
public:
    bool isNumber(string s) {
        int len = s.size();
        int left = 0, right = len - 1;
        bool eExisted = false;
        bool dotExisted = false;
        bool digitExisited = false;
        // Delete spaces in the front and end of string
        while (s[left] == ' ') ++left;
        while (s[right] == ' ') --right;
        // If only have one char and not digit, return false
        if (left >= right && (s[left] < '0' || s[left] > '9')) return false;
        //Process the first char
        if (s[left] == '.') dotExisted = true;
        else if (s[left] >= '0' && s[left] <= '9') digitExisited = true;
        else if (s[left] != '+' && s[left] != '-') return false;
        // Process the middle chars
        for (int i = left + 1; i <= right - 1; ++i) {
            if (s[i] >= '0' && s[i] <= '9') digitExisited = true;
            else if (s[i] == 'e' || s[i] == 'E') { // e/E cannot follow +/-, must follow a digit
                if (!eExisted && s[i - 1] != '+' && s[i - 1] != '-' && digitExisited) eExisted = true;
                else return false;
            } else if (s[i] == '+' || s[i] == '-') { // +/- can only follow e/E
                if (s[i - 1] != 'e' && s[i - 1] != 'E') return false;                
            } else if (s[i] == '.') { // dot can only occur once and cannot occur after e/E
                if (!dotExisted && !eExisted) dotExisted = true;
                else return false;
            } else return false;
        }
        // Process the last char, it can only be digit or dot, when is dot, there should be no dot and e/E before and must follow a digit
        if (s[right] >= '0' && s[right] <= '9') return true;
        else if (s[right] == '.' && !dotExisted && !eExisted && digitExisited) return true;
        else return false;
    }
};

看看 SimonS 用有限状态机的图解:

状态机

这个图的复杂度也尤其高。

其实这种题最适合用解析器组合子来做。

写个parser combinator 边看着《幸福三重奏》福原爱秀恩爱边 debug 不怎么费脑细胞就搞定了,就是 Java 代码太累赘,200行代码看起来比别的答案都长,用Kotlin 应该能短点。

通过组合子定义 Number 规则,复杂度瞬间降低

Parser validNumberRule1 = new And(new ZeroOrOne(sign), multiDigit, dot, multiDigit);
Parser validNumberRule2 = new And(new ZeroOrOne(sign), multiDigit, ePart);
Parser validNumberRule3 = new And(new ZeroOrOne(sign), multiDigit, dot, new ZeroOrOne(multiDigit), ePart);
Parser validNumberRule4 = new And(new ZeroOrOne(sign), dot, multiDigit, new ZeroOrOne(ePart));
Parser validNumberRule5 = new And(new ZeroOrOne(sign), multiDigit, new ZeroOrOne(dot));

完整的代码如下:

class Solution {
    public boolean isNumber(String s) {
        // write your code here
        if (s == null || s.trim().length() == 0 || s.trim().equals(".")) {
            return false;
        }
        Parser digit = new SAT(new IsDigit(), new Item());
        Parser sign = new SAT(new IsSign(), new Item());
        Parser dot = new Ch('.');
        Parser letterE = new Ch('e');
        Parser multiDigit = new Many(digit);
        Parser ePart = new And(letterE, new ZeroOrOne(sign), multiDigit);

        Parser validNumberRule1 = new And(new ZeroOrOne(sign), multiDigit, dot, multiDigit);
        Parser validNumberRule2 = new And(new ZeroOrOne(sign), multiDigit, ePart);
        Parser validNumberRule3 = new And(new ZeroOrOne(sign), multiDigit, dot, new ZeroOrOne(multiDigit), ePart);
        Parser validNumberRule4 = new And(new ZeroOrOne(sign), dot, multiDigit, new ZeroOrOne(ePart));
        Parser validNumberRule5 = new And(new ZeroOrOne(sign), multiDigit, new ZeroOrOne(dot));

        Parser validNumber = new Or(validNumberRule5, validNumberRule1, validNumberRule2, validNumberRule3, validNumberRule4);
        try {
            Result result = validNumber.parse(s.trim());
            return true;
        } catch(Exception e) {
            return false;
        }
    }
    
    interface Predicate {
        boolean satisfy(String target);
    }

    interface Parser {
        Result parse(String target);
    }

    static class ZeroOrOne implements Parser {

        private Parser parser;
        public ZeroOrOne(Parser parser) {
            this.parser = parser;
        }

        @Override
        public Result parse(String target) {
            try {
                return parser.parse(target);
            } catch (Exception e) {
                //do nothing
            }
            return Result.buildResult("", target);
        }
    }

    static class Or implements Parser {

        private Parser[] parsers;
        public Or(Parser... parsers) {
            this.parsers = parsers;
        }

        @Override
        public Result parse(String target) {
            for (Parser parser : parsers) {
                try {
                    Result result = parser.parse(target);
                    if (result.remain.length() == 0) {
                        return result;
                    }
                } catch (Exception e) {
                    //do nothing
                }
            }
            throw new RuntimeException("OR Ignore Expect " + parsers + " parse " + target);
        }
    }

    static class Many implements Parser {

        private Parser parser;

        public Many(Parser parser) {
            this.parser = parser;
        }

        @Override
        public Result parse(String target) {
            Result result = parser.parse(target);
            try {
                while (result.remain.length() > 0) {
                    result = Result.concat(result, parser.parse(result.remain));
                }
            } catch (Exception e) {
                //do nothing
            }
            return result;
        }
    }

    static class Item implements Parser {
        @Override
        public Result parse(String target) {
            return Result.buildResult(target.substring(0, 1),
                    target.substring(1, target.length()));
        }
    }

    static class And implements Parser {

        private Parser[] parsers;

        public And(Parser... parsers) {
            this.parsers = parsers;
        }

        @Override
        public Result parse(String target) {
            Result result = Result.buildResult("", target);
            for (Parser parser : parsers) {
                result = Result.concat(result, parser.parse(result.remain));
            }
            return result;
        }
    }

    static class SAT implements Parser {

        private Predicate predicate;
        private Parser parser;

        public SAT(Predicate predicate, Parser parser) {
            this.predicate = predicate;
            this.parser = parser;
        }

        @Override
        public Result parse(String target) {
            Result result = parser.parse(target);
            if (predicate.satisfy(result.recognized))
                return result;
            throw new RuntimeException("parse error: " + parser + " " + predicate + " parse " + target + " fail");
        }
    }

    static class Result {
        private String recognized;
        private String remain;

        static Result buildResult(String recognized, String remain) {
            Result result = new Result();
            result.recognized = recognized;
            result.remain = remain;
            return result;
        }

        static Result concat(Result result1, Result result2) {
            return Result.buildResult(result1.recognized + result2.recognized, result2.remain);
        }

        @Override
        public String toString() {
            return "recognized: " + recognized + " remain: " + remain;
        }
    }

    static class IsSign implements Predicate {
        @Override
        public boolean satisfy(String target) {
            return target.charAt(0) == '+' || target.charAt(0) == '-';
        }
    }

    static class Ch implements Parser {

        private char c;
        public Ch(char c) {
            this.c = c;
        }

        @Override
        public Result parse(String target) {
            Item item = new Item();
            Result result = item.parse(target);
            if (String.valueOf(c).equals(result.recognized)) {
                return result;
            }
            throw new RuntimeException("parser error: Ch parser " + c + " parse error");
        }
    }

    static class IsDigit implements Predicate {

        @Override
        public boolean satisfy(String target) {
            return target.charAt(0) >= '0' && target.charAt(0) <= '9';
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 有句话说: " 我陪他度过了最美好的青春年华,最后却还是要羡慕那个陪他共度一生的人 " 。你教会了一个人去爱,他却...
    丶浅若夏沫阅读 223评论 0 0
  • 打球,其实很累的。 每一次下课铃声还没响起的时候,就想着得赶紧冲出教室,去抢占篮球场,就怕晚了一刻,没场地了,还得...
    29颗牙齿阅读 502评论 0 1
  • 小孩子不听话是为什么?一朋友问我。 “少了影响力!” 凡是问题都有许多答案,这个也一样,可无论你是答了她进入叛逆期...
    雅俗儿的手帐阅读 524评论 8 9
  • 等待往往会被人误解为坚持,觉得一件事只有坚持下去总能等到一个结局,往往这个结局还不坏。但我想提醒自己的是:别人那是...
    呆子Y默阅读 117评论 2 2