用于匹配任何长度的所有重复子串的正则表达式

时间:2012-12-14 07:52:16

标签: php regex duplicates substring pcre

假设我们有一个字符串:“abcbcdcde”

我想使用正则表达式识别在此字符串中重复的所有子字符串(即没有暴力迭代循环)。

对于上述字符串,结果集将为: {“b”,“bc”,“c”,“cd”,“d”}

我必须承认,对于有经验的人来说,我的正则表达式应该比它应该更加生疏。我尝试使用反向引用,但这只会匹配连续重复项。我需要匹配所有重复项,连续或其他。

换句话说,我希望匹配为> =第二次出现的任何字符。如果子串出现5次,那么我想捕获每个出现2-5。有意义吗?

到目前为止,这是我可悲的尝试:

preg_match_all( '/(.+)(.*)\1+/', $string, $matches );  // Way off!

我试着玩前瞻但是我只是在屠杀它。我在PHP(PCRE)中这样做,但问题或多或少与语言无关。我发现自己对此感到困惑,这有点令人尴尬。

4 个答案:

答案 0 :(得分:9)

你的问题是递归的......你知道什么,忘了递归! = p它在PHP中不会很好用,如果没有它,算法也很清楚。

  function find_repeating_sequences($s)
  {
    $res = array();
    while ($s) {
        $i = 1; $pat = $s[0];
        while (false !== strpos($s, $pat, $i)) {
            $res[$pat] = 1;
            // expand pattern and try again
            $pat .= $s[$i++];
        }
        // move the string forward
        $s = substr($s, 1);
    }
    return array_keys($res);
  }

出于兴趣,我在PHP中写了Tim's answer

function find_repeating_sequences_re($s)
{
    $res = array();
    preg_match_all('/(?=(.+).*\1)/', $s, $matches);
    foreach ($matches[1] as $match) {
        $length = strlen($match);
        if ($length > 1) {
            for ($i = 0; $i < $length; ++$i) {
                for ($j = $i; $j < $length; ++$j) {
                    $res[substr($match, $i, $j - $i + 1)] = 1;
                }
            }
        } else {
            $res[$match] = 1;
        }
    }
    return array_keys($res);
}

我让他们在800字节的随机数据的小基准测试中解决它:

$data = base64_encode(openssl_random_pseudo_bytes(600));

每个代码运行10轮,并测量执行时间。结果?

Pure PHP      - 0.014s (10 runs)
PCRE          - 40.86s <-- ouch!

当你看到24k字节(或者真正高于1k的任何东西)时,它会变得更奇怪:

Pure PHP      - 4.565s (10 runs)
PCRE          - 0.232s <-- WAT?!

事实证明,正则表达式在1k个字符后出现故障,因此$matches数组为空。这些是我的.ini设置:

pcre.backtrack_limit => 1000000 => 1000000
pcre.recursion_limit => 100000 => 100000

我不清楚只有1k个字符后才会触发回溯或递归限制。但即使这些设置以某种方式“固定”,结果仍然很明显,PCRE似乎不是答案。

我想用C语写这个会加速它,但我不确定到什么程度。

<强>更新

hakre's answer的帮助下,我整理了一个改进版本,在优化以下内容后,性能提高了约18%:

  1. 删除外部循环中的substr()调用以推进字符串指针;这是我以前的递归化身遗留下来的。

  2. 将部分结果用作正缓存,以跳过内循环内的strpos()次调用。

  3. 这就是它的荣耀(:

    function find_repeating_sequences3($s)
    {
        $res = array(); 
        $p   = 0;
        $len = strlen($s);
    
        while ($p != $len) {
            $pat = $s[$p]; $i = ++$p;
            while ($i != $len) {
                if (!isset($res[$pat])) {
                    if (false === strpos($s, $pat, $i)) {
                        break;
                    }
                    $res[$pat] = 1;
                }
                // expand pattern and try again
                $pat .= $s[$i++];
            }
        }
        return array_keys($res);
    }
    

答案 1 :(得分:2)

您无法在单个正则表达式中获得所需的结果,因为正则表达式将贪婪地(找到bc...bc)或懒惰(找到b...bc...c)匹配,但从不两者都匹配。 (在您的情况下,它确实找到c...c,但只是因为c重复两次。)

但是一旦你找到一个长度为&gt;的重复子串从逻辑上讲,它必须重复所有较小的“子串”的子串。如果你想让它们拼出来,你需要单独完成。

举个例子(使用Python因为我不懂PHP):

>>> results = set(m.group(1) for m in re.finditer(r"(?=(.+).*\1)", "abcbcdcde"))
>>> results
{'d', 'cd', 'bc', 'c'}

然后,您可以将以下功能应用于每个结果:

def substrings(s):
    return [s[start:stop] for start in range(len(s)-1) 
                          for stop in range(start+1, len(s)+1)]

例如:

>>> substrings("123456")
['1', '12', '123', '1234', '12345', '123456', '2', '23', '234', '2345', '23456',
 '3', '34', '345', '3456', '4', '45', '456', '5', '56']

答案 2 :(得分:1)

我最接近的是/(?=(.+).*\1)/

前瞻的目的是允许多次匹配相同的字符(例如,ccd)。但是,出于某种原因,它似乎没有获得b ...

答案 3 :(得分:1)

有趣的问题。我基本上在Jacks answer中使用了该功能,并且正在尝试减少测试次数。

我首先尝试只搜索一半字符串,但事实证明每次创建模式以通过substr搜索太昂贵了。通过在每次迭代中附加一个字符来完成Jacks回答的方式就像它看起来更好。然后我没时间用完所以我无法深入了解它。

然而,在寻找这样一种替代实现时,我至少发现我想到的算法中的一些差异也可以应用于Jacks函数:

  1. 不需要在每个外部迭代中剪切字符串的开头,因为已经使用偏移进行了搜索。
  2. 如果要查找重复的主题的其余部分小于重复针,则无需搜索针。
  3. 如果已经搜索了针头,则无需再次搜索。
      

    注意: 这是一次记忆交易。如果你有很多重复,你将使用类似的记忆。但是,如果您的重复次数较少,则此变体使用的内存比以前多。

  4. 功能:

    function find_repeating_sequences($string) {
        $result = array();
        $start  = 0;
        $max    = strlen($string);
        while ($start < $max) {
            $pat = $string[$start];
            $i   = ++$start;
            while ($max - $i > 0) {
                $found = isset($result[$pat]) ? $result[$pat] : false !== strpos($string, $pat, $i);
                if (!$result[$pat] = $found) break;
                // expand pattern and try again
                $pat .= $string[$i++];
            }
        }
        return array_keys(array_filter($result));
    }
    

    所以,请将此视为Jacks回答的补充。