Leetcode - Regular Expression Matching

**
Question:

'.' Matches any single character.
'*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char *s, const char *p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true

**

这是我写过的最恶心的代码!!!我连续写了四个小时,针对不同的细节。及其繁琐。
最后实在扛不住了,放弃了。
现在很累。实在看不动分析了。明天再说吧


Referenced Code:

public class Solution {
    public boolean isMatch(String s, String p) {
        if (p.isEmpty()) {
            return s.isEmpty();
        }

        if (p.length() == 1 || p.charAt(1) != '*') {
            if (s.isEmpty() || (p.charAt(0) != '.' && p.charAt(0) != s.charAt(0))) {
                return false;
            } else {
                return isMatch(s.substring(1), p.substring(1));
            }
        }

        //P.length() >=2
        while (!s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'))  {  
            if (isMatch(s, p.substring(2))) { 
                return true;                     
            }                                    
            s = s.substring(1);
        }

        return isMatch(s, p.substring(2));
    }
}

Test result:

Paste_Image.png

思路:
这次作业挺难的,很难的,对于我来说。。。
是一种模式识别,要考虑较多的情况。每次碰到这样的编程题,程序员之间,或者说,码农和程序员之间的区别就会体现出来了。这是一种思路的区别。码农想到的一定是用if将各种情况考虑到,比如我。而真正的程序员,想到的一定是,以一种更加宏观的思路,来解决这个问题,而不拘泥于细节。这种宏观的思路,就是,递归。
Recursion has helped code become simple.
首先说下我犯下的错误吧。
最严重的错误,给出的两个字符串,s , p, 其实只会在p中出现 "." and "". 因为是表达式匹配,所以s中不会出现这些符号。
然后就是对 '
'含义的理解。"*"表示的是,前面那个字符出现的次数。
比如,

aa = a*
aaa = a*
ac = ab*c
....

这里会有几个问题。
1.前面如果没有字符,即他是字符串的首位,那么,他会想普通字符一样与s中字符进行比较。
2.
若连续的出现,******,我们仍将它两个两个的分组,前面一个默认为有意义的字符,后面一个才表示其出现的次数。所以前面一个*与普通字符作比较时永远是不相等的。所以一定是false。

  1. ".*" 的情况比较特殊。
ab = .*
abc = .*
abcdefghijkldsadf = .*
abcd != .*c
abcd = .*abcd
abcd = .*bcd
abcd = .*cd
abcd = .*d
abcd = .*

可以看出,如果 .* 后面没有尾巴,他与任何的s相匹配。如果有尾巴,他的尾巴必须与s中的尾巴相匹配。
4.该用何种思路来处理 * 的比较。
我一开始的思路,或者大多数人的思路,一定是贪婪算法。就是将*表示的次数刷到最大。比如:

aaa = a*
aaaaaab = a*b

这个时候我们会将*刷到最大。然后进行下一组比较。但是,有些情况是特殊的,也是我最后发现的,然后决定放弃自己的思路看答案。

aaaaa = a*a

在这样一个匹配中,如果用最大化 * 思路的话,那么就是不匹配的了, *会一直刷到s结束。然后p最后还有一个 a。那就不匹配了。所以这个时候就得判断, * 到底代表多少次。这并不是越大越好。我当时也想了一些方法来处理这种情况。比如统计相同字符的个数。。。。后来觉得太复杂了。网上一看,果不其然,
用递归!

下来说下思路吧。
应该分成两种情况。
1.p中第二个字符不是""。 那么,该怎么比就怎么比。
2.p中第二个字符是"
"。那么。就需要用递归。
如果 s.charAt(0) == p.charAt(0) || p.charAt(0) == '.' ----> .* (万能匹配)
那么,就会判断, s是否与p.substring(2),即p字符串剩下的部分匹配。如果匹配,那么就返回true。如果不匹配。
s = s.substring(1).
再判断 s.charAt(0) == p.charAt(0) || p.charAt(0) == '.'
如果满足,继续判断 此时的s是否与p.substring(2),即p字符串剩下的部分匹配。如果匹配,那么就返回true。如果不匹配。

aaa = a*a

比较次序是

s = aaa;
s != a;
s = aa;
s != a;
s = a;
s = a;
return true;

这就是这个代码的基本思路。用递归来实现这种判断,判断s剩下的部分(尾部)是否与p的尾部相匹配。已经,何处开始是s的尾部。

**
总结:
递归,感觉有种人工智能的感觉。其实也只是一种遍历,但是这种遍历,又包含了人的思维。因为他的每次执行,不仅仅是简单的遍历,还会将我们的思维再次重复执行一遍。这就是递归。我的实力还远远达不到这个水平。。
而且这次写代码,我再次可以感受到,自己思维中的那些琐碎的,冗余的想法。
而且以后有什么思路,一定要写在纸上,尤其对于这么复杂的题目。情况一多,很多东西脑子里想到过,但后来又忘了。
**

Anyway, Good luck, Zhidong Liu!

My code:

public class Solution {
    public boolean isMatch(String s, String p) {
        if (p.length() == 0) {
            return s.length() == 0;
        }
        else if (p.length() == 1) {
            if (s.length() == 1 && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.')) {
                return true;
            }
            else {
                return false;
            }
        }
        else if (p.charAt(1) != '*') {
            if (s.length() == 0) {
                return false;
            }
            else if (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.') {
                return isMatch(s.substring(1), p.substring(1));
            }
            else {
                return false;
            }
        }
        else {
            if (isMatch(s, p.substring(2))) {
                return true;
            }
            while (s.length() > 0 && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.')) {
                 s = s.substring(1);
                if (isMatch(s, p.substring(2))) {
                    return true;
                }
            }
            return false;
        }
    }
}

看的同学的代码,觉得逻辑很清楚。
dfs
首先,p每次移动的单位是2位。所有,如果p中含有 *,那么,一定在index = 1 的位置。
所以,
首先判断, p.length() == 0
如果等于 0, 那么s也必须等于0,否则就是false

然后,如果 p.length() == 1
那么,p不可能含有 *
所以可以简单地比较,
s.charAt(0) == p.charAt(0) || p.charAt(0)
此时可以返回true,否则返回false

如果p.length() >= 2 && p.charAt(1) != '*'
此时也可以放心的比较。
首先判断s 长度。如果已经是0了,因为 p第二位不是 , 所以一定不match,返回false
如果s.length() > 0, 并且s.charAt(0) == p.charAt(0) || p.charAt(0)
那么,就可以比较, isMatch(s.substring(1), p.substring(1))
有人会问,这里不是只移动了一格而没有移动两格吗?
因为第二位不是 '
'啊。所以我们移动一格并不会破坏顺序。

如果p.length() >= 2 && p.charAt(1) == '*'

这个时候,* 可以代表0个或多个他前面的字符。
所以,首先,如果*代表0个,那么我直接匹配s and p.substring(2)
即,
if (isMatch(s, p.substring(2))) {...}

然后如果匹配不成功,这个时候我就需要移动 s 了
首先看 s.charAt(0) 是否与 p.charAt(0) 匹配。
如果匹配,ok,移动一位,
s = substring(1);
然后匹配此时的 s and p
if (isMatch(s, p.substring(2))) {...}

如果不匹配。再判断此刻的s(注意,这里的s已经向右移动了一位。)
if s.charAt(0) 和 p.charAt(0), 是否匹配。

如果不匹配,退出循环,直接返回 false 就行。

注意这里,只有 p 第二位是 '*',我每次都移动两位。

差不多就这样了。这个解法比较通俗易懂,很不错。

一直记录生活,计划,的小笔记本丢了。很可惜。里面的东西也很复原了。幸亏上周把所有的日程记录到了 Mac Calendar上,减少了些损失。是啊,如果我的小笔记本是放在云端的,怎么会丢呢?
但是电子设备记录笔记,还是没有纸质笔记本的感觉好。
不知如何选。
这周经历了许多机会,许多失去。到现在,总体来说,还是收获更多。下周,下下周,真正的挑战就要来临了。现在还只是玩玩。
加油吧,多刷题,把简单medium 刷熟练了,高频hard也刷熟练了。

Anyway, Good luck, Richardo! -- 09/16/2016

推荐阅读更多精彩内容