字串處理 Rabin-Karp (Rolling Hash)及相關LeetCode題目

weixin_34007291發表於2018-03-22

關於我的 Leetcode 題目解答,程式碼前往 Github:https://github.com/chenxiangcyr/leetcode-answers


LeetCode題目:686 Repeated String Match
Given two strings A and B, find the minimum number of times A has to be repeated such that B is a substring of it. If no such solution, return -1.
For example, with A = "abcd" and B = "cdabcdab".
Return 3, because by repeating A three times (“abcdabcdabcd”), B is a substring of it; and B is not a substring of A repeated two times ("abcdabcd").

傳統演算法程式碼如下:時間複雜度O(length(A) * length(B))

class Solution {
    public int repeatedStringMatch(String A, String B) {
        
        for(int i = 0; i < A.length(); i++) {
            
            // find the first match index
            if(A.charAt(i) == B.charAt(0)) {
                
                int times = 1;
                
                int k = i;
                int j = 0;
                
                while(j < B.length() && A.charAt(k) == B.charAt(j)) {
                    j++;
                    
                    // reach the end of A and not reach the end of B, times + 1
                    if(k == A.length() - 1 && j < B.length()) {
                        k = 0;
                        times++;
                    }
                    else {
                        k++;
                    }
                }
                
                // reach the end of B
                if(j == B.length()) {
                    return times;
                }
            }
        }
        
        return -1;
    }
}

Rabin-Karp (Rolling Hash 旋轉雜湊)演算法

字串可以理解為字元陣列,而字元可以被轉換為整數,可以把字串當成一個整形陣列。
找到一種方式將一組整形數字轉化為一個數字,就能夠使得我們藉助一個預期的輸入值來Hash字串。

假設,一個模式串 P(需要被匹配的串)長度為L,需要在其中查詢匹配的串S長度為N
一種在S中查詢P的方式為:

  • 雜湊 P 得到 h(P),時間複雜度 O(L)
  • S 的索引為0開始來列舉 S 里長度為 L 的子串,雜湊子串並計算出 h(P)',時間複雜度O(N*L)
  • 如果一個子串的雜湊值 h(P)'h(P) 匹配,將該子串與 P 進行比較,時間複雜度 O(L)

這個做法的時間複雜度為 O(N*L)。我們可以通過使用 Rolling Hash 來優化這種做法。在上述步驟2中,我們看到對於 O(N) 的子串,都花費了 O(L) 來雜湊他們,然而可以看到這些子串中很多字元都是重複的。

我們可以利用前一個子串的計算結果雜湊值來計算當前子串的雜湊值。因此在上述步驟2中,時間複雜度可以優化為O(N)
假設第一個子串雜湊值H0 = (S[0] * 10^2 + S[1] * 10^1 + S[2] * 10^0) mod m
則第二個子串雜湊值H1 = (10 * H0 - S[0] * 10^3 + S[3] * 10^0) mod m

程式碼如下:時間複雜度O(length(A) + length(B))

class Solution {

    public boolean check(int index, String A, String B) {
        for (int i = 0; i < B.length(); i++) {
            if (A.charAt((i + index) % A.length()) != B.charAt(i)) {
                return false;
            }
        }
        return true;
    }
    
    public int repeatedStringMatch(String A, String B) {
        int q = (B.length() - 1) / A.length() + 1;
        
        // 素數
        int p = 113;
        int MOD = 1_000_000_007;
        
        // 乘法逆模
        int pInv = BigInteger.valueOf(p).modInverse(BigInteger.valueOf(MOD)).intValue();

        // 計算字串B的雜湊值,時間複雜度 O(B.length())
        long bHash = 0, power = 1;
        for (int i = 0; i < B.length(); i++) {
            bHash += power * B.codePointAt(i);
            bHash %= MOD;
            power = (power * p) % MOD;
        }

        // 計算字串A的第一個子串雜湊值,時間複雜度 O(B.length())
        long aHash = 0; power = 1;
        for (int i = 0; i < B.length(); i++) {
            aHash += power * A.codePointAt(i % A.length());
            aHash %= MOD;
            power = (power * p) % MOD;
        }

        // 如果一個子串的雜湊值與 B 的雜湊值匹配,將該子串與 B 進行比較,時間複雜度 O(B.length())
        if (aHash == bHash && check(0, A, B)) return q;
        
        power = (power * pInv) % MOD;

        // 利用前一個子串的計算結果雜湊值來計算當前子串的雜湊值
        for (int i = B.length(); i < (q + 1) * A.length(); i++) {
            aHash -= A.codePointAt((i - B.length()) % A.length());
            aHash *= pInv;
            aHash += power * A.codePointAt(i % A.length());
            aHash %= MOD;
            
            if (aHash == bHash && check(i - B.length() + 1, A, B)) {
                return i < q * A.length() ? q : q + 1;
            }
        }
        
        return -1;
    }
}

引用:
Rolling Hash(Rabin-Karp 演算法)匹配字串與anagram串

相關文章