1、设有两个串:目标串target和模式串pattern,在目标串target中查找与模式串pattern相等的一个子串并确定该子串位置的操作成为串的模式匹配。
(1)这里的相等是指:长度相等,且各对应字符相同
(2)这里介绍两种算法:Brute-Force算法和
2、Brute-Force算法,其实就是蛮力算法,一个个的进行寻找,匹配失败,就回溯:
(1)查找:
其中算法代码:
package list; /* * 定义一个常量字符串类,但是比String功能要少 */ public final class MyString implements Comparable<MyString>, java.io.Serializable { private final char[] value; // 字符串类都要有一个char数组用来存储字符集合 /* * 构造方法,构造一个空串 */ public MyString() { this.value = new char[0]; } /* * 由字符串常量构造字符串,本质是对字符串的深拷贝,只是方法不一样罢了 */ public MyString(java.lang.String str) { this.value = new char[str.length()]; // 申请空间 for (int i = 0; i < this.value.length; i++) { this.value[i] = str.charAt(i); // 进行赋值 } } /* * 把字符串value中的从第i位(包括第i位)开始后n位构造字符串 */ public MyString(char[] value, int i, int n) { if (i >= 0 && n >= 0 && i + n < value.length) { // 容错处理 this.value = new char[n]; // 申请空间 for (int j = 0; j < n; j++) this.value[j] = value[i + j]; // 赋值 } else throw new StringIndexOutOfBoundsException("i=" + i + ",n=" + n + ",i+n=" + (i + n)); } /* * 这个方法本质上和第二个构造方法是一致的,同时也是上面的构造方法的一个特例 */ public MyString(char[] value) { this(value, 0, value.length); } /* * 深拷贝构造方法,调用的是上面的方法 */ public MyString(MyString str) { this(str.value); } /* * 返回串长度,即字符数组容量 */ public int length() { return this.value.length; } /* * 形成字符串 */ public String toString() { return new String(this.value); } /* * 查询第i位字符,并返回 */ public char charAt(int i) { if (i >= 0 && i < this.value.length) return this.value[i]; throw new StringIndexOutOfBoundsException(i); } /* * 求子串,有开始位置也有结束位置 */ public MyString substring(int start, int end) { if (start == 0 && end == this.value.length) return this; return new MyString(this.value, start, end - start); // 最后要对应的是start之后的几位,所以用减号 } /* * 求子串方法重载,这个方法其实是是上一个方法的一个特例,所以直接调用上面方法 */ public MyString substring(int begin) { return substring(begin, this.value.length); } /* * 连接串,将自身串与参数str连接在一起,其实本质就是在串后面再添加一个串str 和字符串类String中的+差不多,不过不如那个好理解 * 这里实现了深度拷贝,返回不再是之前那个串了,而是一个新的对象 */ public MyString connect(MyString str) { if (str == null) str = new MyString("null"); char[] buffer = new char[this.value.length + str.length()]; int i; for (i = 0; i < this.value.length; i++) // 复制当前串 buffer[i] = this.value[i]; for (int j = 0; j < str.value.length; j++) // 复制指定串str buffer[i + j] = str.value[j]; return new MyString(buffer); } /* * 返回当前串(目标串)中首个与模式串pattern匹配的子串序号,失败则返回-1 调用了下面的方法 */ public int indexOf(MyString pattern) { return this.indexOf(pattern, 0); } /* * 返回当其串(目标串)从begin开始首个与模式串pattern匹配的子串序号,匹配失败返回-1 * */ public int indexOf(MyString pattern, int begin) { int n = this.length(), m = pattern.length(); if (begin < 0) // 容错机制 begin = 0; if (n == 0 || n < m || begin >= n) // 失败返回-1 return -1; int i = begin, j = 0; // i和j分别记录目标串和模式串当前字符下标 int count = 0; // 记载比较次数 while (i < n && j < m) { count++; if (this.charAt(i) == pattern.charAt(j)) { // 若当前两字符串相等,则继续比较后续的字符 i++; j++; } else { // 如果失败,i与j进行回溯,进行下一次匹配 i = i - j + 1; // 这里+1表示开始下一次匹配了 j = 0; if (i > n - m) break; } } if (j == m) // 匹配成功 return i - j; return -1; } /* * 比较串,如果同一位置出现不一样的,则返回差值 如果没有出现不一样的,则返回两者字符串的长度差 */ public int compareTo(MyString str) { for (int i = 0; i < this.value.length && i < str.value.length; i++) if (this.value[i] != str.value[i]) return this.value[i] - str.value[i]; return this.value.length - str.value.length; } public static void main(String[] args) { // TODO Auto-generated method stub MyString target = new MyString("aababcd"), pattern = new MyString("abcd"); System.out.println("\"" + target + "\".indexOf(\"" + pattern + "\")=" + target.indexOf(pattern)); } }
这个算法是建立在之前写的那个MyString类上的,里面具体的算法是IndexOf,里面的主方法是对该算法的验证,输出如下:
如果用不习惯,可以将其拿出来通过String类进行比较:
/* * 返回当前串(目标串)中首个与模式串pattern匹配的子串序号,失败则返回-1 调用了下面的方法 */ public int indexOf(String target, String pattern) { return indexOf(target, pattern, 0); } /* * 返回当其串(目标串)从begin开始首个与模式串pattern匹配的子串序号,匹配失败返回-1 * */ public int indexOf(String target, String pattern, int begin) { int n = target.length(), m = pattern.length(); if (begin < 0) // 容错机制 begin = 0; if (n == 0 || n < m || begin >= n) // 失败返回-1 return -1; int i = begin, j = 0; // i和j分别记录目标串和模式串当前字符下标 int count = 0; // 记载比较次数 while (i < n && j < m) { count++; if (target.charAt(i) == pattern.charAt(j)) { // 若当前两字符串相等,则继续比较后续的字符 i++; j++; } else { // 如果失败,i与j进行回溯,进行下一次匹配 i = i - j + 1; // 这里+1表示开始下一次匹配了 j = 0; if (i > n - m) break; } } if (j == m) // 匹配成功 return i - j; return -1; }
然后可以进行验证一下:
String a = "aababcd", b = "abcd"; test t = new test(); //test是我随便写的一个类,将上面的方法放入test类中,就可以直接进行字符串比较了 System.out.println("aababcd与abcd进行匹配:" + t.indexOf(a, b));
输出结果是:
(2)替换子串
/* * 替换子串,将target串中所有与pattern匹配的子串全部替换成sir,返回替换后的target串 */ public static StringBuffer replaceAll(StringBuffer target,String pattern,String str) { int i = target.indexOf(pattern); while(i!=-1) { target.delete(i, i+pattern.length()); target.insert(i, str); i = target.indexOf(pattern, i+str.length()); } return target; }
上面替换子串的原理很简单,首先匹配一下target串中所有的pattern串,然后将其删除,再重新插入你想要的串!
(3)移除子串
直接利用上面的替换子串的方法,也可以移除子串(将要替换的串变成“”即可),但是每次都要调用delete方法,效率太低了
采用另外一种方法:
/* * 删除target串中所有与pattern匹配的子串,返回删除后的target串 */ public static StringBuffer removeAll(StringBuffer target, String pattern) { int n = target.length(), m = pattern.length(); int empty = target.indexOf(pattern), next = empty; //empty为首个与pattern匹配的子串序号,每次都在变 while (next != -1) { //每次循环都要删除一个匹配子串 int move = next + m; next = target.indexOf(pattern, move); while (next > 0 && move < next || next < 0 && move < n) target.setCharAt(empty++, target.charAt(move++)); } if (empty != -1) target.setLength(empty); return target; }
原理介绍:
3、KMP算法(目标串不回溯)
(1)查找
其实也不算不回溯,而是部分回溯,请看下图:
大家看pattern串中,有abcabc,也就是前三个abc与后三个字母abc是相同的,如果当你匹配到第四位失败后,就可以从当前匹配的地点重新开始,因为前三个字母abc没有相似的,也就如果你回溯三位,也不会出现匹配的模式。
再看上面的图,可以看到如果当你匹配到第6位时,失败,这时就要回溯部分,因为后三位abc有一部分匹配成功了,也就是如果你回溯到四位至少前两位是匹配成功,至于后面要继续进行匹配
算法代码:
private static int[] next; // 模式串pattern改进的next数组 private static int[] nextk; // 模式串pattern未改进的next数组 /* * 返回目标串target中首个与模式串pattern匹配的子串序号,匹配失败时返回-1 */ public static int indexOf(String target, String pattern) { return indexOf(target, pattern, 0); } /* * 返回目标串target从begin开始首个与模式串pattern匹配的子串序号,匹配失败时返回-1。 * 0≤begin<target.length()。对begin容错,若begin<0,从0开始;若begin序号越界,查找不成功。 * 若target、pattern为null,抛出空对象异常。 */ public static int indexOf(String target, String pattern, int begin) { int n = target.length(), m = pattern.length(); if (begin < 0) // 对begin容错,若begin<0,从0开始 begin = 0; if (n == 0 || n < m || begin >= n) // 若目标串空、较短或begin越界,不需比较 return -1; int count = 0; // 记载比较次数 nextk = getNextk(pattern); System.out.print("nextk[]: "); next = getNext(pattern); // 返回模式串pattern改进的next数组 System.out.print("next[]: "); print(next); int i = begin, j = 0; // i、j分别为目标串、模式串比较字符下标 while (i < n && j < m) { if (j != -1) count++; if (j == -1 || target.charAt(i) == pattern.charAt(j))// 若当前两字符相等,则继续比较后续字符 { if (j != -1) System.out.print("t" + i + "=p" + j + ","); i++; j++; } else // 否则,下次匹配,目标串下标i不回溯 { System.out.println("t" + i + "!=p" + j + ",next[" + j + "]=" + next[j]); j = next[j]; // 模式串下标j退回到下次比较字符序号 if (n - i + 1 < m - j + 1) // 若目标串剩余子串的长度不够,不再比较, //比第3版增加此句 break; } } System.out.println("\tKMP.count=" + count); if (j == m) // 匹配成功 return i - j; // 返回匹配的子串序号 return -1; // 匹配失败 } /* * 返回模式串pattern的next数组 */ private static int[] getNextk(String pattern) { int j = 0, k = -1, next[] = new int[pattern.length()]; next[0] = -1; while (j < pattern.length() - 1) if (k == -1 || pattern.charAt(j) == pattern.charAt(k)) { j++; k++; next[j] = k; // 有待改进 } else k = next[k]; return next; } /* * 返回模式串pattern改进的next数组 */ private static int[] getNext(String pattern) { int j = 0, k = -1, next[] = new int[pattern.length()]; next[0] = -1; while (j < pattern.length() - 1) if (k == -1 || pattern.charAt(j) == pattern.charAt(k)) { j++; k++; if (pattern.charAt(j) != pattern.charAt(k)) // 改进之处 next[j] = k; else next[j] = next[k]; } else k = next[k]; return next; }
这里比较关键的一点,如果找到pattern子串中有哪些相同的重叠部分(比如abcabc中,前三个字符和后三个字符重叠,这只是一个非常简单的例子,还有aaaa,那你怎么算它的重叠部分呢?如果是这种情况,KMP算法和之前的暴力算法其实是一致的,必须要全部回溯)
那么如何查找重叠部分(程序自动查找,不能针对某一种特殊的结构去设计),这里可以采用一个一个数组的方式,去匹配pattern的内容,记住这里的匹配只与pattern串有关,与其他无关!
可以看一下上面的例子,就是这个数组的计算方法,通过这里的k是指相同的数值,当前子串中的最大整数,不过我没有明白为什么当j=0的时候,k=-1,而不是0呢,后来我想明白了当只有一位的时候:a,它本身就和它本身是相同的,所以也算一位(0-1=1)。
上面有一个改进的next数组方法,是减少了一些不必要的比较,更加优化
(2)其他的替换和删除和上一种方法是一致的,这里就不说了。
参考书籍:《数据结构(java版)》叶核亚,有不懂的,可以再看一下这本书