Rabin-Karp 算法
概念
用於在 一個字符串 中查找 另外一個字符串 出現的位置。
與暴力法不同,基本原理就是比較字符串的 哈希碼 ( HashCode ) , 快速的確定子字符串是否等於被查找的字符串
比較哈希值采用的是滾動哈希法
- 如何計算哈希值:
如 : “abcde” 的哈希碼值為
-
滾動哈希法:
母串是"abcde",子串是"cde"
則母串先計算"abc"的哈希值:\[a×31^2+b×31^1+c×31^0 \]而子串"cde"的哈希值是:
\[c×31^2+d×31^1+e×31^0 \]與母串哈希值不匹配,於是母串向后繼續計算哈希值,下標i=3指向字母d,
\[(a×31^2+b×31^1+c×31^0)×31+d-a×31^3 \]前n個字符的hash * 31-前n字符的第一字符 * 31的n次方(n是子串長度)
可以計算出母串中"bcd"的哈希值,再與子串哈希值進行比較
代碼實現
public static void main(String[] args) {
String s = "ABABABA";
String p = "ABA";
match(p, s);
}
//p是母串,s是子串
private static void match(String p, String s) {
long hash_p = hash(p);//p的hash值
long[] hashOfS = hash(s, p.length());
match(hash_p, hashOfS);
}
private static void match(long hash_p, long[] hash_s) {
for (int i = 0; i < hash_s.length; i++) {
if (hash_s[i] == hash_p) {
System.out.println(i);
}
}
}
final static long seed = 31;
/**
* n是子串的長度
* 用滾動方法求出s中長度為n的每個子串的hash,組成一個hash數組
*/
static long[] hash(final String s, final int n) {
long[] res = new long[s.length() - n + 1];
//前m個字符的hash
res[0] = hash(s.substring(0, n));
for (int i = n; i < s.length(); i++) {
char newChar = s.charAt(i);
char ochar = s.charAt(i - n);
//前n個字符的hash*seed-前n字符的第一字符*seed的n次方
long v = (res[i - n] * seed + newChar - pow(seed, n) * ochar) % Long.MAX_VALUE; //防止溢出
res[i - n + 1] = v;
}
return res;
}
static long pow(long a,int b){
long ans = 1;
while(b>0){
ans*=a;
b--;
}
return ans;
}
/**
* 使用100000個不同字符串產生的沖突數,大概在0~3波動,使用100百萬不同的字符串,沖突數大概110+范圍波動。
* 如果數據量非常大,可以在子串和母串哈希值匹配成功的時候多進行一步朴素的字符串比較,以防萬一。
*/
static long hash(String str) {
long h = 0;
for (int i = 0; i != str.length(); ++i) {
h = seed * h + str.charAt(i);
}
return h % Long.MAX_VALUE;
}
時間復雜度分析
設母串長度為m,子串長度為n。
則滾動計算母串哈希值復雜度是O(m)
計算子串哈希值復雜度是O(n)
遍歷母串進行哈希值匹配的復雜度是O(m)
綜上,Rabin-Karp算法的時間復雜度是O(m+n)
KMP 算法
概念
KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它為克努特——莫里斯——普拉特操作(簡稱KMP算法)。
主要用於在文本串S中查找模式串P出現的位置。
-
KMP和暴力匹配的不同
-
如何求解next數組
代碼實現
public static void main(String[] args) {
String src = "babababcbabababb";
String p = "bababb";
int index = kmp(src, p);
System.out.println(index);
}
//s是文本串,p是模式串
private static int kmp(String s, String p) {
if (s.length() == 0 || p.length() == 0) return -1;
if (p.length() > s.length()) return -1;
int[] next = next(p);
int i = 0; //文本串的下標
int j = 0; //模式串的下標
int slength = s.length();
int plength = p.length();
while (i < slength) {
//①如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++
//j=-1,因為next[0]=-1,說明p的第一位和i這個位置無法匹配,這時i,j都增加1,i移位,j從0開始
if (j == -1 || s.charAt(i) == p.charAt(j)) {
i++;
j++;
} else {
//②如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]回退
//next[j]即為j所對應的next值
j = next[j];
}
if (j == plength) { //匹配成功了
return i - j;
}
}
return -1;
}
private static int[] next(String p) {
int[] next = new int[p.length() + 1];
int left = -1;
int right = 0;
next[0] = -1;
while (right < p.length()) {
if (left == -1 || p.charAt(left) == p.charAt(right)) {
next[++right] = ++left; //最長匹配位置加一
} else {
left = next[left]; //前綴回退到上一個最長匹配位置
}
}
return next;
}
KMP算法改進(nextval數組)
可以把next數組改造成nextval數組
下標(j) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
模式串(P) | a | b | c | d | a | b | d |
next | -1 | 0 | 0 | 0 | 0 | 1 | 2 |
nextval | -1 | 0 | 0 | 0 | -1 | 0 | 2 |
當 j 處模式串字符不等於next[j]處模式串字符時,nextval[j]=next[j]
當 j 處模式串字符等於next[j]處模式串字符時,nextval[j]=nextval[next[j]]
比如:
下標為j=4處的模式串字符是a,而下標為next[j]處的模式串字符也是a,則nextval[4]
拷貝nextval[next[4]]
處的值,也就是-1
解釋一下,按照next數組回退的話,下標為4處next[4]=0
,會回退到下標為0處,而下標為0處next[0]
=-1,會回退到下標為-1處,回退了兩次。
但是如果應用改進的nextval數組,下標為4處next[4]=-1
,直接回退到下標為-1處,只需要回退一次。
當遇到有大量連續重復元素的數組時,性能提升最為明顯。
比如:
當 j=3 時,通過next數組回退需要先退到下標為2,再退到下標為1,在退到下標為0,最后退到下標為-1。
而通過nextval數組回退,一次就可以回退到下標為-1處。
//求nextval數組
private static int[] nextval(String p, int[] nextval) {
int right = 0, left = -1; //left是前綴,right是后綴
nextval[0] = -1;
while (right < p.length()) {
if (left == -1 || p.charAt(right) == p.charAt(left)) {
left++;
right++; //多加了一次判斷比較 nextval[right] 和 nextval[left]
if (nextval[right] != nextval[left]) {
nextval[right] = left;
} else {
nextval[right] = nextval[left]; //注意
}
} else {
left = nextval[left]; //回退
}
}
return nextval;
}