簡體   English   中英

部分匹配長度的正則表達式 - 字符串相似度

[英]Regular Expression for partial match length - String similarity

假設我有字符串“Torcellite”和另一個字符串“Tor” - 這兩個字符串的相似長度是3,因為它們都以“Tor”開頭。 現在另一個字符串“christmas”和“mas”的相似度為0,因為它們不是以相同的字符集開頭的。

在這兩種情況下,第二個字符串都是第一個字符串的后綴。

一個更清晰的例子:

字符串長度:1到10 ^ 5

字符串: abaabc

后綴: abaabcbaabcaabcabcbcc

相似之處: abaabc ,none, aab ,none,none

相似度長度: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)然后找到最長的組匹配。

是否有更高效的算法可以實現他的?

到目前為止,最好的方法是在輸入字符串上構建后綴樹 構建后綴樹只需要O(n)時間,其中n是字符串的長度。 后綴樹在邏輯上由樹組成,其中通過從根到每個葉子的步行可以找到字符串的所有后綴。 您可以閱讀Wikipedia以獲取有關這些樹如何工作的更多詳細信息。

從本質上講,后綴樹允許您簡單地將當前問題重新設置為“查找”后綴樹中的原始字符串。 當您沿着樹走下去時,您會計算每個子樹中的后綴數量,並乘以當前的匹配長度來確定您的分數。 這種“搜索”也需要O(n)時間。

因此,最終結果是您可以在保證 O(n)時間和O(n)空間中解決問題,並使用O(n)預處理時間。 這非常有效! 並且,沒有產生二次行為的“最壞情況”。 有了它,您可以輕松處理長度達10 ^ 7的字符串。

實現中唯一的困難是構建后綴樹,但您可以找到免費的代碼。

已經由Valdar Moridin發布的Simliar算法,但不需要創建子串(每次調用substring都會創建一個新的String對象,其中包含其源的char[]的指定范圍的副本)。 這不會改善時間復雜度,但可能會減少常量因子的總運行時間:

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個相等的字符。

這就是我如上所述的做法。 我不知道這應該完成什么,但是因為你已經指定只需要匹配字符串的開頭,即使它是O(n ^ 2),大多數時候它不會在任何地方運行接近全長的n。 最糟糕的情況顯然是像“aaaaaaaaaaaaaaaaa”這樣的字符串。 這需要不到5秒的時間來處理我的機器上的60,000個'a'字符串。

我認為沒有必要涉及為嚴格的前綴匹配生成和編譯正則表達式的開銷。 我錯過了這一點嗎?

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;
}

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個字符生成正則表達式。 缺少第一場比賽的一部分(即整個比賽),但糾正這是一個明智的選擇。

這對您的數據集有何影響?

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)。

請嘗試以下方法。 我測試過這個。
任何輸入字符串(長度從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;
}

從我的觀點來看,無論你選擇哪種方法來實現它,它通常都會有自己最糟糕的情況。
不同之處在於其最壞情況下的表現。
例如,我已經在我的PC中測試了isnot2bad的方法,我的第一個實現(oneTry)和我的第二個實現(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)

從測試結果中,我們可以看到“secondTry”的最差性能時間與字符串長度幾乎呈線性關系,而其他的則與字符串長度幾乎呈正方形


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 =“aabaab”,然后T2 ='b',只有T0('a')可以繼續到T2,而T1('a')不能繼續到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可以整體組合在一起。
...

詳細的實現代碼如下所示。 希望有人可以為此主題發布他們的最佳實施和測試結果。 謝謝。


    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;
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM