正则表达式查找两个字符串的最长公共前缀

Posted

技术标签:

【中文标题】正则表达式查找两个字符串的最长公共前缀【英文标题】:Regexp finding longest common prefix of two strings 【发布时间】:2012-02-25 05:30:58 【问题描述】:

是否有一个正则表达式可以找到两个字符串的最长公共前缀?如果这不能通过一个正则表达式来解决,那么使用正则表达式(perl、ruby、python 等等)的最优雅的代码或单行代码将是什么。

PS:我可以通过编程轻松地做到这一点,我只是出于好奇,因为在我看来这可以通过正则表达式来解决。

PPS:使用正则表达式的 O(n) 解决方案的额外奖励。来吧,它应该存在的!

【问题讨论】:

我认为这不可能。使用 RE,您可以查看一段数据(字符串)是否与表达式匹配(RE:如果您愿意,则为程序)。您现在必须处理数据——(正确的)RE 也不是。要找到最长的公共前缀,您需要将两者都作为输入的“某物”……但 RE 不这样做:需要一些胶水。 我对胶水解决方案很满意 - 也许将其中一个字符串转换为正则表达式,然后在第二个字符串上使用它...... 这两个字符串是随机输入还是其中一个有权限?在第二种情况下(例如,字符串被多次重复用于相同的匹配)一些优化是值得做的。 另见:***.com/questions/7475437/…(php;不确定它是否也适用于 Python) 我下面的解决方案解决了外星生命形式提出的问题。你有两个输入和一个输出。 Perl 风格的伪正则表达式不允许这样做。但更通用的方法是可能的。另一个奇怪地未被提及的问题是 lcp 最常用于后缀数组的上下文中,在此上下文中,有用于查找完整 lcp 表的线性时间算法。最著名的是 Ko 等人。 【参考方案1】:

如果存在两个字符串都不包含的字符——比如\0——你可以写

"$first\0$second" =~ m/^(.*).*\0\1/s;

最长的公共前缀将被保存为$1


编辑添加:这显然是非常低效的。我认为如果效率是一个问题,那么这根本不是我们应该使用的方法;但我们至少可以通过将.* 更改为[^\0]* 来改进它,以防止不得不再次回溯的无用贪婪,并将第二个[^\0]* 包装在(?>…) 中以防止无济于事的回溯。这个:

"$first\0$second" =~ m/^([^\0]*)(?>[^\0]*)\0\1/s;

这将产生相同的结果,但效率更高。 (但仍然没有像直接的非基于正则表达式的方法那样有效地 as。如果字符串都有长度 n,我希望它的最坏情况至少需要O(n2) 时间,而直接的非基于正则表达式的方法将花费 O(n) 时间 its 最坏的情况。)

【讨论】:

哇,好主意——太简单了,我没想过要加入他们。我不完全理解为什么它会匹配最长的部分,但我会考虑一下:-) +1:聪明但昂贵。一个 RE 不是解决问题的方法,即使这实现了结果 - 受制于接受字符串中嵌入的空值的正则表达式。 (这不是一个致命的反对意见;您可以使用没有出现在任一字符串中的任何字符序列作为分隔符。) 昂贵 - 同意,但这很好。我要求优雅,而不是快速。 顺便说一句——这真的有多贵?分隔符有助于回溯引擎非常有效地工作,所以它不只是 O(string lentgth) 吗? 对于 ruby​​ist:(/^(.*).*\0\1/s).match(first+"\0"+second).to_a[1]【参考方案2】:

这是一个 Python 单行代码:

>>> a = '***'
>>> b = 'stackofpancakes'
>>> a[:[x[0]==x[1] for x in zip(a,b)].index(0)]
0: 'stacko'
>>> a = 'nothing in'
>>> b = 'common'
>>> a[:[x[0]==x[1] for x in zip(a,b)].index(0)]
1: ''
>>> 

【讨论】:

可爱。解释器结果中的0:1: 是什么?您是否在使用某种增强的 Python shell 来对输出进行编号之类的? Stack-O:特技程序员。喜欢练习极限编程。 不幸的是,它不处理这种情况,其中ab 的前缀,例如:a, b = 'test', 'testing'。它会抛出一个ValueError,因为0 不会在列表中。 @DawidFatyga:是的,但这很容易解决:a[:([x[0]==x[1] for x in zip(a,b)]+[0]).index(0)] 虽然这看起来不错,但我要求提供正则表达式解决方案,所以这里不适用。【参考方案3】:

非正则表达式,每次迭代解决方案不重复字符串:

def common_prefix(a, b):
   #sort strings so that we loop on the shorter one
   a, b = sorted((a,b), key=len)
   for index, letter in a:
      if letter != b[index]:
          return a[:index - 1]
   return a

【讨论】:

感谢干净整洁的代码。但是,我宁愿寻找纯粹用于心理锻炼的正则表达式解决方案,在这种情况下,您建议的解决方案可能更实用。不过有些答案很有创意……【参考方案4】:

您将遇到的问题是正则表达式一次匹配一个字符串,因此不适用于比较两个字符串。

如果您可以确定某个字符不在任一字符串中,您可以使用它将它们分隔在一个字符串中,然后使用对组的反向引用进行搜索。

所以在下面的示例中,我使用空格作为分隔符

>>> import re
>>> pattern = re.compile("(?P<prefix>\S*)\S*\s+(?P=prefix)")
>>> pattern.match("stack stable").group('prefix')
'sta'
>>> pattern.match("123456 12345").group('prefix')
'12345'

【讨论】:

是的,这是一个很好的答案。我已经接受了另一个具有相同基本想法的人,但谢谢。【参考方案5】:

我认为这是最低效的。没有错误检查等。

#!/usr/bin/perl
use strict;
use warnings;

my($s1,$s2)=(@ARGV);
#find the shortest string put it into s1, if you will.

my $n=0;
my $reg;

foreach my $c (split(//,$s1))  $reg .="($c"; $n++;

$reg .= ")?" x $n;

$s2 =~ /$reg/; 

print $&,"\n";

【讨论】:

1) OP 特别要求提供基于正则表达式的解决方案。 2)看到我的代码中的第一条评论,我懒得写代码来做到这一点; 3) 阅读 TFQ 很少会受到伤害。【参考方案6】:

我第二个 ruakh 对正则表达式的回答(我建议在 cmets 中进行优化)。编写简单,但如果第一个字符串很长,则运行起来并不简单高效。

这是一个高效、非正则表达式、可读、单行的答案:

$ perl -E '($n,$l)=(0,length $ARGV[0]); while ($n < $l)  $s = substr($ARGV[0], $n, 1); last if $s ne substr($ARGV[1], $n, 1); $n++  say substr($ARGV[0], 0, $n)' abce abcdef
abc

【讨论】:

【参考方案7】:

简单高效

def common_prefix(a,b):
  i = 0
  for i, (x, y) in enumerate(zip(a,b)):
    if x!=y: break
  return a[:i]

【讨论】:

除非ab 足够大,以至于a+b 不适合内存;-) itertools.izip 是这里更好的选择。【参考方案8】:

受 ruakh 的回答启发,这里是 O(n) 正则表达式解决方案:

"$first \0$second" =~ m/^(.*?)(.).*\0\1(?!\2)/s;

注意事项: 1. 两个字符串都不包含 \0 2.最长公共前缀将保存为$1 3. 空间很重要!

编辑: rukach metions 是不正确的,但想法是正确的,但我们应该推动正则表达式机器不要重复检查开头的字母。基本思想也可以用这个 perl oneliner 重写。

perl -e ' $_="$first\0$second\n"; while(s/^(.)(.*?)\0\1/\2\0/gs) print $1;; '

我想知道它是否可以重新合并到正则表达式解决方案中。

【讨论】:

至少仍然是 O(n²)。最坏的情况发生在$first$second 相同时:然后引擎需要测试$first 的每个前缀。如果一个前缀的长度是m,那么测试它至少是O(m),所以最坏情况的时间至少是O(0+1+2+...+n),这是 O(n²)。 (另外,如果$second$first 后跟一个空格开头,那会有点不正常,因为那时\2 总是匹配,所以(?!\2) 永远不会成功。但这也是可以补救的再次使用\0 而不是空格——因为假设$second 不包含\0——或者说最长的公共前缀是$1 // $first 而不是简单的$1。) 确实,我没有考虑这样一个事实,即随着前缀的变长,每次都会从头开始重新检查。但这不是必需的 - 它可以只检查它正在添加的字母,但我不知道如何将它放在一个正则表达式中。 (不,你错了,是的,角箱很烦人 :-) 双\0,当我快速尝试时不起作用,所以我引入了空间,我相信它有效。如果 $second$first 和空格开头,则 if 将不匹配,因为如果 \2 吃掉 \0 并且 \0 无法匹配。 ) (是的,对于\0\0,您需要进一步调整它,否则(.*?) 的长度可能为零,然后\0\1(?!\2) 将匹配第一个\0,因为(?=\0) 意味着 (?!\2)。我认为将 \0\1(?!\2) 更改为 \0(?!\0)\1(?!\2) 就足够了。)【参考方案9】:

这是一种使用正则表达式的相当有效的方法。代码是 Perl 的,但原则上应该可以适配其他语言:

my $xor = "$first" ^ "$second";    # quotes force string xor even for numbers
$xor =~ /^\0*/;                    # match leading null characters
my $common_prefix_length = $+[0];  # get length of match

(值得注意的一个微妙之处是 Perl 的字符串异或运算符 (^) 实际上用空值填充较短的字符串以匹配较长字符串的长度。因此,如果字符串可能包含空字符, 如果较短的字符串恰好是较长字符串的前缀,则使用此代码计算的公共前缀长度可能会超过较短字符串的长度。)

【讨论】:

非常好的技巧!我没有找到在 ruby​​ 思想中做字符串异或的简短优雅的方法。 sub LCP my $match = shift; for (@_) ($match ^ $_) =~ /^\0*/; substr($match, $+[0]) = ''; $match; gist.github.com/3309172【参考方案10】:

O(n) 解的另一种尝试:

$x=length($first); $_="$first\0$second"; s/((.)(?!.$x\2)).*//s;

这取决于 .n 被认为是 O(1) 还是 O(n),我不知道它的实现效率如何。

注意:1. \0 不应在任何一个字符串中,它用作分隔符 2. 结果在 $_ 中

【讨论】:

它可以优化,但这是主要思想没有被掩盖 +1,聪明!虽然,一个小点:你需要添加/s(为了正确,甚至可以想象.n是O(1)——没有/s.n至少必须扫描对于换行符,除非正则表达式引擎进行某种预检查以确保没有任何内容)。你可以放弃/g,虽然我认为它没有害处。 是的,抱歉,应该有 s 而不是 g,已更正。总而言之,它似乎有很多有趣的解决方案:-) ...我真正疯狂的想法之一是反转其中一个字符串,而不是从中间吃掉双倍的字符:-)【参考方案11】:

使用 Foma 或 Xfst 中的扩展正则表达式。

def range(x) x.l;
def longest(L) L - range(range(L ∘ [[Σ:ε]+ [Σ:a]*]) ∘ [a:Σ]*); 
def prefix(W) range(W ∘ [Σ* Σ*:ε]);
def lcp(A,B) longest(prefix(A) ∩ prefix(B));

这里最难的部分是定义“最长”。一般来说,对 优化,您构建一组非最佳字符串(恶化)和 然后删除这些(过滤)。

这确实是一种纯粹的方法,它避免了非常规操作 这样的捕捉。

【讨论】:

【参考方案12】:

这是一个 O(N) 的解决方案,在三元组上使用类似 Foma 的伪代码正则表达式(对于 lcp,您有两个输入和一个输出)。为简单起见,我假设一个二进制字母 a,b:

def match a:a:a, b:b:b;
def mismatch a:b:ε, b:a:ε;
def lcp match* ∪ (match* mismatch (Σ:Σ:ε)*)

现在您只需要一种实现多磁带转换器的语言。

【讨论】:

谢谢汤姆。从本质上讲,这就是问题所要求的,我相信大多数其他答案都是试图破解 3 磁带传感器而不给设备命名。一旦有了名字,就可以找到各种语言的各种实现。你可以找到关于闭包和可判定性等的论文【参考方案13】:

在某些远程情况下可能很有用,所以这里是:

RegEx 3 个步骤的唯一解决方案(无法一次性创建 RegEx):

字符串 A:abcdef 字符串 B:abcxef

第一遍:从 String A 创建 RegEx (第 1 部分): 匹配:/(.)/g 替换:\1( 结果:a(b(c(d(e(f( 解释演示:http://regex101.com/r/aJ4pY7

第二遍:从1st pass result创建正则表达式 匹配:/^(.\()(?=(.*)$)|\G.\(/g 替换:\1\2)?+ 结果:a(b(c(d(e(f()?+)?+)?+)?+)?+)?+ 解释演示:http://regex101.com/r/xJ7bK7

第三次通过:针对在 2nd pass 中创建的 RegEx 测试 String B 匹配:/a(b(c(d(e(f()?+)?+)?+)?+)?+)?+/ 结果:abc (explained demo)

这是 PHP 中的美化 one-liner

preg_match('/^'.preg_replace('/^(.\()(?=(.*)$)|\G.\(/','\1\2)?+',preg_replace('/(.)/','\1(',$a)).'/',$b,$longest);

代码直播地址:http://codepad.viper-7.com/dCrqLa

【讨论】:

我必须承认,虽然它有点“蛮力”,但同时也有一些优雅。谢谢你,特别是我喜欢积极的前瞻正则表达式的第二步。【参考方案14】:

这是我为一个 leetcode 问题实现的解决方案:

def max_len(strs):
    """
    :type strs: List[str]
    :rtype: int
    """
    min_s = len(strs[0]);
    for s in strs:
        if (len(s) < min_s):
            min_s = len(s);
    return min_s;


class Solution2:
    def longestCommonPrefix(self, strs):
    """
    :type strs: List[str]
    :rtype: str
    """
    acc = -1;
    test_len = max_len(strs);
    for i in range(test_len):
        t = strs[0][i];
        acc2 = 0;
        for j in range(len(strs)):
            if (strs[j][i] == t):
                acc2 += 1;
        if (acc2 == len(strs)):
            acc += 1;

    if (acc == -1):
        return ""
    else:
        return strs[0][:acc + 1]

希望对你有帮助

【讨论】:

这不使用正则表达式,因为我制定了原始请求,但谢谢。【参考方案15】:
string1=input()
string2=input()
string1=string1.lower()
string2=string2.lower()
l1=len(string1)
l2=len(string2)
min_len=min(l1,l2)
for i in range(min_len):
    if string1[i]!=string2[i]:
        break
if i==0:
    print(-1)
else:
    print(string2[:i])

【讨论】:

正则表达式在哪里?

以上是关于正则表达式查找两个字符串的最长公共前缀的主要内容,如果未能解决你的问题,请参考以下文章

修改正则表达式以在带有或不带有 http 前缀的字符串中查找 URL [重复]

PHP的正则表达式

正则表达式将字符串限制为最短匹配与最长匹配(非贪婪组)?

PHP正则表达式的使用技巧

正则表达式在 Python 中查找两个字符串之间的字符串

nginx之location介绍