部分匹配长度的正则表达式 - 字符串相似度

时间:2014-08-11 03:40:01

标签: java regex string pattern-matching

假设我有字符串“Torcellite”和另一个字符串“Tor” - 这两个字符串的相似长度为3,因为它们都以“Tor”开头。现在另一个字符串“christmas”和“mas”的相似度为0,因为它们不是以相同的字符集开头。

在这两种情况下,第二个字符串都是第一个字符串的后缀。

一个更清晰的例子:

字符串长度:1到10 ^ 5

字符串:abaabc

后缀:abaabcbaabcaabcabcbcc

相似度:abaabc,无,aab,无,无

相似度长度:6,0,1,2,0,0

答案:6 + 0 + 1 + 2 + 0 + 0 = 9

我有一个低效的逻辑来使用正则表达式找到这些部分后缀匹配。

算法:

  • 查找给定字符串的所有子字符串。
  • 从后缀的子串中创建一个模式。

    for(int i=1; i<substrings[i].length; i++) {
        Pattern p = Pattern.compile("^"+substrings[i].substring(0, i));
        Matcher m = p.find(string); //the given string for which similarities need to be  calculated
        if(m.find())
            similaryLengths +=  i;
    }
    
  • 这种情况的复杂性大致为O(n ^ 2),因为我需要遍历后缀的字符串,然后是模式的子串。

  • 我曾想过在模式中使用分组来查找组,但我不确定正则表达式会是什么样子。我想到的是第一个子字符串是:((((((a)b)a)a)b)c),然后找到最长的组匹配。

是否有更高效的算法可以实现他的?

7 个答案:

答案 0 :(得分:3)

到目前为止,最好的方法是在输入字符串上构建后缀树。构建后缀树只需要O(n)时间,其中n是字符串的长度。后缀树在逻辑上由树组成,其中通过从根到每个叶子的步行可以找到字符串的所有后缀。您可以read Wikipedia了解有关这些树如何工作的更多详细信息。

基本上,后缀树允许您简单地将当前问题重新设置为“查找”后缀树中的原始字符串。当您沿着树走下去时,您会计算每个子树中的后缀数量,并乘以当前的匹配长度来确定您的得分。这种“搜索”也需要O(n)时间。

因此,最终结果是您可以使用O(n)预处理时间来解决保证 O(n)时间和O(n)空间中的问题。这非常有效!并且,没有产生二次行为的“最坏情况”。有了它,您可以轻松处理长度达10 ^ 7的字符串。

实现中唯一的困难是构建后缀树,但您可以找到免费的代码。

答案 1 :(得分:2)

已经由Valdar Moridin发布的Simliar算法,但不需要创建子串(每次调用substring都会创建一个新的String对象,其中包含指定范围char[]的副本1}}它的来源)。这不会提高时间复杂度,但可能会减少常数因子的总运行时间:

public static int partialSuffixMatch(CharSequence input) {
    int count = input.length();
    for (int i = 1; i < input.length(); i++) {
        for (int a = 0, b = i; b < input.length(); a++, b++) {
            if (input.charAt(a) != input.charAt(b))
                break;
            count++;
        }
    }
    return count;
}

经过短暂的热身,这个算法在我的计算机上处​​理一个String,在大约40毫秒内有10,000个相等的字符,在大约4秒内处理100,000个相等的字符。

答案 2 :(得分:1)

这就是我如上所述的做法。我不知道这应该完成什么,但是因为你已经指定只需要匹配字符串的开头,即使它是O(n ^ 2),大多数时候它不会在任何地方运行接近全长的n。最糟糕的情况显然是像“aaaaaaaaaaaaaaaaa”这样的字符串。在我的机器上处理60,000个'a'字符串的时间不到5秒。

我认为没有必要涉及为严格的前缀匹配生成和编译正则表达式的开销。我错过了这一点吗?

int similarity(String input) {
    int count = 0;
    for (int i = 0; i < input.length() ; i++) {
        String sub = input.substring(i);
        for (int j = 0; j < sub.length(); j++) {
            if (input.charAt(j) != sub.charAt(j))
                break;
            count++;
        }
    }
    return count;
}

答案 3 :(得分:1)

abaabc示例中,我收集到您正在尝试查找与原始字符串的开头匹配的所有子字符串。这个可以使用单个正则表达式完成,有点类似于你提出的模式。当然,该正则表达式的长度与原始字符串成比例。正则表达式本身非常简单;它表示整个字符串,但字符串的尾部(任意长度)是可选的。实际上,此正则表达式匹配字符串的任何前缀。对于字符串abcdef,正则表达式为:

(?=(a(?:b(?:c(?:d(?:ef?)?)?)?)?))

注意:

  • 我为除了外部子模式之外的每个子模式使用(?: ... ),以避免大量不必要的捕获。
  • 我使用了前瞻模式(?= ... ),因为匹配可以(并且会)重叠。没有它,第一个匹配(整个字符串abcdef)将消耗整个输入,导致跳过所有其他可能的匹配。

当然,abcdef不是一个有趣的例子;它没有重复的子串,因此正则表达式只有一个匹配,即整个字符串abcdef。你的例子abaabc更好,所以我为它做了一个小提琴。正如你所指出的,它找到了3个匹配:abaabc,a,ab。
http://regex101.com/r/vJ8uQ9/1

随意使用它,但不要忘记,对于测试字符串中的每个更改,您都需要相应地更改正则表达式。对于长串,这变得乏味。幸运的是,一个简单的递归程序可以为任何给定的字符串生成一个正则表达式。

function generateRegex(string input)
{
    return input.substring(0, 1) +
           (input.length > 2 ? "(?:" + generateRegex(input.substring(1)) + ")" : input.substring(1)) +
           "?";
}

string myRegex = "(?=(" + generateRegex(myInput) + "))";

我手边没有Java测试环境,但我确实用JavaScript测试过它 小提琴:http://jsfiddle.net/gqehcjf9/1/

性能似乎没问题(对于一个9000个字符的字符串不到一秒钟),但是在对超过9361个字符的字符串(Firefox 31.0)进行测试时,我确实得到了“正则表达式过于复杂”的异常。我希望Java的正则表达式引擎限制性较小。如果没有,那么就有一种可能的优化。如果您非常确定重复的子字符串永远不会超过1000个字符,那么您可以考虑仅为字符串的前1000个字符生成正则表达式。你缺少第一场比赛的一部分(即整个字符串),但纠正这是不费脑筋的。

答案 4 :(得分:0)

这对您的数据集有何影响?

int sum = s.length;
for (int i = 1; i < s.length; i++) {
    for (int j = i; j < s.length; j++) {
        for (int k = 0; k < s.length - j; k++) {
            if (s.charAt(i+k) != s.charAt(j+k)) break;
            sum++;
        }
    }
}

而不是迭代i,你可以找到下一次出现的s.charAt(0)。

答案 5 :(得分:0)

请尝试以下方法。我测试了这一个。
任何输入字符串(长度从1到10 ^ 5),执行时间在我的电脑中的时间 20ms

public static int oneTry(CharSequence input) {
    int tail = input.length();
    for (int i = 1; i < input.length(); i++) {
        if (input.charAt(i) == input.charAt(0)) {
            tail = i;
            break;
        }
    }

    int count = 0;

    int head = 0;
    int next = 0;
    int base = 0;
    int two = -1;
    boolean start = false;
    boolean end = false;
    for (int i = tail; i < input.length(); i++) {
        if (input.charAt(i) == input.charAt(next)) {

            count++;

            if (next>0 && !start && input.charAt(i) == input.charAt(0)) {
                base = 1;
                start = true;
            }

            if (start) {
                if (!end && input.charAt(i) == input.charAt(head)) {
                    count = count + base;
                    head++;
                    head = head < tail ? head : 0;
                    if(head == 0) {
                        base++;
                    }
                } else {
                    end = true;
                }

                if(end) {
                    if(two <0 && input.charAt(i) == input.charAt(0)) {
                        two = i;
                    }
                }
            }

            next++;

            if(i==input.length()-1 && two > 0) {
                i = two - 1;

                next = 0;
                base = 0;
                two = -1;
                start = false;
                end = false;
                head = 0;
            }

        } else {
            if(two > 0) {
                i = two - 1;

                next = 0;
                base = 0;
                two = -1;
                start = false;
                end = false;
                head = 0;
            } else {
                if(end || !start) {
                    if(input.charAt(i) == input.charAt(0)) i--;

                    next = 0;
                    base = 0;
                    two = -1;
                    start = false;
                    end = false;
                    head = 0;
                } else {
                    i--;

                    next = next - tail;
                    base = base -1;
                    two = -1;
                    start = base==0 ? false : true;
                    end = false;
                    //head = 0;
                }                   
            }
        }
    }
    count = count + input.length();
    return count;
}

答案 6 :(得分:0)

从我的观点来看,无论你选择哪种方法来实现它,通常都会有自己最糟糕的情况 不同之处在于其最坏情况下的表现 例如,我测试了isnot2bad的方法,我的第一个实现(oneTry)&amp;我的第二次实现(secondTry)在我的电脑里 最坏情况的测试结果是:
isnot2bad 方法:~330s(2 * 10 ^ 5),~74s(10 ^ 5),~0.8(10 ^ 4),~0.01(10 ^ 3) 我的第一个实现( oneTry ):〜200s(2 * 10 ^ 5),~45s(10 ^ 5),~0.5s(10 ^ 4),~0.01(10 ^ 3)
我的第二次实施( secondTry ):〜4s(10 ^ 6),~0.4s(10 ^ 5),~0.05s(10 ^ 4),~0.007(10 ^ 3)

从测试结果中,我们可以看到&#34; secondTry&#34;字符串长度接近线性,而字符串长度接近正方形


secondTry实施的想法是这样的:
对于任何字符串输入 T(T0 ... Tn-1,len = n),,总字符串的相似度值( St )是每个字符串的总和字符串S.
中单个字符的相似度值( Si )       例如:St = S0 + ... + Si + ... + Sn-1
显然,子串中的T0总数[T0 ... Ti]> = Si> = 1 Si的精确值等于子串[T0 ... Ti]中的T0总数,后者继续与Ti匹配。
例如:T =&#34; aabaab&#34;,然后T2 =&#39; b&#39;,只有T0(&#39; a&#39;)可以继续T2,而T1(&#39; a&#39;)无法继续T2。因此,S2 = 1
因此,我们需要跟踪T0是否继续(如果是,请将其保留在数组中,否则,将其从数组中删除)。然后,很容易计算每个Ti的相似性 同时,为了提高性能,我们不需要检查每个conitnuing T0。实际上,对于某些T0,它们可以组合在一起 因为它们属于重复模式。(它可能是长图案,也可能是短图案) 例如:
    ababababab ......:T0,T2,T4,T6 ......可以整体组合在一起     aaaaaaaaaa ...:T0,T1,T2,T3 ......可以整体组合在一起。
    aaaabaaaabaaaab ...:
        T0,T5,T10,T15 ......可以整体组合在一起         T1,T2,T3可以整体组合在一起         T6,T7,T8可以整体组合在一起         ...

详细的实施代码如下所示。 希望有人可以发布他们最好的实现&amp;测试此主题的结果。 谢谢。


    public static List<ANode> anodes = null;
    public static List<ANode> tnodes = null;
    public static void checkANodes(CharSequence input, int num) {
        tnodes = new Vector<ANode>(); 
        for(int i=anodes.size()-1; i>=0; i--) {
            ANode anode = anodes.get(i);
            if(input.charAt(num) == input.charAt(num-anode.pos)) {
                tnodes.add(anode);
            }else {
                if(tnodes.size() > 0) {
                    // ok to do the changes
                    ANode after = tnodes.get(tnodes.size()-1);
                    tnodes.remove(after);
                    if(after.c > 1) {
                        tnodes.add(new ANode(after.pos + after.shift, after.shift ,after.c-1)); 
                        tnodes.add(new ANode(after.pos, after.pos-anode.pos + anode.shift,1));
                    }else {
                        tnodes.add(new ANode(after.pos, after.pos-anode.pos + anode.shift,1));  
                    }
                }
            }
        }

        anodes.clear();
        for(int i=tnodes.size() - 1; i >= 0; i--) {
            anodes.add(tnodes.get(i));
        }
    }

    public static int secondTry(CharSequence input) {
        anodes = new Vector<ANode>();

        int start = 0;
        for (int i = 1; i < input.length(); i++) {
            if (input.charAt(i) == input.charAt(0)) {
                start = i;
                break;
            }
        }

        int count = 0;
        int base = 0;
        for (int i = start; i < input.length(); i++) {
            checkANodes(input, i);
            if(input.charAt(0) == input.charAt(i)) {
                if(anodes.size() == 0) {
                    anodes.add(new ANode(i,  i, 1));
                }else {
                    ANode last = anodes.get(anodes.size()-1);
                    int shift = i - last.pos;
                    int mod = shift % last.shift;
                    if(mod == 0) {
                        last.c++;
                    }else {
                        anodes.add(new ANode(i, mod, 1));
                    }
                }
            }

            base = 0;
            for(ANode anode : anodes) {
                base = base + anode.c;
            }           
            count = count + base;
        }

        count = count + input.length();
        return count;
    }

public class ANode {
    public int pos = 0;
    public int c = 1;
    public int shift = 0;

    public ANode(int pos, int shift, int c) {
        this.pos = pos;
        this.shift = shift;
        this.c = c;
    }
}