题目:假如要判断字符串A"ABA"是不是字符串B"ABABABA"的子串。
解法一:暴力破解法, 直接枚举所有的长度为3的子串,然后依次与A比较,这样就能得出匹配的位置。 这样的时间复杂度是O(M*N),M为B的长度,N为A的长度。
解法二:Rabin-Karp算法
思想:假设待匹配字符串的长度为N,目标字符串的长度为M(M>N);首先计算待匹配字符串的hash值,计算目标字符串前N个字符的hash值;比较前面计算的两个hash值,比较次数M-N+1:若hash值不相等,则继续计算目标字符串的下一个长度为N的字符子串的hash值,若hash值相同,则需要使用比较字符是否相等再次判断是否为相同的子串(这里若hash值相同,则直接可以判断待匹配字符串是目标字符串的子串,之所以需要再次判断字符是否相等,是因为不同的字符计算出来的hash值有可能相等,称之为hash冲突或hash碰撞,不过这是极小的概率,可以忽略不计);
哈希函数定义如下:
其中Cm表示字符串中第m项所代表的特地数字,有很多种定义方法,我习惯于用java自带的char值,也就是ASCII码值。java中的char是16位的,用的Unicode编码,8位的ASCII码包含在Unicode中。b是哈希函数的基数,相当于把字符串看作是b进制数。h是防止哈希值溢出。
代码:
1 public class RabinKarp { 2 3 public static void main(String[] args) { 4 String s = "ABABABA"; 5 String p = "ABA"; 6 match(p, s); 7 } 8 9 /** 10 * @param p 模式 11 * @param s 源串 12 */ 13 static void match(String p,String s){ 14 long hash_p = hash(p);//p的hash值 15 int p_len = p.length(); 16 for (int i = 0; i+p_len<= s.length(); i++) { 17 long hash_i = hash(s.substring(i, i+p_len));// i 为起点,长度为p_len的子串的hash值 18 if (hash_p==hash_i) { 19 System.out.println("match:"+i); 20 } 21 } 22 } 23 24 final static long seed = 31; // 进制数 25 26 /** 27 * 不同的字符计算出来的hash值相同 称为hash冲突 28 * 使用100000个不同字符串产生的冲突数,大概在0~3波动,使用100百万不同的字符串,冲突数大概110+范围波动。 29 * @param str 30 * @return 31 */ 32 private static long hash(String str) { 33 long h = 0; 34 for (int i = 0; i !=str.length(); i++) { 35 // 这个计算方式就是 An²+Bn+c 的循环表达式,而这个计算方式就是二进制转十进制的计算方式 36 // 这里n=31,可以理解为转为31进制 37 h = seed * h + str.charAt(i); 38 39 } 40 return h%Long.MAX_VALUE; // 防止hash值过大 41 } 42 43 }
结果:
在这里计算一下时间复杂度,计算hash值的时间为O(N),目标字符串长度为M,所以时间复杂度为O(M*N)。好像和暴力破解差不多。下面会通过一种类似于预处理的方式来进行优化,叫做滚动哈希。就是提前计算好源串的hash值,构建成一个hash数组,再通过比较hash值,这样就成功匹配出来了。通过这种优化,时间复杂度下降到O(M+N),O(N)为计算待匹配的字符串计算hash值的时间,O(M)为计算hash数组的时间。
滚动哈希的技巧就是:如果已经算出从k到k+m的子串的哈希值H(S[k,k+1...k+m]),那么从k+1到k+m+1的子串的哈希值就可以基于前一个的哈希值计算得出。
代码:
1 /** 2 * 滚动哈希法 3 * 对目标字符串按d进制求值,mod h 取余作为其hash 4 * 对源串,一次求出m个字符的hash,保存在数组中(滚动计算) 5 * 匹配时,只需对比目标串的hash值和预存的源串的hash值表 6 */ 7 public class RabinKarp_1 { 8 9 public static void main(String[] args) { 10 String s = "ABABABA"; 11 String p = "ABA"; 12 match(p, s); 13 } 14 15 static void match(String p,String s){ 16 long hash_p = hash(p);//p的hash值 17 long[] hashOfS = hash(s, p.length()); 18 for (int i = 0; i < hashOfS.length; i++) { 19 if (hashOfS[i] == hash_p) { 20 System.out.println("match:" + i); 21 } 22 } 23 } 24 25 final static long seed = 31; 26 27 /** 28 * 滚动哈希 29 * @param s 源串 30 * @param n 子串的长度 31 * @return 32 */ 33 private static long[] hash(String s, int n) { 34 long[] res = new long[s.length() - n + 1]; 35 //前n个字符的hash 36 res[0] = hash(s.substring(0, n)); 37 for (int i = n; i < s.length(); i++) { 38 char newChar = s.charAt(i); // 新增的字符 39 char oldchar = s.charAt(i - n); // 前n字符的第一字符 40 //前n个字符的hash*seed-前n字符的第一字符*seed的n次方 41 long v = (long) ((res[i - n] * seed + newChar - Math.pow(seed, n) * oldchar) % Long.MAX_VALUE); 42 res[i - n + 1] = v; 43 } 44 return res; 45 } 46 47 static long hash(String str) { 48 long h = 0; 49 for (int i = 0; i != str.length(); ++i) { 50 h = seed * h + str.charAt(i); 51 } 52 return h % Long.MAX_VALUE; 53 } 54 }
结果: