【问题标题】:Regex to match the longest repeating substring正则表达式匹配最长的重复子字符串
【发布时间】:2012-03-02 06:33:21
【问题描述】:

我正在编写正则表达式来检查是否有一个子字符串,该子字符串包含至少 2 个彼此相邻的某些模式的重复。我将正则表达式的结果与前一个字符串匹配 - 如果相等,则存在这样的模式。最好举个例子:1010 包含模式 10,它在连续系列中出现 2 次。另一方面,10210 不会有这样的模式,因为这 10 个不相邻。

更重要的是,我需要找到可能的最长模式,并且它的长度至少为 1。我已经编写了表达式来检查它^.*?(.+)(\1).*?$。为了找到最长的模式,我在模式之前使用了非贪婪版本来匹配某些东西,然后模式匹配到组 1,并且再次匹配与组 1 匹配的相同的东西。然后匹配字符串的其余部分,产生相等的字符串。但是存在一个问题,即正则表达式在找到第一个模式后急于返回,并且没有真正考虑到我打算在尽可能短的前后使这些子字符串变得尽可能短(让其余的尽可能长)。所以从字符串01011010 我得到了正确的匹配,但是存储在第1组中的模式只是01,尽管我会除了101

因为我相信我不能在“更不贪婪”之前和之后让模式“更贪婪”或垃圾,所以我只能想出一个让正则表达式不那么渴望的想法,但我不确定这是否是可能的。

更多示例:

56712453289 - no pattern - no match with former string
22010110100 - pattern 101 - match with former string (regex resulted in 22010110100 with 101 in group 1)
5555555 - pattern 555 - match
1919191919 - pattern 1919 - match
191919191919 - pattern 191919 - match
2323191919191919 - pattern 191919 - match

使用当前表达式我会得到什么(使用相同的字符串):

no pattern - no match
pattern 2 - match
pattern 555 - match
pattern 1919 - match
pattern 191919 - match
pattern 23 - match

【问题讨论】:

  • 如果你提供一系列的例子和你想要的结果,这个问题会更容易回答。
  • @JohnFeminella 给定一个字符串,他想找到与(.+)(\1) 模式匹配的最长子字符串。所以对于输入yabyababyab,他想找到子字符串abyababyab(而不是yabyab)。
  • /^.*?(.+)(\1).*?$//(.+)(\1)/ 相同。 (除了第一个跑得慢)
  • @BradGilbert true,但仅在查找重复子字符串的上下文中。第一个将执行此操作并生成与前一个字符串匹配的字符串(这意味着如果没有找到模式,则这两者之间将不会匹配).. 但是如果您告诉检查零大小组,您将再次成为真的1 会是更好的选择,这就是问题首先被解决的方式。
  • @Raven 我的陈述唯一不正确的情况是当有一个嵌入的换行符,而你没有设置/m or /s。 (除非您正在谈论设置 $&,否则您可能不应该使用它。)

标签: regex perl pattern-matching


【解决方案1】:

在 Perl 中,您可以在 (??{ code }) 的帮助下使用一个表达式来完成:

$_ = '01011010';
say /(?=(.+)\1)(?!(??{ '.+?(..{' . length($^N) . ',})\1' }))/;

输出:

101

这里发生的是,在匹配连续的一对子字符串之后,我们用否定的前瞻来确保它后面不再有对。

为了使更长的对的表达式使用延迟子表达式构造(??{ code }),它(每次)评估内部代码并将返回的字符串用作表达式。

它构造的子表达式的形式为.+?(..{N,})\1,其中N是第一个捕获组的当前长度(length($^N)$^N包含前一个捕获组的当前值)。

因此完整的表达式将具有以下形式:

(?=(.+)\1)(?!.+?(..{N,})\2}))

使用神奇的N(并且第二个捕获组不是原始表达式的“真实”/正确捕获组)。


Usage example:

use v5.10;

sub longest_rep{
    $_[0] =~ /(?=(.+)\1)(?!(??{ '.+?(..{' . length($^N) . ',})\1' }))/;
}

say longest_rep '01011010';
say longest_rep '010110101000110001';
say longest_rep '2323191919191919';
say longest_rep '22010110100';

输出:

101
10001
191919
101

【讨论】:

  • 这是一个令人惊叹的正则表达式,如果您能更详细地解释它,我会很感兴趣。但是,对于 232319191919191922010110100 的值,它不适用于我,因为它们的匹配时间较短。
  • @TLP,添加了描述。它对我有用,你得到了什么?
  • perl -lnwe '/(?=(.+)\1)(?!(??{ ".+?(.{" . (1+length $^N) . ",})\1" }))\1/ && print $1;' 在 shell 中运行,我粘贴了 OP 给出的数字。请注意,我更改了单引号。
  • 似乎也对我有用,要测试其他一些案例,但这令人印象深刻!
  • @Qtax 在脚本中使用时似乎可以工作,所以我猜我的测试已经关闭。
【解决方案2】:

可以在单个正则表达式中执行此操作,您只需手动从结果列表中选择最长的匹配项。

def longestrepeating(strg):
    regex = re.compile(r"(?=(.+)\1)")
    matches = regex.findall(strg)
    if matches:
        return max(matches, key=len)

这给了你(因为re.findall() 返回匹配捕获组的列表,即使匹配本身是零长度):

>>> longestrepeating("yabyababyab")
'abyab'
>>> longestrepeating("10100101")
'010'
>>> strings = ["56712453289", "22010110100", "5555555", "1919191919", 
               "191919191919", "2323191919191919"]
>>> [longestrepeating(s) for s in strings]
[None, '101', '555', '1919', '191919', '191919']

【讨论】:

  • 这看起来像 Python 而不是 Perl。
  • @Pavel:你说得对,我一定忽略了 Perl 标签。有趣的是,您是 5 年后第一个注意到的人。无论如何我都会留下答案,因为我认为它很容易理解并且可以应用于除 Perl 之外的许多语言。
【解决方案3】:

这是一个很长的脚本,可以满足您的要求。它基本上会遍历您的输入字符串,将其缩短一个,然后再次遍历它。一旦找到所有可能的匹配项,它就会返回最长的匹配项之一。可以对其进行调整,以便返回所有最长的匹配项,而不仅仅是一个,但我将把它留给你。

这是非常基本的代码,但希望您能掌握其中的要点。

use v5.10;
use strict;
use warnings;

while (<DATA>) {
    chomp;
    print "$_ : ";
    my $longest = foo($_);
    if ($longest) {
        say $longest;
    } else {
        say "No matches found";
    }
}

sub foo {
    my $num = shift;
    my @hits;
    for my $i (0 .. length($num)) {
        my $part = substr $num, $i;
        push @hits, $part =~ /(.+)(?=\1)/g;
    }
    my $long = shift @hits;
    for (@hits) {
        if (length($long) < length) {
            $long = $_;
        }
    }
    return $long;
}

__DATA__
56712453289
22010110100
5555555
1919191919
191919191919
2323191919191919

【讨论】:

    【解决方案4】:

    不知道有没有人想到这个……

    my $originalstring="pdxabababqababqh1234112341";
    
    my $max=int(length($originalstring)/2);
    my @result;
    foreach my $n (reverse(1..$max)) {
        @result=$originalstring=~m/(.{$n})\1/g;
        last if @result;
    }
    
    print join(",",@result),"\n";
    

    最长的双重匹配不能超过原始字符串长度的一半,所以我们从那里开始倒数。

    如果怀疑匹配项相对于原始字符串的长度很小,那么可以颠倒这个想法……而不是倒数直到找到匹配项,我们倒数直到没有更多匹配项。然后我们需要备份 1 并给出结果。我们还需要在正则表达式中的 $n 之后放置一个逗号。

    my $n;
    foreach (1..$max) {
        unless (@result=$originalstring=~m/(.{$_,})\1/g) {
            $n=--$_;
            last;
        }
    }
    @result=$originalstring=~m/(.{$n})\1/g;
    
    print join(",",@result),"\n";
    

    【讨论】:

      【解决方案5】:

      正则表达式可以帮助解决这个问题,但我认为您不能将其作为单个表达式来完成,因为您想找到 最长成功 匹配,而正则表达式只是寻找他们能找到的第一场比赛。贪婪可用于调整首先找到哪个匹配项(在字符串中较早与较晚),但我想不出一种方法更喜欢 earlier, long 子字符串而不是 later , 更短的 子字符串,而 更喜欢稍后,更长 的子字符串,而不是更早,更短的 子字符串。

      使用正则表达式的一种方法是按降序迭代可能的长度,并在找到指定长度的匹配项后立即退出:

      my $s = '01011010';
      my $one = undef;
      for(my $i = int (length($s) / 2); $i > 0; --$i)
      {
        if($s =~ m/(.{$i})\1/)
        {
          $one = $1;
          last;
        }
      }
      # now $one is '101'
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-02-15
        • 2013-05-11
        • 2023-03-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多