正则表达式查找两个字符串的最长公共前缀
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) 吗? 对于 rubyist:(/^(.*).*\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:特技程序员。喜欢练习极限编程。
不幸的是,它不处理这种情况,其中a
是b
的前缀,例如: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]
【讨论】:
除非a
和b
足够大,以至于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])
【讨论】:
正则表达式在哪里?以上是关于正则表达式查找两个字符串的最长公共前缀的主要内容,如果未能解决你的问题,请参考以下文章