`
JayWang
  • 浏览: 2406 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
最近访客 更多访客>>
社区版块
存档分类
最新评论

正则表达式_回溯

阅读更多

回溯

Backtracking

NFA引擎最重要的性质是,它会依次处理各个子表达式或组成元素,遇到需要在两个可能成功的可能中进行选择的时候,它会选择其一,同时记住另一个,以备稍后可能的需要。

需要做出选择的情形包括量词(决定是否尝试另一次匹配)和多选结构(决定选择哪个多选分支,留下哪个稍后尝试)。

不论选择那一种途径,如果它能匹配成功,而且正则表达式的余下部分也成功了,匹配即告完成。如果正则表达式中余下的部分最终匹配失败,引擎会知道需要回溯到之前做出选择的地方,选择其他的备用分支继续尝试。这样,引擎最终会尝试表达式的所有可能途径(或者是匹配完成之前需要的所有途径)。

真实世界中的例子:面包屑

A Really Crummy Analogy

回溯就像是在道路的每个分岔口留下一小堆面包屑。如果走了死路,就可以照原路返回,直到遇见面包屑标示的尚未尝试过的道路。如果那条路也走不通,你可以继续返回,找到下一堆面包屑,如此重复,直到找到出路,或者走完所有没有尝试过的路。

在许多情况下,正则引擎必须在两个(或更多)选项中做出选择——我们之前看到的分支的情况就是一例。另一个例子是,在遇到「…x?…」时,引擎必须决定是否尝试匹配「x」。对于「…x+…」的情况,毫无疑问,「x」至少尝试匹配一次——因为加号要求必须匹配至少一次。第一个「x」匹配之后,此要求已经满足,需要决定是否尝试下一个「x」。如果决定进行,还要决定是否匹配第三个「x」,第四个「x」,如此继续。每次选择,其实就是洒下一堆“面包屑”,用于提示此处还有另一个可能的选择(目前还不能确定它能否匹配),保留起来以备用。

一个简单的例子

现在来看个完整的例子,用先前的「to(nite|knight|night)」匹配字符串‘hot–tonic– tonight! ’(看起来有点无聊,但是个好例子)。第一个元素「t」从字符串的最左端开始尝试,因为无法匹配‘h’,所以在这个位置匹配失败。传动装置于是驱动引擎向后移动,从第二个位置开始匹配(同样也会失败),然后是第三个。这时候「t」能够匹配,接下来的「o」无法匹配,因为字符串中对应位置是一个空格。至此,本轮尝试宣告失败。

继续下去,从…ætonic…开始的尝试则很有意思。to匹配成功之后,剩下的3个多选分支都成为可能。引擎选取其中之一进行尝试,留下其他的备用(也就是洒下一些面包屑)。在讨论中,我们假定引擎首先选择的是「nite」。这个表达式被分解为“「n」+「i」+「t」+「e」”,在…toniæc…遭遇失败。但此时的情况与之前不同,这种失败并不意味着整个表达式匹配失败——因为仍然存在没有尝试过的多选分支(就好像是,我们仍然可以找到先前留下的面包屑)。假设引擎然后选择「knight」,那么马上就会遭遇失败,因为「k」不能匹配‘n’。现在只剩下最后的选项「night」,但它不能失败。因为「night」是最后尝试的选项,它的失败也就意味着整个表达式在…ætonic…的位置匹配失败,所以传动机构会驱动引擎继续前进。

直到引擎开始从…ætonight!处开始匹配,情况又变得有趣了。这一次,多选分支「night」终于可以匹配字符串的结尾部分了(于是整体匹配成功,现在引擎可以报告匹配成功了)。

回溯的两个要点

Two Important Points on Backtracking

回溯机制的基本原理并不难理解,还是有些细节对实际应用很重要。它们是,面对众多选择时,哪个分支应当首先选择?回溯进行时,应该选取哪个保存的状态?第一个问题的答案是下面这条重要原则:

如果需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”,而对于忽略优先量词,会选择“跳过尝试”。

此原则影响深远。对于新手来说,它有助于解释为什么匹配优先的量词是“匹配优先”的,但还不完整。要想彻底弄清楚这一点,我们需要了解回溯时使用的是哪个(或者是哪些个)之前保存的分支,答案是:

距离当前最近储存的选项就是当本地失败强制回溯时返回的。使用的原则是LIFO(last in first out,后进先出)。

用面包屑比喻就很好理解——如果前面是死路,你只需要沿原路返回,直到找到一堆面包屑为止。你会遇到的第一堆面包屑就是最近洒下的。传统的LIFO比喻也是这样:就像堆叠盘子一样,最后叠上去的盘子肯定是最先拿下来的。

备用状态

Saved States

用NFA正则表达式的术语来说,那些面包屑相当于“备用状态(saved state)”。它们用来标记:在需要的时候,匹配可以从这里重新开始尝试。它们保存了两个位置:正则表达式中的位置,和未尝试的分支在字符串中的位置。因为它是NFA匹配的基础,我们需要再看一遍某些已经出现过的简单但详细的例子,说明这些状态的意义。如果你觉得现有的内容都不难懂,请继续阅读。

未进行回溯的匹配

来看个简单的例子,用「ab?c」匹配abc。「a」匹配之后,匹配的当前状态如下:

‘aæbc’

「aæb?c」

现在轮到「b?」了,正则引擎需要决定:是需要尝试「b」呢,还是跳过?因为?是匹配优先的,它会尝试匹配。但是,为了确保在这个尝试最终失败之后能够恢复,引擎会把:

‘aæbc’

「ab? æc」

添加到备用状态序列中。也就是说,稍后引擎可以从下面的位置继续匹配:从正则表达式中的「b?」之后,字符串的b之前(也就是当前的位置)匹配。这实际上就是跳过「b」的匹配,而问号容许这样做。

引擎放下面包屑之后,就会继续向前,检查「b」。在示例文本中,它能够匹配,所以新的当前状态变为:

‘abæc’

「ab? æc」

最终的「c」也能成功匹配,所以整个匹配完成。备用状态不再需要了,所以不再保存它们。

进行了回溯的匹配

如果需要匹配的文本是‘ac’,在尝试「b」之前,一切都与之前的过程相同。显然,这次「b」无法匹配。也就是说,对「…?」进行尝试的路走不通。因为有一个备用状态,这个“局部匹配失败”并不会导致整体匹配失败。引擎会进行回溯,也就是说,把“当前状态”切换为最近保存的状态。在本例中,情况就是:

‘aæc’

「ab? æc」

在「b」尝试之前保存的尚未尝试的选项。这时候,「c」可以匹配c,所以整个匹配宣告完成。

不成功的匹配

现在,我们用同样的表达式匹配‘abX’。在尝试「b」以前,因为存在问号,保存了这个备用状态:

‘aæbX’

「ab? æc」

「b」能够匹配,但这条路往下却走不通了,因为「c」无法匹配X。于是引擎会回溯到之前的状态,“交还”b给「c」来匹配。显然,这次测试也失败了。如果还有其他保存的状态,回溯会继续进行,但是此时不存在其他状态,在字符串中当前位置开始的整个匹配也就宣告失败。

事情到此结束了吗?没有。传动装置会继续“在字符串中前行,再次尝试正则表达式”,这可能被想象为一个伪回溯(pseudo-backtrack)。匹配重新开始于:

‘aæbX’

「æab?c」

从这里重新开始整个匹配,如同之前一样,所有的道路都走不通。接下来的两次(从abæX到abXæ)都告失败,所以最终会报告匹配失败。

忽略优先的匹配

现在来看最开始的例子,使用忽略优先匹配量词,用「ab??c」来匹配‘abc’。「a」匹配之后的状态如下:

‘aæbc’

「aæb??c」

接下来轮到「b??」,引擎需要进行选择:尝试匹配「b」,还是忽略?因为??是忽略优先的,它会首先尝试忽略,但是,为了能够从失败的分支中恢复,引擎会保存下面的状态:

‘aæbc’

「aæbc」

到备用状态列表中。于是,引擎稍后能够用正则表达式中的「b」来尝试匹配文本中的b(我们知道这能够匹配,但是正则引擎不知道,它甚至都不知道是否会要用到这个备用状态)。状态保存之后,它会继续向前,沿着忽略匹配的路走下去:

‘aæbc’

「ab?? æc」

「c」无法匹配‘b’,所以引擎必须回溯到之前保存的状态:

‘aæbc’

「aæbc」

显然,此时匹配可以成功,接下来的「c」匹配‘c’。于是我们得到了与使用匹配优先的「ab?c」同样的结果,虽然两者所走的路不相同。

回溯与匹配优先

Backtracking and Greediness

如果工具软件使用的是NFA正则表达式主导的回溯引擎,理解正则表达式的回溯原理就成了高效完成任务的关键。我们已经看到「?」的匹配优先和「??」的忽略优先是如何工作的,现在来看星号和加号。

星号、加号及其回溯

如果认为「x*」基本等同于「x?x?x?x?x?x?…」(或者更确切地说是「(x(x(x(x…?)?)?)?)?)」)(注5),那么情况与之前没有大的差别。每次测试星号作用的元素之前,引擎都会保存一个状态,这样,如果测试失败(或者测试进行下去遭遇失败),还能够从保存的状态开始匹配。这个过程会不断重复,直到包含星号的尝试完全失败为止。

所以,如果用「[0-9]+」来匹配‘a–1234–num’,「[0-9]」遇到4之后的空格无法匹配,而此时加号能够回溯的位置对应了四个保存的状态:

a 1æ234 num

a 12æ34 num

a 123æ4 num

a 1234æ num

也就是说,在每个位置,「[0-9]」的尝试都代表一种可能。在「[0-9]」遇到空格匹配失败时,引擎回溯到最近保存的状态(也就是最下面的位置),选择正则表达式中的「[0-9]+ æ」和文本中的‘a–1234æ–num’。当然,到此整个正则表达式已经结束,所以我们知道,整个匹配宣告完成。

请注意,‘a–æ1234–num’并不在列表中,因为加号限定的元素至少要匹配一次,这是必要条件。那么,如果正则表达式是「[0-9]*」,这个状态会保存吗?(提示:这个问题得动点脑筋)。要知道答案,v请翻到下一页。

重新审视更完整的例子

有了更详细的了解之后,我们再来看看第152页的「^.*([0-9][0-9])」的例子。这一次,我们不是只用“匹配优先”来解释为什么会得到那样的匹配结果,我们能够根据NFA的匹配机制做出精确解释。

以‘CA–95472–USA’为例。在「.*」成功匹配到字符串的末尾时,星号约束的点号匹配了13
个字符,同时保存了许多备用状态。这些状态表明稍后的匹配开始的位置:在正则表达式中是「^.*æ ([0-9][0-9])」,在字符串中则是点号每次匹配时保存的备用状态。

现在我们已经到了字符串的末尾,并把控制权交给第一个「[0-9]」,显然这里的匹配不能成功。没问题,我们可以选择一个保存的状态来进行尝试(实际上保存了许多的状态)。现在回溯开始,把当前状态设置为最近保存的状态,也就是「.*」匹配最后的A之前的状态。忽略(或者,如果你愿意,可以使用“交还”)这个匹配,于是有机会用「[0-9]」匹配这个A,但这同样会失败。

这种“回溯-尝试”的过程会不断循环,直到引擎交还2为止,在这里,第一个「[0-9]」可以匹配。但是第二个「[0-9]」仍然无法匹配,所以必须继续回溯。现在,之前尝试中第一个「[0-9]」是否匹配与本次尝试并无关系了,回溯机制会把当前的状态中正则表达式内的对应位置设置到第一个「[0-9]」以前。我们看到,当前的回溯同样会把字符串中的位置设置到7以前,所以第一个「[0-9]」可以匹配,而第二个「[0-9]」也可以(匹配2)。所以,我们得到一个匹配结果‘CA–95472,–USA’,$1得到72。

需要注意的是:第一,回溯机制不但需要重新计算正则表达式和文本的对应位置,也需要维护括号内的子表达式所匹配文本的状态。在匹配过程中,每次回溯都把当前状态中正则表达式的对应位置指向括号之前,也就是「^.*æ ([0-9][0-9])」。在这个简单的例子中,所以它等价于「^.*æ [0-9][0-9]」,因此我说“使用第一个[0-9]之前的位置”。然而,回溯对括号的这种处理,不但需要同时维护$1的状态,也会影响匹配的效率。

最后需要注意的一点也许读者早就了解:由星号(或其他任何匹配优先量词)限定的部分不受后面元素影响,而只是匹配尽可能多的内容。在我们的例子中,「.*」在点号匹配失败之前,完全不知道,到底应该在哪个数字或者是其他什么地方停下来。在「^.*([0-9]+)」的例子中我们看到,「[0-9]+」只能匹配一位数字(F153)。
分享到:
评论

相关推荐

    精通正则表达式_第三版(高清版).

    正则表达式,也称为“正则式”,是一种文本模式,包括普通字符(例如,字母和数字)和特殊字符(称为“元字符”)。它使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。正则表达式在编程和文本处理领域...

    正则表达式必知必会_正则表达式_

    正则表达式是一种威力无比强大的武器,几乎在所有的程序设计语言里和计算机平台上都可以用它来完成各种复杂的文本处理工作。本书从简单的文本匹配开始,循序渐进地介绍了很多复杂内容,其中包括回溯引用、条件性求值...

    精通正则表达式_第三版(英文版)

    高级主题包括回溯控制、条件子表达式和正则表达式库。回溯控制可以优化匹配过程,避免不必要的计算。条件子表达式允许在正则表达式内部进行条件判断。正则表达式库则是预先定义的一系列模式,可以方便地复用和组合。...

    C语言正则表达式库

    3. **回溯算法**:PCRE库使用了高效的回溯算法来执行正则表达式匹配。虽然这可能会导致性能问题,但通过优化的匹配引擎和使用预编译模式,可以显著提高效率。 4. **匹配选项**:提供了许多可配置的匹配选项,例如不...

    精通正则表达式_第三版

    - 探讨正则表达式内部工作原理,如回溯算法等。 5. **Crafting a Regular Expression(构建正则表达式)** - 教授如何设计和编写高效的正则表达式。 6. **Tool-Specific Information(特定工具的信息)** -...

    tiny-regex-c-master_C语言_master表达式_最小正则表达_

    虽然它可能无法实现一些高级特性,如回溯、非贪婪匹配或预查等,但其基础功能足以处理许多常见的正则表达式操作。例如,它支持基本的字符类(如 `[a-z]` 表示小写字母)、量词(如 `*` 表示零个或多个前面的字符)、...

    Delphi2010正则表达式插件

    《Delphi 2010正则表达式插件详解》 在编程世界中,正则表达式(Regular Expression)是一种强大的文本处理工具,能够帮助开发者高效地进行字符串的匹配、查找、替换等操作。在Delphi 2010这个经典的集成开发环境中...

    正则表达式 必知必会 pdf

    本书基于各种实用场景,从基础的文本匹配开始,逐步深入到回溯引用、条件性求值以及前后查找等高级特性,使得读者能够系统、全面地掌握正则表达式的使用方法,并将其应用于解决实际问题中。 书中介绍的正则表达式...

    精通正则表达式(第三版)简体中文版

    ### 正则表达式基础知识与应用 #### 一、正则表达式的定义及用途 正则表达式(Regular Expression)是一种强大的文本处理工具,能够帮助用户查找、替换以及操作特定的字符串或字符组合。它在多种编程语言和操作...

    正则表达式调试工具

    2. **步骤跟踪**:通过逐步执行正则引擎的匹配过程,帮助开发者理解正则表达式的匹配逻辑,尤其是在面对复杂的嵌套或回溯情况时。 3. **经典例程**:内建了各种经典的正则表达式示例,方便学习和参考,有助于开发者...

    正则表达式学习资料以及练习项目代码很多

    - **正则表达式性能优化**:避免过度复杂的正则表达式,合理使用非贪婪匹配,减少回溯。 - **正则表达式调试**:使用`re.DEBUG`标志编译正则表达式,查看其内部结构。 - **正则表达式在其他语言中的差异**:虽然...

    源码(精通正则表达式&实战正则表达式)

    本资源“源码(精通正则表达式&实战正则表达式)”专注于JavaScript环境下的正则表达式学习,通过一系列视频教程和配套源码,帮助开发者提升对正则表达式的理解和应用能力。 首先,"精通正则表达式五部视频"可能涵盖...

    正则表达式_正则表达式_正则_

    正则表达式,简称为正则,是一种强大的文本处理工具,用于匹配、查找、替换等操作。它在编程、数据分析、网页抓取等领域有着广泛的应用。这篇个人总结将深入探讨正则表达式的核心概念、语法元素以及实际应用。 1. *...

    正则表达式工具类,正则表达式封装,Java正则表达式

    在Java编程语言中,正则表达式是一种强大的文本处理工具,用于匹配、查找、替换等操作。本节我们将深入探讨正则表达式工具类`RegUtils`,它封装了正则表达式的常用功能,便于在实际开发中进行复用。 首先,`...

    正则表达式综合练习

    正则表达式(Regular Expression,简称regex)是用于匹配字符串的一种模式,它在IT行业中扮演着重要的角色,尤其是在数据处理、文本分析、爬虫技术等领域。正则表达式通过使用预定义的字符集和特殊符号,可以高效地...

    正则式工具(自动生成正则表达式)

    正则式,全称为“正则表达式”,是编程领域中一种强大的文本处理工具,用于匹配、查找、替换和分析字符串。它通过一系列特定的字符和语法构建模式,可以高效地处理各种复杂的文本匹配任务。在软件开发、数据处理、...

    [精通正则表达式(第三版)].(美)佛瑞德.扫描版-带详细目录书签.pdf

    此外,书中详述了正则表达式的回溯机制,这是理解正则表达式效率和性能的关键。回溯是当匹配失败时,正则引擎退回到上一步尝试其他可能性的过程。掌握回溯原理有助于编写更高效、避免无限循环的正则表达式。 《精通...

    正则表达式验证器,验证常用的编程语言的正则表达式

    正则表达式(Regular Expression,简称regex)是用于匹配字符串的一种模式,广泛应用于文本处理、数据验证、搜索和替换等场景。在编程中,正确构造和理解正则表达式至关重要,因为它们能帮助我们高效地处理字符串...

    深入浅出之正则表达式

    正则表达式是一种强大的文本处理工具,用于模式匹配和数据提取。它们在编程语言、文本编辑器、搜索引擎等各类软件中广泛应用。本文将深入探讨正则表达式的基本概念、不同类型的正则表达式引擎,以及它们的内部工作...

Global site tag (gtag.js) - Google Analytics