【问题标题】:Regex matching characters of one string in another in any order正则表达式以任意顺序匹配另一个字符串中的字符
【发布时间】:2021-09-28 05:04:06
【问题描述】:

考虑以下字符串wizard。我想在任何情况下以任何顺序查找它是否在另一个字符串中。

我尝试了以下

while(<>){if($_=~/(?:([wizard])(?!.*\1)){6}/i){print"0"}else{print"1"}}

对于输入

Garry Kasparov
Bobby Fischer
Vladimir Kramnik
Wayne Drimaz
Lionel Messi
La Signora

打印的是111111,但肯定是111011

所以,我尝试了这个(对于相同的输入)

while(<>){if($_=~/(?=[wizard]{6})(?!.*(.).*\1).*/i){print"0"}else{print"1"}}

它再次打印了111111。 在输入数字 4 中,我们可以创建 WaDriaz,但只需要一个 a。无论如何,我们可以通过重新排列拼写wizard,无论如何。为什么它不起作用?

我的代码有什么问题?

【问题讨论】:

  • 添加use re 'debug' 并查看输出。
  • 或许/w/ &amp;&amp; /i/ &amp;&amp; /z/ &amp;&amp; /a/ &amp;&amp; /r/ &amp;&amp; /d/.
  • 问题:您认为latte 属于late,还是计数重要?
  • @ikegami 计数不重要请看这个>我们可以制作WaDriaz,但只需要一个a
  • @JohnBrookfields "有没有其他方法可以用一个正则表达式来做到这一点" ---嗯,是的,看看我的回答。

标签: regex perl


【解决方案1】:

这是一个纯正则表达式:为每个字符执行 positive lookahead

use warnings;
use strict;
use feature 'say';
use List::Util qw(uniq);  # before v. 1.45 in List::MoreUtils    

my $string = shift // q(wizard);

my $patt = join '', map { qq{(?=.*\Q$_\E)} } uniq split //, $string; 
# say $patt;  
#--> (?=.*w)(?=.*i)(?=.*z)(?=.*a)(?=.*r)(?=.*d)   (for wizard)

while (<DATA>) {
    say "Found '$string' in: $_" if /^$patt/is;
}

__DATA__
Garry Kasparov
Bobby Fischer
Vladimir Kramnik
Wayne Drimaz
Lionel Messi
La Signora

全部在一个正则表达式中,具有锚定的前瞻性并且没有开销,这应该非常快。

\Q...\E 用于防止搜索字符串包含正则表达式敏感字符。

请注意,此代码会查找具有重复字符的单词(latterareletter)以匹配没有重复字符的单词(later)。 cmets 明确了这确实是想要的行为:重复字符只需要在目标中找到一次(letter 匹配 later 等)。

【讨论】:

  • @ikegami 好点,谢谢。 (锚定特别有趣:)。已编辑,目前仅对重复字符进行注释。希望他们澄清。
【解决方案2】:

以下应该是相当快的(特别是如果你内联潜艇):

use feature qw( fc say );

sub make_key {
   my %counts;
   ++$counts{$_} for split //, fc($_[0]) =~ s/\PL//rg;
   return \%counts;
}

sub search {
   my ($substr, $str) = @_;
   $str = make_key($str);
   no warnings qw( uninitialized );
   return !( grep { $str->{$_} < $substr->{$_} } keys(%$substr) );
}

my $substr = make_key("wizard");
while (<>) {
   chomp;
   say search($substr, $_) ? 0 : 1;
}

与几乎所有以前的解决方案不同,这个解决方案不认为lattelate 中。


以下是基于正则表达式的解决方案(有一些准备)。这也应该很快(特别是如果你内联潜艇)。

use feature qw( fc say );

sub make_re {
   my $pat = join ".*?", map quotemeta, sort split //, fc($_[0]) =~ s/\PL//rg;
   return qr/$pat/s;
}

sub search {
   my ($substr, $str) = @_;
   return ( join "", sort split //, $str ) =~ $substr;
}

my $substr = make_re("wizard");   # qr/a.*?d.*?i.*?r.*?w.*?z/is
while (<>) {
   chomp;
   say search($substr, $_) ? 0 : 1;
}

最后,一个纯粹的基于正则表达式的解决方案。

use feature qw( fc say );

sub make_re {
   my %counts;
   ++$counts{$_} for split //, fc($_[0]) =~ s/\PL//rg;
   my $pat =
      join "",   
         map { "(?=".( ( ".*?" . quotemeta($_) ) x $counts{$_} ).")" }
            #sort
               keys(%counts);
   return qr/^$pat/s;
}

my $re = make_re("wizard");  # qr/^(?=.*?a)(?=.*?d)(?=.*?i)(?=.*?r)(?=.*?w)(?=.*?z)/is
while (<>) {
   say /$re/ ? 0 : 1;
}

与几乎所有以前的解决方案不同,我没有人认为lattelate 中。

【讨论】:

  • @TLP Re &lt;=,不。我们正在做!any { not_found() }
  • @TLP 我稍后在电脑上添加use feature
【解决方案3】:

我发现输入和模式的规范化是一种更通用和易于理解的方法:

#!/usr/bin/env perl

use strict;
use warnings;

sub canonchars {
    my %c;
    $c{$_} = undef for map lc, grep /\S/, split //, $_[0];
    sort keys %c;
}

sub pattern {
    map "$_.*", canonchars($_[0]);
}

my %canonical;

while (my $line = <DATA>) {
    last unless $line =~ /\S/;
    push $canonical{join '', canonchars($line)}->@*, $line;
}

my $pat = qr/@{[join '', pattern('wizard')]}/;

for my $k (keys %canonical) {
    if ($k =~ $pat) {
        print for $canonical{$k}->@*;
    }
}

__DATA__
Garry Kasparov
Bobby Fischer
Vladimir Kramnik
Wayne Drimaz
Lionel Messi
La Signora

输出:

C:\Temp> perl t.pl
Wayne Drimaz

您试图将很多逻辑融入正则表达式模式,而您发现和修复的每一个边缘情况都会使其更加复杂和脆弱。

【讨论】:

  • 说到边缘情况,如果他们想查找字符 "wizard+" 或其他在 RE 中特别的东西怎么办? (如果你想让它足够健壮来处理这样的事情,很容易解决,至少感谢\Q...\E)。
  • @Shawn 这就是为什么规范化和模式生成进入它们自己的可单独记录的方法。然后,更容易跟踪各个更改/增强功能。这只是一个插图。
  • 如果我没看错的话,这认为latte 可以在late 中找到。不确定是否可以。
【解决方案4】:

对每个角色使用积极的前瞻只是一件简单的事情。

my @stars = (
  'Garry Kasparov',
  'Bobby Fischer',
  'Vladimir Kramnik',
  'Wayne Drimaz',
  'Lionel Messi',
  'La Signora'
);

say /^(?=.*w)(?=.*i)(?=.*z)(?=.*a)(?=.*r)(?=.*d)/i ? 0 : 1 for @stars;

这会输出111011

【讨论】:

  • 要防止在late 中找到latte,请使用/^(?=.*a)(?=.*e)(?=.*l)(?=.*t.*t)/is
【解决方案5】:

不需要正则表达式...它们只会使事情复杂化,尤其是如果您不是在寻找提前知道的字符串时。在规范化他们的情况后,依次寻找每个字符。

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

sub contains_chars {
    my ($needle, $haystack) = @_;
    $haystack = lc $haystack;
    my %positions;
    for my $char (split //, lc $needle) {
        my $p = index $haystack, $char, $positions{$char}//0;
        return 1 if $p < 0;
        $positions{$char} = $p + 1;
    }
    return 0;
}

while (<DATA>) {
    print contains_chars("wIzArD", $_);
}
print "\n";

__DATA__
Garry Kasparov
Bobby Fischer
Vladimir Kramnik
Wayne Drimaz
Lionel Messi
La Signora

【讨论】:

  • 非常感谢@Shawn 但我已经有了一个没有正则表达式的解决方案。但我想用正则表达式来做。这就是我添加标签正则表达式的原因。
【解决方案6】:

这是一种可能发生乱序匹配的黑魔法。
源字符串中的每个字符只被访问一次。
无需为每个字母从头开始递归字符串。

use strict;
use warnings;

while (my $line = <DATA>) {
   if ( $line =~ /
     (?:
       .*?
       (?:
           (?(1)(?!))(w)
         | (?(2)(?!))(i)
         | (?(3)(?!))(z)
         | (?(4)(?!))(a)
         | (?(5)(?!))(r)
         | (?(6)(?!))(d)
       )
     ){6}/ix ) { print $line, "\n" }
}

__DATA__
Garry Kasparov
Bobby Fischer
Vladimir Kramnik
Wayne Drimaz
Lionel Messi
La Signora

韦恩·德里马兹

捕获可以以两种状态存在,已定义或未定义。
这种黑色艺术的本质是使用捕获状态作为标志
以确保所有项目都在乱序状态下匹配。

上面也可以这样写,结果相同。

use strict;
use warnings;

while (my $line = <DATA>) {
   if ( $line =~ /
     (?im)
     ^ 
     (?>
        .*? 
        (?:
           w ( )       # (1)
         | i ( )       # (2)
         | z ( )       # (3)
         | a ( )       # (4)
         | r ( )       # (5)
         | d ( )       # (6)
        )
     )+
     (?= \1 \2 \3 \4 \5 \6 )
   /x ) { print $line, "\n" }
}

__DATA__
Garry Kasparov
Bobby Fischer
Vladimir Kramnik
Wayne Drimaz
Lionel Messi
La Signora

【讨论】:

  • 如果我没看错的话,这认为latte 可以在late 中找到。不知道这样行不行。
  • @ikegami - 最慢的速度是纯前瞻速度的 1/3。最快的是很多倍。纯前瞻将更快地失败。如果纯前瞻实际上消耗了该行,它的速度将与标志版本相匹配。两个极端条件是: 1. 最后需要的字符相等,一个匹配然后在开始下一个匹配。 2.根本没有匹配。要知道为什么这些事情是真的,需要一些严肃的引擎知识。
  • 在第一个添加锚点和/s
  • 至于速度,只有基准测试才能说明问题。如果它区分大小写且仅 ASCII,则 Lookahead 可以很容易地更快。但是根据手头的场景和我上面提到的调整,你可能是对的。
【解决方案7】:

只需添加我在 cmets 中提到的 /w/ &amp;&amp; /i/ &amp;&amp; /z/ ... 解决方案的简单变体。如果您希望此解决方案匹配许多不同的字符串,而不是将正则表达式与&amp;&amp; 一起排列,您可以简单地循环字符。一个有用的工具是使用&amp;&amp;= 运算符来模拟一长串条件的行为。如果我们发现不匹配,这也将允许我们将匹配短路,从而为我们带来速度优势。

例如:

/a/ && /b/ && /c/

相当于

my $match = 1;
for my $w (qw(a b c)) {
    $match &&= (/$w/);    # $match = ($match && /$w/)
}

要记住字母的数量,即是否应将latte 视为late 的子字符串,您可以简单地使用替换运算符s/// 而不是匹配运算符m//。我添加了多字母条件,并添加了两个测试用例进行演示。

我喜欢这个解决方案,因为它很简单,但并不是说它是最好的。

use strict;
use warnings;

my $word = "wizzard";

while (<DATA>) {
    print search($_, $word), " $_";
}

sub search {
    my ($str, $substr) = @_;
    my $match = 1;                        # assume true
    for my $w (split //, $substr) {       # for each char in substr...
        $match &&= ($str =~ s/\Q$w//i);   # ...remove character
        return 0 if not $match;           # ...return false if no match found
    }
    return 1 if $match;
}

__DATA__
wizard
wizzard
Garry Kasparov
Bobby Fischer
Vladimir Kramnik
Wayne Drimaz
Wayne Drimazz
Lionel Messi
La Signora

输出:

0 wizard
1 wizzard
0 Garry Kasparov
0 Bobby Fischer
0 Vladimir Kramnik
0 Wayne Drimaz
1 Wayne Drimazz
0 Lionel Messi
0 La Signora

如果您不关心多字母匹配,只需将s/// 替换为m//

【讨论】:

    猜你喜欢
    • 2017-06-12
    • 1970-01-01
    • 1970-01-01
    • 2015-08-19
    • 2013-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-22
    相关资源
    最近更新 更多