串结构之串的模式匹配

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版)》叶核亚,有不懂的,可以再看一下这本书

猜你喜欢

转载自blog.csdn.net/yuangan1529/article/details/80310702