【问题标题】:How do I determine the longest similar portion of several strings?如何确定几个字符串的最长相似部分?
【发布时间】:2010-10-04 17:44:00
【问题描述】:

根据标题,我正在尝试找到一种方法来以编程方式确定几个字符串之间最长的相似部分。

例子:

  • file:///home/gms8994/Music/t.A.T.u./
  • file:///home/gms8994/Music/nina%20sky/
  • file:///home/gms8994/Music/A%20Perfect%20Circle/

理想情况下,我会回复 file:///home/gms8994/Music/,因为这是所有 3 个字符串共有的最长部分。

具体来说,我正在寻找 Perl 解决方案,但任何语言(甚至伪语言)的解决方案都足够了。

来自 cmets:是的,只是在开始;但有可能在列表中有其他条目,对于这个问题将被忽略。

【问题讨论】:

  • 相似度必须从字符串的开头开始吗?如果是这样,很容易解决。如果不是,那就有点复杂了。
  • 同上那个查询——我会补充一下——“相似”是指“精确”吗?
  • 您提出的问题不明确。首先,相似是否意味着精确。此外,例如,如果前 15 个字符共有 10 个字符串,这 10 个字符串中的另外 5 个字符串对于另外 7 个字符是常见的,你想要哪个前缀?

标签: perl algorithm string similarity


【解决方案1】:

听起来你想要k-common substring algorithm。它的编程非常简单,是动态编程的一个很好的例子。

【讨论】:

  • 问题不是子串而是前缀。对于请求的问题,子字符串查找算法更加复杂和低效。
【解决方案2】:

如果您在谷歌上搜索“最长的公共子字符串”,您会得到一些很好的指示,以了解序列不必从字符串开头开始的一般情况。 例如,http://en.wikipedia.org/wiki/Longest_common_substring_problem

Mathematica 恰好有一个内置函数: http://reference.wolfram.com/mathematica/ref/LongestCommonSubsequence.html(注意它们的意思是连续子序列,即子串,就是你想要的。)

如果您只关心最长的公共前缀,那么从 0 循环 for i 直到第 i 个字符不完全匹配并返回 substr(s, 0, i-1) 应该会快得多。

【讨论】:

  • 问题不是子串而是前缀。对于请求的问题,子字符串查找算法更加复杂且效率低下。
  • 没错,我只是认为最好为那些稍后搜索此内容的人提供最一般的答案。我将编辑我的答案,指出如果你只需要最长的公共前缀会更快。
【解决方案3】:

我的第一直觉是运行一个循环,从每个字符串中取出下一个字符,直到字符不相等。记下你在字符串中的位置,然后从 0 到字符不相等之前的位置取一个子字符串(来自三个字符串中的任何一个)。

在 Perl 中,您必须首先使用类似的方式将字符串拆分为字符

@array = split(//, $string);

(在一个空字符上拆分将每个字符设置为它自己的数组元素)

然后做一个循环,也许是整体:

$n =0;
@array1 = split(//, $string1);
@array2 = split(//, $string2);
@array3 = split(//, $string3);

while($array1[$n] == $array2[$n] && $array2[$n] == $array3[$n]){
 $n++; 
}

$sameString = substr($string1, 0, $n); #n might have to be n-1

或者至少是类似的东西。如果这不起作用,请原谅我,我的 Perl 有点生锈了。

【讨论】:

    【解决方案4】:

    Brett Daniel 已经为“Longest common substring problem”上的 Wikipedia 条目提供的参考是非常好的通用参考(带有伪代码),如您所述。然而,该算法可以是指数的。看起来你可能真的想要一个最长公共前缀的算法,这是一个更简单的算法。

    这是我用于最长公共前缀(以及原始 URL 的引用)的那个:

    use strict; use warnings;
    sub longest_common_prefix {
        # longest_common_prefix( $|@ ): returns $
        # URLref: http://linux.seindal.dk/2005/09/09/longest-common-prefix-in-perl
        # find longest common prefix of scalar list
        my $prefix = shift;
        for (@_) {
            chop $prefix while (! /^\Q$prefix\E/);
            }
        return $prefix;
    }
    
    my @str = map {chomp; $_} <DATA>;
    print longest_common_prefix(@ARGV), "\n";
    __DATA__
    file:///home/gms8994/Music/t.A.T.u./
    file:///home/gms8994/Music/nina%20sky/
    file:///home/gms8994/Music/A%20Perfect%20Circle/
    

    如果您真的想要 LCSS 实现,请参阅 PerlMonks.org 上的这些讨论(Longest Common SubstringLongest Common Subsequence)。 Tree::Suffix 可能是您最好的通用解决方案,据我所知,它实现了最好的算法。不幸的是,最近的构建被破坏了。但是,在这个 post by Limbic~Region 中引用的 PerlMonks 讨论中确实存在一个工作子例程(在此处与您的数据一起复制)。

    #URLref: http://www.perlmonks.org/?node_id=549876
    #by Limbic~Region
    use Algorithm::Loops 'NestedLoops';
    use List::Util 'reduce';
    
    use strict; use warnings;
    
    sub LCS{
        my @str = @_;
        my @pos;
        for my $i (0 .. $#str) {
            my $line = $str[$i];
            for (0 .. length($line) - 1) {
                my $char= substr($line, $_, 1);
                push @{$pos[$i]{$char}}, $_;
            }
        }
        my $sh_str = reduce {length($a) < length($b) ? $a : $b} @str;
        my %map;
        CHAR:
        for my $char (split //, $sh_str) {
            my @loop;
            for (0 .. $#pos) {
                next CHAR if ! $pos[$_]{$char};
                push @loop, $pos[$_]{$char};
            }
            my $next = NestedLoops([@loop]);
            while (my @char_map = $next->()) {
                my $key = join '-', @char_map;
                $map{$key} = $char;
            }
        }
        my @pile;
        for my $seq (keys %map) {
            push @pile, $map{$seq};
            for (1 .. 2) {
                my $dir = $_ % 2 ? 1 : -1;
                my @offset = split /-/, $seq;
                $_ += $dir for @offset;
                my $next = join '-', @offset;
                while (exists $map{$next}) {
                    $pile[-1] = $dir > 0 ?
                        $pile[-1] . $map{$next} : $map{$next} . $pile[-1];
                    $_ += $dir for @offset;
                    $next = join '-', @offset;
                }
            }
        }
        return reduce {length($a) > length($b) ? $a : $b} @pile;
    }
    
    my @str = map {chomp; $_} <DATA>;
    print LCS(@str), "\n";
    __DATA__
    file:///home/gms8994/Music/t.A.T.u./
    file:///home/gms8994/Music/nina%20sky/
    file:///home/gms8994/Music/A%20Perfect%20Circle/
    

    【讨论】:

      【解决方案5】:

      编辑:我很抱歉犯了错误。遗憾的是,我发现在 countit(x, q{}) 中使用 my 变量是个大错误。这个字符串在 Benchmark 模块中被评估,@str 在那里是空的。这个解决方案没有我提出的那么快。请参阅下面的更正。再次抱歉。

      Perl 可以很快:

      use strict;
      use warnings;
      
      package LCP;
      
      sub LCP {
          return '' unless @_;
          return $_[0] if @_ == 1;
          my $i          = 0;
          my $first      = shift;
          my $min_length = length($first);
          foreach (@_) {
              $min_length = length($_) if length($_) < $min_length;
          }
      INDEX: foreach my $ch ( split //, $first ) {
              last INDEX unless $i < $min_length;
              foreach my $string (@_) {
                  last INDEX if substr($string, $i, 1) ne $ch;
              }
          }
          continue { $i++ }
          return substr $first, 0, $i;
      }
      
      # Roy's implementation
      sub LCP2 {
          return '' unless @_;
          my $prefix = shift;
          for (@_) {
              chop $prefix while (! /^\Q$prefix\E/);
              }
          return $prefix;
      }
      
      1;
      

      测试套件:

      #!/usr/bin/env perl
      
      use strict;
      use warnings;
      
      Test::LCP->runtests;
      
      package Test::LCP;
      
      use base 'Test::Class';
      use Test::More;
      use Benchmark qw(:all :hireswallclock);
      
      sub test_use : Test(startup => 1) {
          use_ok('LCP');
      }
      
      sub test_lcp : Test(6) {
          is( LCP::LCP(),      '',    'Without parameters' );
          is( LCP::LCP('abc'), 'abc', 'One parameter' );
          is( LCP::LCP( 'abc', 'xyz' ), '', 'None of common prefix' );
          is( LCP::LCP( 'abcdefgh', ('abcdefgh') x 15, 'abcdxyz' ),
              'abcd', 'Some common prefix' );
          my @str = map { chomp; $_ } <DATA>;
          is( LCP::LCP(@str),
              'file:///home/gms8994/Music/', 'Test data prefix' );
          is( LCP::LCP2(@str),
              'file:///home/gms8994/Music/', 'Test data prefix by LCP2' );
          my $t = countit( 1, sub{LCP::LCP(@str)} );
          diag("LCP: ${\($t->iters)} iterations took ${\(timestr($t))}");
          $t = countit( 1, sub{LCP::LCP2(@str)} );
          diag("LCP2: ${\($t->iters)} iterations took ${\(timestr($t))}");
      }
      
      __DATA__
      file:///home/gms8994/Music/t.A.T.u./
      file:///home/gms8994/Music/nina%20sky/
      file:///home/gms8994/Music/A%20Perfect%20Circle/
      

      测试套件结果:

      1..7
      ok 1 - use LCP;
      ok 2 - Without parameters
      ok 3 - One parameter
      ok 4 - None of common prefix
      ok 5 - Some common prefix
      ok 6 - Test data prefix
      ok 7 - Test data prefix by LCP2
      # LCP: 22635 iterations took 1.09948 wallclock secs ( 1.09 usr +  0.00 sys =  1.09 CPU) @ 20766.06/s (n=22635)
      # LCP2: 17919 iterations took 1.06787 wallclock secs ( 1.07 usr +  0.00 sys =  1.07 CPU) @ 16746.73/s (n=17919)
      

      这意味着在您的测试用例中,使用 substr 的纯 Perl 解决方案比 Roy's solution 快大约 20%,并且查找一个前缀大约需要 50us。除非您的数据或性能期望更大,否则没有必要使用 XS。

      【讨论】:

      • +1 用于添加测试套件,包括空字符串等案例; -1 表示关心以脚本语言实现的算法的性能调整。净得分:0。
      • 和 -999 表示神秘;-(
      • 一个很好的分析,但我会警惕鼓励过早优化。特别是对于新的编码人员来说,代码的清晰性更为重要。而且这个问题中隐含的小规模使用可能不会从任何优化中受益。
      • 出于好奇,我创建了Erlang solution,发现它的解释 BEAM 速度快 13 倍,在 HiPE 中编译速度快 28 倍。他们曾经告诉 Erlang 不适合字符串处理 ;-)
      【解决方案6】:

      来自http://forums.macosxhints.com/showthread.php?t=33780

      my @strings =
          (
            'file:///home/gms8994/Music/t.A.T.u./',
            'file:///home/gms8994/Music/nina%20sky/',
            'file:///home/gms8994/Music/A%20Perfect%20Circle/',
          );
      
      my $common_part = undef;
      my $sep = chr(0);  # assuming it's not used legitimately
      foreach my $str ( @strings ) {
      
          # First time through loop -- set common
          # to whole
          if ( !defined $common_part ) {
              $common_part = $str;
              next;
          }
      
          if ("$common_part$sep$str" =~ /^(.*).*$sep\1.*$/)
          {
              $common_part = $1;
          }
      }
      
      print "Common part = $common_part\n";
      

      【讨论】:

      • 您使用的字符串长度可能无关紧要,但对于较长的字符串,这将非常慢。即使 Perl 可以优化您的正则表达式中的最终“.*$”,每次循环迭代将花费 O(n^2) 时间在 $str 的长度上找到匹配初始“.*.*”的正确方法.
      • 使用 .*$ 对我来说似乎没用。这个解决方案运行良好,几乎和我的一样快。
      【解决方案7】:

      比上面更快,使用 perl 的本机二进制异或函数,改编自 perlmongers 解决方案($+[0] 对我不起作用):

      sub common_suffix {
          my $comm = shift @_;
          while ($_ = shift @_) {
              $_ = substr($_,-length($comm)) if (length($_) > length($comm));
              $comm = substr($comm,-length($_)) if (length($_) < length($comm));
              if (( $_ ^ $comm ) =~ /(\0*)$/) {
                  $comm = substr($comm, -length($1));
              } else {
                  return undef;
              }
          }
          return $comm;
      }
      
      
      sub common_prefix {
          my $comm = shift @_;
          while ($_ = shift @_) {
              $_ = substr($_,0,length($comm)) if (length($_) > length($comm));
              $comm = substr($comm,0,length($_)) if (length($_) < length($comm));
              if (( $_ ^ $comm ) =~ /^(\0*)/) {
                  $comm = substr($comm,0,length($1));
              } else {
                  return undef;
              }
          }
          return $comm;
      }
      

      【讨论】:

        猜你喜欢
        • 2011-12-14
        • 2017-02-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-05-13
        • 2010-10-22
        • 1970-01-01
        相关资源
        最近更新 更多