LeetCode065-验证数字

Posted 千里之行,始于足下。

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode065-验证数字相关的知识,希望对你有一定的参考价值。

v2-cff4e31091fe0b54552a443b85a78df0_1200x500

写在前面

前面研究OS的经历实在是令人心力憔悴。。所以换个新鲜的,把自己的刷题感悟整理一番。刷了有些题了,就先拿最近几天hard题打头阵吧。首先说的是(065)Valid Number这个题,其实一眼看起来很简单,不就是for/while/if/else吗?那么你可能不知道这道题其实有一个更加简(bian)洁(tai)的方法,听我慢慢道来。

题目要求

Validate if a given string is numeric.

Some examples: "0" => true " 0.1 " => true "abc" => false "1 a" => false "2e10" => true

Note: It is intended for the problem statement to be ambiguous. You should gather all requirements up front before implementing one.

常规解法

Java for LeetCode 065 Valid Number

public boolean isNumber(String s) {
    s = s.trim();
    String[] splitArr = s.split("e");
    if (s.length() == 0 || s.charAt(0) == \'e\'
            || s.charAt(s.length() - 1) == \'e\' || splitArr.length > 2)
        return false;
    for (int k = 0; k < splitArr.length; k++) {
        String str = splitArr[k];
        boolean isDecimal = false;
        if (str.charAt(0) == \'-\' || str.charAt(0) == \'+\')
            str = str.substring(1);
        if (str.length() == 0)
            return false;
        for (int i = 0; i < str.length(); i++) {
            if (\'0\' <= str.charAt(i) && str.charAt(i) <= \'9\')
                continue;
            else if (str.charAt(i) == \'.\' && !isDecimal) {
                if (k == 0 && str.length() > 1)
                    isDecimal = true;
                else
                    return false;
            } else
                return false;
        }
    }
    return true;
}

对常规解法的评价

只要会点C++,那么常规解法就不在话下,其实就是手动实现一遍itoa而已。

常规解法的优点是:门槛低/常人写得出/容易修改,也就是定制性好/扩展性差。

同时,它的缺点是:一旦验证逻辑变复杂,那就gg了。比如我想把复数也算进去啊,那又得改那堆杂七杂八的代码,令人感觉不会再爱了。

正则表达式

正则表达式30分钟入门教程总结得比较好。简单来说,正则表达式(regex)可以表示一个特定的词法(编译原理之词法分析、语法分析、语义分析 - nic_r的专栏 - 博客频道 - CSDN.NET),如整数、实数、复数、邮箱地址、电话号码等。regex除了有匹配的功能之外,它还带有替换/解析功能,这样,能够满足涉及字符串操作的大多数需求。

比如,匹配方面,涉及用户名匹配、邮箱地址的匹配等,如果这时你用常规解法就太臃肿、太麻烦了。替换/解析方面,如解析html/XML/JSON等,比如便捷。

那么正则表达式与常规解法有什么不同呢?

刚才提到,常规解法虽然容易修改,但它的扩展性不足,我想更改一点需求,就要大刀阔斧改代码,令人不会再爱。那么如何解决这个扩展性的问题呢?那就需要将算法给抽象出来。

如果单用if/else/while/for做一个邮箱匹配,这时又需要做一个数字匹配功能,那么这两种代码是八竿子打不着的,根本没法子复用代码啊,怎么办呢?

其实稍微用脑子想一想——你写的爬虫程序和他写的游戏程序也是风马牛不相及吧?但是编译器将它们翻译成汇编语言后,是不是又有共同点了?比如都有Jump跳转啊,有mov啊,相似度瞬间提高。这里面的原理是什么呢?原来杂七杂八的代码间,通过编译器的翻译,竟然变成了两份差不多的汇编代码(指用的指令大体相似)。那么方法是翻译吗?

也就是说,原始的两种内容不同的代码,可能甲有着C++的高级特性,乙又是C写的,它们翻译成汇编后,用到的指令有99%都是相同的。反过来,如果我以汇编语言为标准,来表示甲和乙,那么这时候两者的代码有99%是相似的。这时,我们发现了可重用性!

回过头来,想一想,假如有一种语言a可以表达数字、邮箱地址,那我们就不需要再写不同的C/C++代码了,即:有一种机制将你的语言a的表达式翻译成对应的代码,运行这个代码,可以完成匹配工作。这不就是编译器干的事么?

啰嗦了那么多,其实意思就是:想要增加两种功能不同的代码之间的相似程度,必须从代码中的相同点/不同点抽象出一种崭新的语言,用这种崭新的语言可以以统一的语法形式来表达这两份代码。

而正则表达式,正是一种崭新的语言。涉及正则表达式的语法、使用、解析,及NFA、DFA等知识这里不再赘述,请参阅专业书籍或是一些博客。

大致步骤是:

  1. 输入正则表达式串pat
  2. 根据手写的LL1解析pat,生成AST
  3. 根据AST构建NFA,添加Epsilon边
  4. 从NFA转换为DFA,合并状态,确定终态
  5. DFA最小化,生成状态转移矩阵
  6. 根据状态转移矩阵进行匹配

轮子的用武之地

还好自己的https://github.com/bajdcc/jMinilang中有生成DFA的代码。

正则匹配部分在priv.bajdcc.util.lexer.test.TestRegex,直接运行它,然后输入上述正则表达式,那么具体信息就出来了。

详细信息(程序自动生成):

#### 正则表达式语法树 ####
序列 {
	循环{0,-1} {
		字符	[\\u0020,\' \']
	}
	循环{0,1} {
		字符	[\\u002b,\'+\'],[\\u002d,\'-\']
	}
	分支 {
		序列 {
			循环{0,-1} {
				字符	[\\u0030,\'0\']-[\\u0039,\'9\']
			}
			循环{0,1} {
				字符	[\\u002e,\'.\']
			}
			循环{1,-1} {
				字符	[\\u0030,\'0\']-[\\u0039,\'9\']
			}
		}
		序列 {
			循环{1,-1} {
				字符	[\\u0030,\'0\']-[\\u0039,\'9\']
			}
			循环{0,1} {
				字符	[\\u002e,\'.\']
			}
			循环{0,-1} {
				字符	[\\u0030,\'0\']-[\\u0039,\'9\']
			}
		}
	}
	循环{0,1} {
		序列 {
			字符	[\\u0065,\'e\']
			循环{0,1} {
				字符	[\\u002b,\'+\'],[\\u002d,\'-\']
			}
			循环{1,-1} {
				字符	[\\u0030,\'0\']-[\\u0039,\'9\']
			}
		}
	}
	循环{0,-1} {
		字符	[\\u0020,\' \']
	}
}

#### 状态集合 ####
[\\u0020,\' \']
[\\u002b,\'+\']
[\\u002d,\'-\']
[\\u002e,\'.\']
[\\u0030,\'0\']-[\\u0039,\'9\']
[\\u0065,\'e\']
#### 最小化 ####
状态[0] => 0,
	边 => [1]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
	边 => [0]
		类型 => 字符区间	[\\u0020,\' \']
	边 => [2]
		类型 => 字符区间	[\\u002e,\'.\']
	边 => [3]
		类型 => 字符区间	[\\u002b,\'+\']
	边 => [3]
		类型 => 字符区间	[\\u002d,\'-\']
状态[1][结束] => 3,4,6,
	边 => [4]
		类型 => 字符区间	[\\u002e,\'.\']
	边 => [5]
		类型 => 字符区间	[\\u0065,\'e\']
	边 => [6]
		类型 => 字符区间	[\\u0020,\' \']
	边 => [1]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
状态[2] => 5,
	边 => [7]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
状态[3] => 2,
	边 => [2]
		类型 => 字符区间	[\\u002e,\'.\']
	边 => [1]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
状态[4][结束] => 5,8,
	边 => [5]
		类型 => 字符区间	[\\u0065,\'e\']
	边 => [6]
		类型 => 字符区间	[\\u0020,\' \']
	边 => [4]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
状态[5] => 10,
	边 => [8]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
	边 => [9]
		类型 => 字符区间	[\\u002b,\'+\']
	边 => [9]
		类型 => 字符区间	[\\u002d,\'-\']
状态[6][结束] => 11,
	边 => [6]
		类型 => 字符区间	[\\u0020,\' \']
状态[7][结束] => 6,
	边 => [5]
		类型 => 字符区间	[\\u0065,\'e\']
	边 => [7]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
	边 => [6]
		类型 => 字符区间	[\\u0020,\' \']
状态[8][结束] => 14,
	边 => [6]
		类型 => 字符区间	[\\u0020,\' \']
	边 => [8]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']
状态[9] => 13,
	边 => [8]
		类型 => 字符区间	[\\u0030,\'0\']-[\\u0039,\'9\']

#### 状态转移矩阵 ####
	0	3	3	2	1	-1
	6	-1	-1	4	1	5
	-1	-1	-1	-1	7	-1
	-1	-1	-1	2	1	-1
	6	-1	-1	-1	4	5
	-1	9	9	-1	8	-1
	6	-1	-1	-1	-1	-1
	6	-1	-1	-1	7	5
	6	-1	-1	-1	8	-1
	-1	-1	-1	-1	8	-1

解决方案

class Solution {
    inline int getCharMap(const char& c) {
        switch (c) {
            case \' \':
                return 0;
            case \'+\':
                return 1;
            case \'-\':
                return 2;
            case \'.\':
                return 3;
            case \'0\':
            case \'1\':
            case \'2\':
            case \'3\':
            case \'4\':
            case \'5\':
            case \'6\':
            case \'7\':
            case \'8\':
            case \'9\':
                return 4;
            case \'e\':
                return 5;
        }
        return -1;
    }

public:
    bool isNumber(string s) {
        using t = int(*)[6];
        int mm[] = {
                0	,3	,3	,2	,1	,-1,
                6	,-1	,-1	,4	,1	,5,
                -1	,-1	,-1	,-1	,7	,-1,
                -1	,-1	,-1	,2	,1	,-1,
                6	,-1	,-1	,-1	,4	,5,
                -1	,9	,9	,-1	,8	,-1,
                6	,-1	,-1	,-1	,-1	,-1,
                6	,-1	,-1	,-1	,7	,5,
                6	,-1	,-1	,-1	,8	,-1,
                -1	,-1	,-1	,-1	,8	,-1,
        };
        auto m = (t)mm;
        bool final[] = {0, 1, 0, 0, 1, 0, 1, 1, 1, 0};
        int status = 0;
        auto c = s.c_str();
        for (;;) {
            auto local = *c++;
            int charClass = getCharMap(local);
            int refer = -1;
            if (charClass != -1) {
                refer = m[status][charClass];
            }
            if (refer == -1) {
                return local == 0 && final[status];
            } else {
                status = refer;
            }
        }
    }
};
https://zhuanlan.zhihu.com/p/25879478备份。

以上是关于LeetCode065-验证数字的主要内容,如果未能解决你的问题,请参考以下文章

剑指 Offer II 065. 最短的单词编码

剑指 Offer II 065. 最短的单词编码

LeetCode 260 只出现一次的数字(超详细)

算法笔记_065:分治法求逆序对(Java)

JavaScript 有用的代码片段和 trick

力扣(LeetCode)验证回文串 个人题解(C++)