算法学习10:字符串算法(KMP算法和Manacher算法)

KMP算法

KMP算法解决的问题

KMP算法用来解决字符串匹配问题: 找到长串短串出现的位置.

KMP算法思路

暴力比较与KMP的区别

  • 暴力匹配: 对长串的每个位,都从头开始匹配短串的所有位.

    暴力匹配演示

  • KMP算法: 将短字符串前后相同的部分存储在next数组里,让之前匹配过的信息指导之后的匹配.

    KMP算法演示

next数组及其求法

next数组

next数组体现字符串某位前边子串最长匹配前缀最长匹配后缀的匹配长度(可以有交叉).next[i]表示第i位前边的最长匹配后缀对应的最长匹配前缀的后一位.

下面是一个next数组的例子:

下标索引01234567
字符abababca
next数组对应位值-10012340
next数组的求法

next数组的求法用到了递推思想:

  1. 0位前边没有匹配前后缀,因此next[0]=-1

  2. 1前缀字符串的长度为1,因此next[1]=0

  3. 站在第pos位上,就要考虑以第i-1位结尾的最长匹配(也就是不断地求nums[pos-1]next[next[...next[pos-1]]]之间的关系):

    在这里插入图片描述

  • cn指针指向前一个匹配后缀的下一位,即cn=next[pos-1].
    • 若前一个匹配后缀的下一位字符刚好与待匹配字符相同,即ms[cn]==ms[pos-1],则更新next数组,将next[pos]指向cn的下一位next[pos]=cn+1.
    • 若前一个匹配后缀的下一位字符与待匹配字符不同,即ms[cn]!=ms[pos-1],则应该找前一个匹配数组,即cn=next[cn].
  • 若最终找到cn==-1也没找到,则pos-1位实在没有匹配后缀了,将其next数组置0,即next[pos]=0.
// 计算ms字符串的next[]数组
public static int[] getNextArray(char[] ms) {
	if (ms.length == 1) {
		return new int[] { -1 };
	}
	int[] next = new int[ms.length];
	next[0] = -1;	// 第0位前边没有前缀字符串,因此返回-1
	next[1] = 0;	// 第1位前缀字符串的长度为1,若该位匹配不上则只能去匹配首位,因此返回0

	// 从第2位开始,递推出后面位的next[]值
	for (int pos = 2; pos < ms.length; pos++) {
		// 一直向前找前边位的匹配前缀且该前缀的后一位与本位相同
		int cn = next[pos - 1];
		while (cn != -1 && ms[cn] != ms[pos-1]) {
			// 将前缀的后一位与当前位的前一位进行对比
			// 注意容易写错,花了两个小时排查这个玩意
			cn = next[cn];
		}

		// 判断是否能找到匹配前缀
		//if (cn != -1) {
		//	// 若能找到匹配前后缀,则返回匹配前缀的后一位
		//	next[pos] = cn + 1;
		//} else {
		//	// 若找不到匹配前后缀,返回-1
		//	next[pos] = 0;
		//}
		// 上述判断语句可以简化为一句
		next[pos] = cn + 1;
	}	
	return next;
}

在写入next数组的pos位时要注意: 匹配前缀延伸的时候,要判断的是ms[cn]ms[pos-1]之间是否相等

(因为next数组保存的是本位前一位的匹配前缀的下一位,因此ms[cn]指向匹配前缀的下一位,ms[pos-1]指向上次计算未进行匹配的位)

KMP算法匹配步骤leetcode 28

算法步骤:

  1. 短串计算出next数组.

  2. 长串进行匹配:比较长串第i位短串第j位进行比较匹配(初始时i=j=0).

    1. 长串第i位短串第j位相等,则这两位匹配了,i,j都向后移动一位,即i+=1,j==1.
    2. 长串第i位短串第j位不匹配 且 短串前边的所有位都能匹配,为避免重复比较,我们要去找短串第j位前边最长匹配后缀对应的最长匹配前缀.则将短串向后推,将长串第i位短串第next[j]位进行匹配,即j=next[j].
    3. 短串被推至第一位,即next[j]==-1,可以说明长串第i位不可能匹配上了,因此放弃长串这一位,转而寻求长串下一位,即i+=1.
public static int getIndexOf(String s, String m) {
	if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
		return -1;
	}
	char[] ss = s.toCharArray();
	char[] ms = m.toCharArray();
	int[] next = getNextArray(ms);
	int si = 0; // 长串指针
	int mi = 0;// 短串指针

	// 两个指针分别从字符串首位开始,遍历字符串1并进行匹配
	while (si < ss.length && mi < ms.length) {
		if (ss[si] == ms[mi]) {
			// 若两指针指向字符相等,则匹配上了一位,两指针分别滑向下一位
			si++;
			mi++;
		} else if (next[mi] != -1) {
			// 两指针指向字符不相等,但短串指针之前部分是匹配好的
			// 因此短串指针指向其上一个其匹配前缀,即next[i]
			// 这里要注意是next[mi]!=-1而不是mi!=-1,因为mi若为-1则发生索引错误,因此mi退到0就退无可退了
			mi = next[mi];
		} else {
			// 短串指针指向了首位也没匹配上,则此时只能将长串指针向后移动一位了
			si++;
		}
	}

	// 若子串已经被遍历完了,说明其每一位都能匹配上了,否则就没匹配上
	return (mi == ms.length) ? (si - mi) : -1;
}

证明匹配过程的正确性: 反证法

若匹配不成功,将短串向后移动至next[i]位,这本质上否定了中间区域匹配成功的可能性.其正确性可以用反证法来证明:
若后推小于i-next[i]位就能匹配上,那么第i位的最长匹配前后缀应该更长.

应用

应用1: 倍增字符串

题目: 在原字符串后添加尽量少个字符,使得生成的新字符串包含两个等于原字符串的子串(可以重叠).
如: 原字符串为abcabc,在其后添加abc,构成abcabcabc,其包含两个abcabc子串

解法: 本质上就是求整个字符串的最长匹配前后缀,因此构造扩展next数组,计算到第字符串长度位. 然后根据整个字符串的最长匹配前后缀补位.

应用2: 扩充成回文串leetcode 214

问题: 给定一个字符串,在其前边添加尽量少个字符使之成为回文串.

解法: 求出原字符串的逆序串,将逆序串原字符串进行KMP匹配,匹配结束后两字符串遍历指针勾勒出了逆序串原字符串最长匹配部分,将最长匹配部分之外的位进行扩充生成新字符串.

应用3: 重复子字符串leetcode 459

问题: 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成.

解法: 先构造因此构造扩展next数组,计算到第字符串长度位.

  • next[s.length()]为0,说明整个字符串都没有最长匹配前后缀,则其必然不是由一个字串多次重复而成
  • next[s.length()]不为0,则计算delta = s.length() - next[s.length()], 若delta能整除s.length(),则说明前后缀错开delta位也能匹配上,这说明该字符串必然能被分为多个重复子字符串.

应用4: 寻找匹配子树

题目: 给出两棵树,判断其中一棵树是否是另一棵树的子树

解法: 将两棵树分别序列化成字符串,然后根据KMP算法查找子串

Manacher算法

Manacher算法解决的问题

Manacher算法用来寻找字符串的最长回文子串.

Manacher算法思路

暴力查找方法

  • 回文串按照长度的奇偶性分为两种: 奇回文偶回文,
    • 奇回文: 好判断,以某个字符为对称轴向两边延展,找到最长回文子串.
    • 偶回文: 不好判断,其对称中心并不是某个具体字符位.
  • 其中奇回文比较好辨认,因此我们将字符串的所有偶回文都改成奇回文.
  • 具体做法: 向整个字符串当中每两个字符之间加一个标志位,得到扩展串.以扩展串每一位为中心,向两边扩展,查找回文. 这样找到的所有回文都是奇回文,将(回文长度-1)/2得到真实回文长度.

中间加入的标志位字符不一定非得是没出现过的字符,因为在扩展串中,虚轴永远只会和虚轴比对,实轴永远只和实轴比对,不会产生混淆.

回文串的相关概念

  1. 回文右边界: 当前所有回文串中,回文右边界所能到达的最右位置.

  2. 回文右边界中心: 以回文右边界结尾的最长回文串的回文中心.

manacher算法步骤

从第一位开始,遍历字符串的所有位.初始时回文右边界-1.

在这里插入图片描述

当遍历到第pos位时,有以下两种情况:

  1. pos不在回文右边界rightBorder以内(pos > rightBorder): 以该位为中心向两边暴力扩展回文串.

  2. pos回文右边界rightBorder以内(pos <= rightBorder): 找到回文右边界中心rightCenter,对应的回文左边界,以及pos关于回文右边界中心的对称点pos'.这样pos'回文半径可以用来指导pos回文半径.

    1. pos'回文串关于rightCenter的对称串在回文右边界以内(pos + curRadius[pos] -1 <= rightBorder),说明pos回文半径pos'回文半径相同.

    2. pos'回文串关于rightCenter的对称串在回文右边界以外(pos + curRadius[pos] -1 > rightBorder),由对称性可知,对称串超出回文右边界的部分一定不能构成以pos为中心的回文.因此pos的回文半径即为pos回文右边界之间.

在遍历过程中维护的回文右边界串并不一定是最长回文子串,如上面演示图中第一种情况为例: 遍历结束时,回文右边界串"abcba",最长回文子串为"abcbadabcba".

代码实现leetcode 5

// 在字符串中间加标志位,将所有 偶回文 变为 奇回文
public static char[] manacherString(String str) {
	char[] charArr = str.toCharArray();
	char[] res = new char[str.length() * 2 + 1];
	int index = 0;
	for (int i = 0; i != res.length; i++) {
		res[i] = ((i & 1) == 0) ? '#' : charArr[index++];
	}
	return res;
}

// 返回最长回文长度
public int maxLcpsLength(String s) {

	char[] charArr = manacherString(s); 		// 扩展字符串
	int[] curRadius = new int[charArr.length]; 	// 回文半径数组,记录以每一位为中心的回文半径
	Arrays.fill(curRadius, 1);
	int rightBorder = -1;		// 回文右边界,初始时为-1,表示没找到任何回文串
	int rightCenter = -1;		// 回文右边界中心
	int maxRadius = -1;			// 最长回文半径长度

	// 依次遍历所有位,计算其回文半径
	for (int pos = 0; pos != charArr.length; pos++) {
		// 判断该位是否位于 `回文右边界` 以内:
		// 	若 `当前位置` 在 `回文右边界` 之外, 则暴力搜索,向两侧扩展计算其回文半径
		// 	若 `当前位置` 在 `回文右边界` 之内, 则根据其 关于回文右边界中心的对称位 来计算其回文半径
		if (pos <= rightBorder) {
			// 只保证 回文右边界串 之内的部分的回文性,不保证 回文右边界串 之外的回文性
			curRadius[pos] = Math.min(curRadius[2 * rightCenter - pos], rightBorder-pos+1);
		}
		// 暴力扩展回文半径(若 `当前位置` 在 `回文右边界` 之内,也扩不动,刚进入循环就跳出了)
		while ((pos + curRadius[pos] < charArr.length) && (pos - curRadius[pos] > -1)) {
			if (charArr[pos + curRadius[pos]] == charArr[pos - curRadius[pos]]) {
				curRadius[pos]++;
			} else {
				break;
			}
		}
		// 刷新 回文边界,回文边界中心,以及最大回文半径(注意回文右边界串未必是最长回文子串)
		if (pos + curRadius[pos] > rightBorder) {
			rightBorder = pos + curRadius[pos] - 1;
			rightCenter = pos;
		}
		maxRadius = Math.max(maxRadius, curRadius[pos]);
	}
	// `扩展串`的`回文半径`正好是`真实回文长度+1`
	return maxRadius - 1;
}

应用:

应用1: 扩充成回文串leetcode 214

问题: 给定一个字符串,在其前边添加尽量少个字符使之成为回文串.

解法: 进行Manacher匹配,直到回文右边界到达字符串末尾.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值