回溯的例子-正则表达式

回溯

回溯是比较强有力的遍历或枚举算法,掌握这个算法别无他途,只有勤学多练。在leetcode中的第10题就是一个比较有意思题目,一个正则表达式的实现,题目是这样的

#'.' Matches any single character.
#'*' Matches zero or more of the preceding element.
#The matching should cover the entire input string (not partial).

#Note:

#s could be empty and contains only lowercase letters a-z.
#p could be empty and contains only lowercase letters a-z, and characters like . or *.

实现

对于正则表达式,大家应该比较熟悉,这个题目就是实现正则表达式中的.和*。我们最简单的做法就是使用正则表达式,其缺点后续再说明。

对于比对中文本的位置和正则表达式的位置分别用textPos和patternPos来表示,对于'.'的语义就是说,如果比对位是'.',那么就讲两个位置都加1。如果textPos位置的字符和patternPos的字符相同,那么两个位置加1。

接着就是难点的表达,这里按照语义,需要表达的就是0和多个,对于0,例如说“a”和"b",为了表达0,那么textPos不变,讲patternPos+2;如果表达多个,例如"aaaa"和"a*”,将textPos加1即可,因为我们基于回溯去考虑问题,不论这里有几个a,只需要考虑加1,下个字符基于这次的结果继续考虑。因为考虑的是0和多个,这里0和多个之间就是或的关系,可能一个都没有,可能有多个。

再回去看一下回溯的模版

def backtracking(level, paras):
    if exist_condition(level):
        return
    state = keepsate(level)  #保存当前状态
    backtracking(level+1, paras):
    reverseState(level, state) #恢复当前状态

我们的代码如下:

    def isMatch(self, text, pattern):
        if not pattern or not text: return False
        self.textLen, self.patternLen = len(text), len(pattern)
        self.text, self.pattern, self.matched = text, pattern, False
        self.match(0, 0)
        return self.matched

    def match(self, textPos, patternPos):
        if self.matched: return 退出条件
        if patternPos == self.patternLen:
            self.matched = textPos == self.textLen
            return  #退出条件

        first_match = textPos < self.textLen and self.pattern[patternPos] in {self.text[textPos], '.'} #字符匹配或者是.匹配

        if self.patternLen - patternPos >= 2 and self.pattern[patternPos+1] == '*':
            return first_match and self.match(textPos + 1, patternPos) or self.match(textPos, patternPos + 2) # *的0和多个的逻辑,这里是或者的关系
        else:
            return first_match and self.match(textPos + 1, patternPos + 1) #text和pattern位置+1处理。

比对模版,首先是退出条件,回溯必须有退出条件,然后是对于的语义表达和非的语义表达。我们基于语义来思考问题,从而避免了陷入递归中。

如下是C语言版的实现,看起来更加清爽:

bool isMatch(char* s, char* p) {
    int sLen = strLen(s);
    int sPat = strLen(p);
    if (0 == sPat) {
        return 0 == sLen;
    }

    bool firstMatch = 0 != sLen && (p[0] == '.' || p[0] == s[0]);
    if ((sPat > 1) && (p[1] == '*')) {
        return firstMatch && isMatch(s + 1, p) || isMatch(s, p + 2);
    } else {
        return firstMatch && isMatch(s + 1, p + 1);
    }
}

缺点

如上,特别是对于的语义表达式个or的关系,如果用递归树展开,可以看到这是一个非常复杂的递归树,例如说aa和a
----------(aa a)
-----(a a
)----------(aa "")
("" a*) -- (a "")
高度是len(text),每一列最大是2^(len(text)-1)个元素,这样计算为
1 + 2 + 2^2 + ... + 2(len(text)-1),用等比公式计算得到2len(text) -1,时间复杂度为O(2^len(text),这是一个指数级的复杂度。因此需要进行优化,怎么优化,可以使用动态规划的思路进行优化。

如下动态规划的实现,也很清爽:

    def isMatch(self, text, pattern):
        dp = [[False] * (len(pattern) + 1) for _ in range(len(text) + 1)]
        dp[-1][-1] = True
        for i in range(len(text), -1, -1):
            for j in range(len(pattern) - 1, -1, -1):
                first_match = i < len(text) and pattern[j] in {text[i], '.'}
                if j + 1 < len(pattern) and pattern[j + 1] == '*':
                    d[i][j] = first_match and d[i + 1][j] or d[i][j + 2]
                else:
                    d[i][j] = first_match and d[i + 1][j + 1]
        return dp[0][0] 

和上面思路是一样的,用一张表将匹配结果储存起来。时间复杂度为O(len(text)*len(pattern),这个复杂度就远小于上面使用回溯的指数级的实际复杂度了。

小结

这个例子是个非常有意思的例子,通过回溯,我们能比较容易的写出枚举型的实现,但是发现复杂度是指数型的,因此考虑用动态规划来实现,可以达到低一个数量级的实际复杂度。
学习完了,我们可以看一下java的正则表达式的实现,其底层看一看是否是回溯的实现,以及正则表达式式怎么降低时间复杂度的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容

  • 前几天线上一个项目监控信息突然报告异常,上到机器上后查看相关资源的使用情况,发现 CPU 利用率将近 100%。通...
    java菜阅读 387评论 0 0
  • 前几天线上一个项目监控信息突然报告异常,上到机器上后查看相关资源的使用情况,发现 CPU 利用率将近 100%。通...
    温柔的倾诉阅读 346评论 0 0
  • 每个人都有自己的愤怒,忍的,放弃的,发泄的,每一段都可能转变为恼羞成怒,像狮子也像疯狗。
    霉斯捣譜阅读 74评论 0 1
  • 一定要充分利用通过付费节省下来的时间和注意力,做对自己有用的事情,不然这也是一种浪费; 对于兼职这个事情,我也赞同...
    大海的凨阅读 154评论 0 0
  • 创业者和投资人要了解的是,并不是所有共享项目都能成为共享单车,“变形”的共享经济似乎变成了一场又一场披着共享名义的...
    可爱的派大星星阅读 236评论 0 0