正则习点 --- 11

4.4. Backtracking

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

4.4.1. A Recally Crummy Analogy

Backtracking is like leaving a pile ofbread crumbs at every fork in the road.(回溯就像是在道路的每个分岔口留下一小堆面包屑)

4.4.1.1. A crummy little example

用正则表达式「to(nite|knight|night)」匹配字符串’hot·tonic·tonight!’。第一个元素「t」从字符串的最左端开始尝试,因为无法匹配’h’,所以在这个位置匹配失败。传动装置于是驱动引擎向后移动,从第二个位置开始匹配(同样也会失败),然后是第三个。这时候「t」能够匹配,接下来的「o」无法匹配,因为字符串中对应位置是一个空格。至此,本轮尝试宣告失败。

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

4.4.2. Two Important Points onBacktracking

²In situations where thedecision is between “make an attempt” and “skip an attempt,” as with itemsgoverned by quantifiers, the engine always chooses to first make the attemptfor greedy quantifiers, and to first skip the attempt for lazy(non-greedy) ones.

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

The most recently saved option is the onereturned to when a local failure forces backtracking. They’re used LIFO(last infirst out).

距离当前最近储存的选项就是当本地失败强制回溯时返回的。使用的原则是LIFO。

4.4.3 Saved States

用NFA正则表达式的术语来说,那些面包屑相当于“备用状态(savedstate)”。

A state indicates where matching canrestart from, if need be. It reflects both the position in the regex and thepoint in the string where an untried option begins.

他们用来标记:在需要的时候,匹配可以从这里重新开始尝试。他们保存了两个位置:正则表达式中的位置,和未尝试的分支在字符串中的位置。

4.4.3.1. A match without backtracking

用「ab?c」匹配abc。

「a」匹配之后,匹配的当前状态如下:

‘abc’

「ab?c」

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

‘abc’

「ab?c」

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

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

‘abc’

「ab?c」

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

4.4.3.2. A match after backtracking

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

‘ac’

「ab?c」

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

4.4.3.3. A non-match

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

‘abX’

「ab?c」

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

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

‘abX’

ab?c」

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

4.4.3.4. A lazy match

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

‘abc’

「ab??c」

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

‘abc’

「abc」

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

‘abc’

「ab?? c」

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

‘abc’

「abc」

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

4.4.4. Backtracking andGreediness

4.4.4.1. Star, plus, and their backtracking

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

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

a 1234num

a 1234 num

a 1234 num

a 1234 num

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

4.4.4.2. Revisiting a fuller example

以正则表达式「^.*([0-9][0-9])」匹配‘CA·95472·USA’为例,在「.*」成功匹配到字符串的末尾时,星号约束的点号匹配了13个字符,同时保存了许多备用状态。

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

这种“回溯-尝试”(backtrack-and-test)的过程会不断循环,直到引擎交还“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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值