Live each day like it could be your last.
把天天都看成生命的最后一天,认真生活。
大道至简,个人理想是用最简单的代码实现最美的算法。字符串匹配的应用很是普遍,java的indexOf(),js的全家桶一套匹配(find,indexOf,include...)等等。本文主要分享了它们底层依赖的字符串匹配算法。两种简单,两种复杂。话很少说,全部源码均已上传至github:连接java
bf算法俗称朴素匹配算法,为何叫这个名字呢,由于很暴力,在主串中,检查起始位置分别是 0、一、2…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的。git
在这里i循环是跟踪主串txt,j是跟踪模式串pattern,首先外围先肯定访问次数,tLen-pLen。github
j循环来进行比较,这里可能惟一比较很差理解的是i + j,查看测试结果,应该能够明白。算法
private int bfSearch(String txt, String pattern) {
int tLen = txt.length();
int pLen = pattern.length();
if (tLen < pLen) return -1;
for (int i = 0; i <= tLen - pLen; i++) {
int j = 0;
for (; j < pLen; j++) {
System.out.println(txt.charAt(i + j) + " -- " + pattern.charAt(j));
if (txt.charAt(i + j) != pattern.charAt(j)) break;
}
if (j == pLen) return i;
}
return -1;
}复制代码
bf算法还有一个变化,用到了显示回退的思想,i,j的做用和常规的同样,这里的i至关于常规的i+j,只不过当发现不匹配的时候,须要回退i和j这两个指针,j从新回到开头,i指向下一个字符。数组
private int bfSearchT(String txt, String pattern) {
int tLen = txt.length();
int i = 0;
int pLen = pattern.length();
int j = 0;
for (; i < tLen && j < pLen; i++) {
System.out.println(txt.charAt(i) + " -- " + pattern.charAt(j));
if (txt.charAt(i) == pattern.charAt(j)) {
++j;
} else {
i -= j;
j = 0;
}
}
if (j == pLen) {
System.out.println("end... i = " + i + ",plen = " + pLen);
return i - pLen;
}
return -1;
}复制代码
ps: hello worldbash
public static void main(String[] args) {
BFArithmetic bf = new BFArithmetic();
String txt = "hello world";
String pattern = "world";
int res = bf.bfSearch(txt, pattern);
System.out.println("BF算法匹配结果:" + res);
// int resT = bf.bfSearchT(txt, pattern);
// System.out.println("BF算法(显示回退)匹配结果:" + resT);
}复制代码
rk算法至关于bf算法的进阶版,它主要是引入了哈希算法。下降了时间复杂度。经过哈希算法对主串中的 n-m+1 个子串分别求哈希值,而后逐个与模式串的哈希值比较大小。若是某个子串的哈希值与模式串相等,那就说明对应的子串和模式串匹配了。由于哈希值是一个数字,数字之间比较是否相等是很是快速的,因此模式串和子串比较的效率就提升了。dom
这里要把模式串预制进去,生成相对应的hash值,而后随机生成一个大素数,便于后续的使用。函数
private RKArithmetic(String pattern) {
this.pattern = pattern;
pLen = pattern.length();
aLen = 256;
slat = longRandomPrime();
System.out.println("随机素数:" + slat);
aps = 1;
// aLen^(pLen - 1) % slat
for (int i = 1; i <= pLen - 1; i++) {
aps = (aLen * aps) % slat;
}
patHash = hash(pattern, pLen);
System.out.println("patHash = " + patHash);
}复制代码
随机生成一个大素数测试
private static long longRandomPrime() {
BigInteger prime = BigInteger.probablePrime(31, new Random());
return prime.longValue();
}复制代码
哈希算法ui
private long hash(String txt, int i) {
long h = 0;
for (int j = 0; j < i; j++) {
h = (aLen * h + txt.charAt(j)) % slat;
}
return h;
}复制代码
校验字符串是否匹配
private boolean check(String txt, int i) {
for (int j = 0; j < pLen; j++)
if (pattern.charAt(j) != txt.charAt(i + j))
return false;
return true;
}复制代码
该实现仍是比较容易阅读的,只不过将比较换成了hash值的比较。
private int rkSearch(String txt) {
int n = txt.length();
if (n < pLen) return -1;
long txtHash = hash(txt, pLen);
if ((patHash == txtHash) && check(txt, 0))
return 0;
for (int i = pLen; i < n; i++) {
txtHash = (txtHash + slat - aps * txt.charAt(i - pLen) % slat) % slat;
txtHash = (txtHash * aLen + txt.charAt(i)) % slat;
int offset = i - pLen + 1;
System.out.println("第" + offset + "次txtHash = " + txtHash);
if ((patHash == txtHash) && check(txt, offset))
return offset;
}
return -1;
}复制代码
public static void main(String[] args) {
String pat = "world";
String txt = "hello world";
RKArithmetic searcher = new RKArithmetic(pat);
int res = searcher.rkSearch(txt);
System.out.println("RK算法匹配结果:" + res);
}复制代码
BM算法的轮子已经造好。听说是最高效,最经常使用的字符串匹配算法。
构建坏字符哈希表
private void generateBC(char[] patChars, int[] records) {
for (int i = 0; i < aLen; i++) {
records[i] = -1;
}
for (int i = 0; i < patChars.length; i++) {
// 计算 b[i] 的 ASCII 值
int ascii = (int) patChars[i];
records[ascii] = i;
}
System.out.println("坏字符哈希表:");
print(records);
}复制代码
好后缀
private void generateGS(char[] patChars, int[] suffix, boolean[] prefix) {
int pLen = patChars.length;
for (int i = 0; i < pLen; ++i) { // 初始化
suffix[i] = -1;
prefix[i] = false;
}
for (int i = 0; i < pLen - 1; ++i) {
int j = i;
// 公共后缀子串长度
int k = 0;
while (j >= 0 && patChars[j] == patChars[pLen - 1 - k]) {
--j;
++k;
//j+1 表示公共后缀子串在 patChars[0, i] 中的起始下标
suffix[k] = j + 1;
}
// 若是公共后缀子串也是模式串的前缀子串
if (j == -1) prefix[k] = true;
}
}复制代码
移动
private int moveByGS(int index, int pLen, int[] suffix, boolean[] prefix) {
int k = pLen - 1 - index; // 好后缀长度
if (suffix[k] != -1) return index - suffix[k] + 1;
for (int i = index + 2; i <= pLen - 1; i++) {
if (prefix[pLen - i])
return i;
}
return -1;
}复制代码
private int bmSearch(String txt, String pattern) {
// 记录模式串中每一个字符最后出现的位置
int[] records = new int[aLen];
char[] txtChars = txt.toCharArray();
int tLen = txtChars.length;
char[] patChars = pattern.toCharArray();
int pLen = patChars.length;
generateBC(patChars, records);
int[] suffix = new int[pLen];
boolean[] prefix = new boolean[pLen];
generateGS(patChars, suffix, prefix);
//主串与模式串对齐的第一个字符
int index = 0;
while (index <= tLen - pLen) {
int i = pLen - 1;
// 模式串从后往前匹配
for (; i >= 0; --i) {
// 坏字符对应模式串中的下标是 i
if (txtChars[index + i] != patChars[i]) break;
}
if (i < 0) {
return index;
}
int x = i - records[(int) txtChars[index + i]];
int y = 0;
if (i < pLen - 1) {
y = moveByGS(i, pLen, suffix, prefix);
}
System.out.println("x = " + x + ",y = " + y);
index = index + Math.max(x, y);
}
return -1;
}复制代码
public static void main(String[] args) {
BMArithmetic bmArithmetic = new BMArithmetic();
String txt = "hello world";
String pattern = "world";
int res = bmArithmetic.bmSearch(txt, pattern);
System.out.println("BM算法匹配结果:" + res);
}
复制代码
BM算法不愧是号称线性级得计算,听说效率是KMP算法的3~4倍,有时间必定要验一下。
ps:遇到问题若是正着思考行不通,不妨反着考虑,新思想,get√。
private BoyerMoore(String pattern) {
this.pattern = pattern;
pLen = pattern.length();
int aLen = 256;
records = new int[aLen];
//初始化记录数组,默认-1
for (int i = 0; i < aLen; i++) {
records[i] = -1;
}
//模式串中的字符在其中出现的最右位置
for (int j = 0; j < pLen; j++) {
records[pattern.charAt(j)] = j;
}
}复制代码
根据命名skip也能分析出俩关键字倒序,跳跃性。
private int bmSearch(String txt) {
int tLen = txt.length();
int skip;
for (int i = 0; i <= tLen - pLen; i += skip) {
skip = 0;
for (int j = pLen - 1; j >= 0; --j) {
System.out.println(txt.charAt(i + j) + " -- " + pattern.charAt(j));
if (txt.charAt(i + j) != pattern.charAt(j)) {
skip = j - records[txt.charAt(i + j)];
if (skip < 1) skip = 1;
break;
}
}
if (skip == 0) return i;
}
return -1;
}复制代码
public static void main(String[] args) {
String txt = "hello world";
String pattern = "world";
BoyerMoore bm = new BoyerMoore(pattern);
int res = bm.bmSearch(txt);
System.out.println("BM算法匹配结果:" + res);
}复制代码
kmp算法引入一个失效函数--next数组。这个算法的关键就在于next函数是如何计算出来的。妙趣横生?不,是头皮发麻,难以理解。只能debug一步一步跟了。
精髓:理解k = next[k]。由于前一个的最长串的下一个字符不与最后一个相等,须要找前一个的次长串,问题就变成了求0到next(k)的最长串,若是下个字符与最后一个不等,继续求次长串,也就是下一个next(k),直到找到,或者彻底没有。
private int[] getNext(char[] patChars, int pLen) {
int[] next = new int[pLen];
next[0] = -1;
int k = -1;
for (int i = 1; i < pLen; i++) {
while (k != -1 && patChars[k + 1] != patChars[i]) {
k = next[k];
}
if (patChars[k + 1] == patChars[i])
++k;
next[i] = k;
}
System.out.println("好前缀:");
print(next);
return next;
}复制代码
private int kmpSearch(String txt, String pattern) {
char[] txtChars = txt.toCharArray();
int tLen = txtChars.length;
char[] patChars = pattern.toCharArray();
int pLen = patChars.length;
int[] next = getNext(patChars, pLen);
int index = 0;
for (int i = 0; i < tLen; i++) {
while (index > 0 && txtChars[i] != patChars[index]) {
index = next[index - 1] + 1;
}
System.out.println(txtChars[i] + " -- " + patChars[index]);
if (txtChars[i] == patChars[index])
++index;
if (index == pLen)
return i - pLen + 1;
}
return -1;
}复制代码
public static void main(String[] args) {
KMPArithmetic kmpArithmetic = new KMPArithmetic();
String txt = "hello world";
String pattern = "world";
int res = kmpArithmetic.kmpSearch(txt, pattern);
System.out.println("KMP算法匹配结果:" + res);
}复制代码
private KMPByDFA(String pattern) {
this.pattern = pattern;
this.pLen = pattern.length();
int aLen = 256;
dfa = new int[aLen][pLen];
dfa[pattern.charAt(0)][0] = 1;
int i = 0;
for (int j = 1; j < pLen; j++) {
for (int k = 0; k < aLen; k++) {
//复制匹配失败状况下的值
dfa[k][j] = dfa[k][i];
}
//设置匹配成功状况下的值
dfa[pattern.charAt(j)][j] = j + 1;
//更新从新状态
i = dfa[pattern.charAt(j)][i];
}
}复制代码
private int kmpSearch(String txt) {
int i = 0;
int j = 0;
int tLen = txt.length();
for (; i < tLen && j < pLen; i++) {
j = dfa[txt.charAt(i)][j];
}
//找到匹配,到达模式串的结尾
if (j == pLen)
return i - pLen;
return -1;
}复制代码
public static void main(String[] args) {
String txt = "hello world";
String pattern = "world";
KMPByDFA kmp = new KMPByDFA(pattern);
int res = kmp.kmpSearch(txt);
System.out.println("BM算法匹配结果:" + res);
}复制代码
您的点赞和关注是对我最大的支持,谢谢!