1、KMP算法
KMP算法是一种非常高效且经典的字符串匹配算法,它通过巧妙的预处理机制,显著提升了在主文本串中查找模式串(被包含的字符串)的效率。
1.1、KMP算法的核心思想
- 在字符串匹配问题中,最直观的方法是暴力匹配,即从主串的每一个可能位置开始,逐个字符与模式串进行比较。
- 如果失配,主串的指针就回溯到本次起始位置的下一位。
- 这种方法在最坏情况下的时间复杂度是 O(nm)*(n和m分别是主串和模式串的长度),当字符串很长时效率很低。
- KMP算法的核心思想是利用模式串自身的结构信息,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
- 在实现的过程中,KMP算法先求出模式串的next数组,next数组每个位置之前子串的"最长公共前后缀的长度"信息。
- 然后在匹配的过程中,主串的下标x从0开始依次往后走,不进行回溯,子串的下标y一开始也从0开始,x和y下标的位置进行匹配,
- 在这个过程中,x一直不回溯,y根据next数组的信息进行跳转,直到匹配到x或者y到头为止。
- 在这个过程中有三种情况:
- 1、x和y匹配成功,x和y都往后走一位。
- 2、y跳转到了next为-1的时候,即y回到了0位置,x往后走一位。
- 3、x和y匹配失败,y没有到-1,y根据next数组的信息进行跳转(即y = next[y]),直到匹配到x或者y到头为止。
- 为什么y可以根据next的信息进行跳转呢?
- 因为next数组中i位置记录的是i前面的最长公共子串的长度,如果i位置的值为y,则代表模式串str2的str2[0...y-1]等于str2[i-y...i-1],
- 因为主串str1已经走到了x位置,模式串走到了y位置,此时str1[x] != str2[y],说明str1[x-y...x-1]等于str2[0...y-1],
- 而在next数组中,y位置的值为j,则代表模式串str2的str2[0...j-1]等于str2[y-j...y-1],
- 所以可以看出str1[x-j,x-1]、str2[0...j-1]和str2[y-j...y-1]三个数组的值是相等的。
- 此时y跳到j的位置,只需要继续匹配str1[x]和str2[j]的值是否相等即可,恰面0到j-1的位置因为相等,就可以直接跳过了。
- KMP算法的核心就是通过这种跳转,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
- 复杂度为O(n+m),其中n是主串的长度,m是模式串的长度。
1.2、next数组的含义
- next数组的含义:
- next数组的最长公共前后缀的长度的含义包含了几点:
- 1、i位置的信息不包含i位置,是其前面的信息,且不能是[0,i-1]的范围(即不能包含i前面的所有字符)
- 2、i位置的信息的相等的子串的两端字符一端必须以0开头,另一端必须以i-1位置的字符结尾,这个相等的子串的长度就是i位置的next数组的值。
- 3、根据上面的定义,可以知道0位置没有前面的字符,定义为-1,1位置前面只有1个字符,因为不能全包含,所以为0。
1.3、next数组的快速计算
- 根据next数组的含义,i位置的信息代表了前面的最长公共子串的长度,且其中一个字段是从0开始的,所以i位置的信息就是第一个子串的第一个不相等的位置。
- 例如:模式串为:ababa,next数组为:[-1,0,0,1,2],对于为4的位置,其值为2,前面的公共子串为ab,所以在字符串中2的位置刚好是第一个子串ab的b后面的一个位置(下标从0开始)。
- 将原数组记为str,如果要求i位置的值,如果next[i-1]=j,则表示str[0...j-1]等于str[i-1-j...i-1-1],所以j位置和i-1位置分别是第一个和第二个子串不同的位置,
- 1、此时如果str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1,
- 比如模式串str为:ababa,next数组为:[-1,0,0,1,2],求i=4位置的next数组值,因为next[i-1]=1,所以str[0,0]str2,2,
- 此时如果判断str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1=2。即str[4]=2
- 2、如果str[j] != str[i-1],则代表在i-位置的基础上,不能增加一个值,需要重新判断,此时,如果j已经到了0位置,则代表前面没有公共子串,所以next[i]=0。
- 比如str为:abcabdt,next数组为:[-1,0,0,0,1,2,?],求i=6位置的next数组值,此时i-1的位置为5,next[5]=2(即j==2),所以str[0,1]str3,4,
- 此时判断str[j]是否等于str[i-1],因为c不等于d,且next[j]==0,所以next[i]=0。
- 3、如果str[j] != str[i-1]且j != 0,此时,j要跳到next[j]的位置,继续判断,直到判断出相等或者j最终为0。
- 比如str为acdbstacdtxeacdbstacdbk,我们要求i=22(即k位置)的next数组,此时i-1的位置为21,next[21]=9(最长前缀为acdbstacd),即j=9,
- 所以str[0,8]str[12,20] (acdbstacd==acdbstacd),此时判断str[9]是否等于str[21],因为t不等于b,所以不能直接求出next[22]的值,
- 此时要看next[j]的值,str[9]的字符为t,此时next[9]为3(相等的字符为acd),所以此时的j=next[j]=3,此时继续判断str[3]是否等于str[21],因为str[3]为b,
- str[21]为b,相等,next[i]=j+1,此时的j为3,所以next[22]=4。即22位置相等的前缀为acdb.
- 之所以在判断的过程中采用str[i-1]的位置不变,j的位置根据next数组不断往前缩的形式,是因为next数组的性质决定的,因为next要求的前缀公共子串中一个要以0开头,
- 一个要以i-1结尾,我们就可以不断的根据以前的next的值,尝试前面最短相等的前缀,看看能不能加上i-1位置的字符,如果能加上,就可以直接用前面的值加一求出i的值,
- 如果加不上,最后就只能为0.
2、KMP算法的实现
java
/**
* KMP算法
*/
public static int getIndexOf(String s1, String s2) {
if (s1 == null || s2 == null || s2.isEmpty() || s1.length() < s2.length()) {
return -1;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
// str1的匹配位置
int x = 0;
// str2的匹配位置
int y = 0;
// O(M) m <= n
int[] next = getNextArray(str2);
// 开始匹配
while (x < str1.length && y < str2.length) {
if (str1[x] == str2[y]) {
// 对应位置相等,匹配下个位置
x++;
y++;
} else if (next[y] == -1) {
// y到了0的位置,x到下个位置继续匹配
x++;
} else {
// y往前跳转
y = next[y];
}
}
// 如果匹配成功,y就到了str2的末尾
// str2在str1里面的起始位置就是x - y(即x - str2.length)
return y == str2.length ? x - y : -1;
}
/**
* 求next数组
*/
public static int[] getNextArray(char[] str) {
if (str == null || str.length == 0) {
return new int[0];
}
if (str.length == 1) {
return new int[]{-1};
}
int[] next = new int[str.length];
// 0位置为-1,1位置为0
next[0] = -1;
next[1] = 0;
// 求next数组的位置,从2开始
int i = 2;
// next数组的跳转的值
int j = 0;
while (i < next.length) {
if (str[i - 1] == str[j]) {
// 匹配成功,直接在前一个的值上面+1
next[i++] = ++j;
} else if (j > 0) {
// 还可以跳转
j = next[j];
} else {
// j 无法跳转了,目前位置为0
next[i++] = 0;
}
}
return next;
}
全部代码和测试:
java
/**
* KMP算法:
* KMP算法是一种非常高效且经典的字符串匹配算法,它通过巧妙的预处理机制,显著提升了在主文本串中查找模式串(被包含的字符串)的效率。
*
* <br>
* KMP算法的核心思想:
* 在字符串匹配问题中,最直观的方法是暴力匹配,即从主串的每一个可能位置开始,逐个字符与模式串进行比较。
* 如果失配,主串的指针就回溯到本次起始位置的下一位。
* 这种方法在最坏情况下的时间复杂度是 O(nm)*(n和m分别是主串和模式串的长度),当字符串很长时效率很低。
* KMP算法的核心思想是利用模式串自身的结构信息,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
* 在实现的过程中,KMP算法先求出模式串的next数组,next数组每个位置之前子串的"最长公共前后缀的长度"信息。
* 然后在匹配的过程中,主串的下标x从0开始依次往后走,不进行回溯,子串的下标y一开始也从0开始,x和y下标的位置进行匹配,
* 在这个过程中,x一直不回溯,y根据next数组的信息进行跳转,直到匹配到x或者y到头为止。
* 在这个过程中有三种情况:
* 1、x和y匹配成功,x和y都往后走一位。
* 2、y跳转到了next为-1的时候,即y回到了0位置,x往后走一位。
* 3、x和y匹配失败,y没有到-1,y根据next数组的信息进行跳转(即y = next[y]),直到匹配到x或者y到头为止。
* 为什么y可以根据next的信息进行跳转呢?
* 因为next数组中i位置记录的是i前面的最长公共子串的长度,如果i位置的值为y,则代表模式串str2的str2[0...y-1]等于str2[i-y...i-1],
* 因为主串str1已经走到了x位置,模式串走到了y位置,此时str1[x] != str2[y],说明str1[x-y...x-1]等于str2[0...y-1],
* 而在next数组中,y位置的值为j,则代表模式串str2的str2[0...j-1]等于str2[y-j...y-1],
* 所以可以看出str1[x-j,x-1]、str2[0...j-1]和str2[y-j...y-1]三个数组的值是相等的。
* 此时y跳到j的位置,只需要继续匹配str1[x]和str2[j]的值是否相等即可,恰面0到j-1的位置因为相等,就可以直接跳过了。
* KMP算法的核心就是通过这种跳转,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
* 复杂度为O(n+m),其中n是主串的长度,m是模式串的长度。
* <br>
* next数组的含义:
* next数组的最长公共前后缀的长度的含义包含了几点:
* 1、i位置的信息不包含i位置,是其前面的信息,且不能是[0,i-1]的范围(即不能包含i前面的所有字符)
* 2、i位置的信息的相等的子串的两端字符一端必须以0开头,另一端必须以i-1位置的字符结尾,这个相等的子串的长度就是i位置的next数组的值。
* 3、根据上面的定义,可以知道0位置没有前面的字符,定义为-1,1位置前面只有1个字符,因为不能全包含,所以为0。
* <br>
* next数组的快速计算:
* 根据next数组的含义,i位置的信息代表了前面的最长公共子串的长度,且其中一个字段是从0开始的,所以i位置的信息就是第一个子串的第一个不相等的位置。
* 例如:模式串为:ababa,next数组为:[-1,0,0,1,2],对于为4的位置,其值为2,前面的公共子串为ab,所以在字符串中2的位置刚好是第一个子串ab的b后面的一个位置(下标从0开始)。
* 将原数组记为str,如果要求i位置的值,如果next[i-1]=j,则表示str[0...j-1]等于str[i-1-j...i-1-1],所以j位置和i-1位置分别是第一个和第二个子串不同的位置,
* 1、此时如果str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1,
* 比如模式串str为:ababa,next数组为:[-1,0,0,1,2],求i=4位置的next数组值,因为next[i-1]=1,所以str[0,0]str[2,2](str的0位置和2位置的a相等),
* 此时如果判断str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1=2。即str[4]=2
* 2、如果str[j] != str[i-1],则代表在i-位置的基础上,不能增加一个值,需要重新判断,此时,如果j已经到了0位置,则代表前面没有公共子串,所以next[i]=0。
* 比如str为:abcabdt,next数组为:[-1,0,0,0,1,2,?],求i=6位置的next数组值,此时i-1的位置为5,next[5]=2(即j==2),所以str[0,1]str[3,4](ab==ab),
* 此时判断str[j]是否等于str[i-1],因为c不等于d,且next[j]==0,所以next[i]=0。
* 3、如果str[j] != str[i-1]且j != 0,此时,j要跳到next[j]的位置,继续判断,直到判断出相等或者j最终为0。
* 比如str为acdbstacdtxeacdbstacdbk,我们要求i=22(即k位置)的next数组,此时i-1的位置为21,next[21]=9(最长前缀为acdbstacd),即j=9,
* 所以str[0,8]str[12,20](acdbstacd==acdbstacd),此时判断str[9]是否等于str[21],因为t不等于b,所以不能直接求出next[22]的值,
* 此时要看next[j]的值,str[9]的字符为t,此时next[9]为3(相等的字符为acd),所以此时的j=next[j]=3,此时继续判断str[3]是否等于str[21],因为str[3]为b,
* str[21]为b,相等,next[i]=j+1,此时的j为3,所以next[22]=4。即22位置相等的前缀为acdb.
* 之所以在判断的过程中采用str[i-1]的位置不变,j的位置根据next数组不断往前缩的形式,是因为next数组的性质决定的,因为next要求的前缀公共子串中一个要以0开头,
* 一个要以i-1结尾,我们就可以不断的根据以前的next的值,尝试前面最短相等的前缀,看看能不能加上i-1位置的字符,如果能加上,就可以直接用前面的值加一求出i的值,
* 如果加不上,最后就只能为0.
*/
public class KMP {
/**
* KMP算法
*/
public static int getIndexOf(String s1, String s2) {
if (s1 == null || s2 == null || s2.isEmpty() || s1.length() < s2.length()) {
return -1;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
// str1的匹配位置
int x = 0;
// str2的匹配位置
int y = 0;
// O(M) m <= n
int[] next = getNextArray(str2);
// 开始匹配
while (x < str1.length && y < str2.length) {
if (str1[x] == str2[y]) {
// 对应位置相等,匹配下个位置
x++;
y++;
} else if (next[y] == -1) {
// y到了0的位置,x到下个位置继续匹配
x++;
} else {
// y往前跳转
y = next[y];
}
}
// 如果匹配成功,y就到了str2的末尾
// str2在str1里面的起始位置就是x - y(即x - str2.length)
return y == str2.length ? x - y : -1;
}
/**
* 求next数组
*/
public static int[] getNextArray(char[] str) {
if (str == null || str.length == 0) {
return new int[0];
}
if (str.length == 1) {
return new int[]{-1};
}
int[] next = new int[str.length];
// 0位置为-1,1位置为0
next[0] = -1;
next[1] = 0;
// 求next数组的位置,从2开始
int i = 2;
// next数组的跳转的值
int j = 0;
while (i < next.length) {
if (str[i - 1] == str[j]) {
// 匹配成功,直接在前一个的值上面+1
next[i++] = ++j;
} else if (j > 0) {
// 还可以跳转
j = next[j];
} else {
// j 无法跳转了,目前位置为0
next[i++] = 0;
}
}
return next;
}
public static void main(String[] args) {
int possibilities = 5;
int strSize = 20;
int matchSize = 5;
int testTimes = 5000000;
System.out.println("测试开始");
for (int i = 0; i < testTimes; i++) {
String str = getRandomString(possibilities, strSize);
String match = getRandomString(possibilities, matchSize);
int ans = getIndexOf(str, match);
int comparator = str.indexOf(match);
if (ans != comparator) {
System.out.println("测试失败!");
System.out.printf("str:%s,match:%s,ans:%d,comparator:%d", str, match, ans, comparator);
break;
}
}
System.out.println("测试结束");
}
// for test
public static String getRandomString(int possibilities, int size) {
char[] ans = new char[(int) (Math.random() * size) + 1];
for (int i = 0; i < ans.length; i++) {
ans[i] = (char) ((int) (Math.random() * possibilities) + 'a');
}
return String.valueOf(ans);
}
}
3、KMP算法的练习题目
3.1、另一棵树的子树
- 题目一:另一棵树的子树
- 给定两棵二叉树的头节点head1和head2,返回head1中是否有某个子树的结构和head2完全一样
- 测试链接 : https://leetcode.cn/problems/subtree-of-another-tree/
- 这种算法不是这个题的最优解,但是可以用这种思路解
- 思路:
- 将两个数按照同一种遍历方式进行遍历,形成一个数组,然后用kmp算法判断subRoot的数组是否是root的数组的子数组
java
/**
* 判断root是否包含subRoot的子树
* 思路:
* 将两个数按照同一种遍历方式进行遍历,形成一个数组,然后用kmp算法判断subRoot的数组是否是root的数组的子数组
*/
public static boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (subRoot == null) {
return true;
}
if (root == null) {
return false;
}
// 先用前序遍历两棵树,然后转为数组
ArrayList<String> rootList = new ArrayList<>();
ArrayList<String> subRootList = new ArrayList<>();
preSerial(root, rootList);
preSerial(subRoot, subRootList);
// 转为数组
String[] rootStr = new String[rootList.size()];
for (int i = 0; i < rootStr.length; i++) {
rootStr[i] = rootList.get(i);
}
String[] subRootStr = new String[subRootList.size()];
for (int i = 0; i < subRootStr.length; i++) {
subRootStr[i] = subRootList.get(i);
}
// 用kmp算法判断subRoot的数组是否是root的数组的子数组
return getIndexOf(rootStr, subRootStr) != -1;
}
/**
* KMP算法判断subRootStr是否是rootStr的子数组
*/
private static int getIndexOf(String[] rootStr, String[] subRootStr) {
if (rootStr == null || subRootStr == null || rootStr.length < 1 || rootStr.length < subRootStr.length) {
return -1;
}
int x = 0;
int y = 0;
int[] next = getNextArray(subRootStr);
while (x < rootStr.length && y < subRootStr.length) {
if (isEqual(rootStr[x], subRootStr[y])) {
x++;
y++;
} else if (next[y] == -1) {
x++;
} else {
y = next[y];
}
}
return y == subRootStr.length ? x - y : -1;
}
public static int[] getNextArray(String[] ms) {
if (ms.length == 1) {
return new int[]{-1};
}
int[] next = new int[ms.length];
next[0] = -1;
next[1] = 0;
int i = 2;
int cn = 0;
while (i < next.length) {
if (isEqual(ms[i - 1], ms[cn])) {
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
public static boolean isEqual(String a, String b) {
if (a == null && b == null) {
return true;
} else {
if (a == null || b == null) {
return false;
} else {
return a.equals(b);
}
}
}
/**
* 前序遍历,放入list中
* 因为有顺序,这里用ArrayList,
* 因为要填充null,所以要转为string,不能直接用int
*/
public static void preSerial(TreeNode node, ArrayList<String> res) {
if (node == null) {
res.add(null);
} else {
res.add(String.valueOf(node.val));
preSerial(node.left, res);
preSerial(node.right, res);
}
}
3.2、旋转字符串
- 题目一:旋转字符串
- 判断str1和str2是否互为旋转字符串,
- s的旋转操作就是将s最左边的字符移动到最右边,类似于二进制的按位循环左移。
- 例如, 若 s = 'abcde',在旋转一次之后结果就是'bcdea'
- 测试链接 : https://leetcode.cn/problems/rotate-string/
- 思路:
- 因为这里的旋转字符是类似于按位左移的效果,所以可以把一个字符串直接拼接上本上,那么移动后的字符串就会包含在拼接后的字符串中
- 判断是否包含,可以用kmp算法
java
/**
* 判断str1和str2是否互为旋转字符串
* 思路:
* 因为这里的旋转字符是类似于按位左移的效果,所以可以把一个字符串直接拼接上本上,那么移动后的字符串就会包含在拼接后的字符串中
* 判断是否包含,可以用kmp算法
*/
public static boolean rotateString(String a, String b) {
if (a == null || b == null || a.length() != b.length()) {
return false;
}
String b2 = b + b;
return getIndexOf(b2, a) != -1;
}
// KMP Algorithm
public static int getIndexOf(String s, String m) {
if (s.length() < m.length()) {
return -1;
}
char[] ss = s.toCharArray();
char[] ms = m.toCharArray();
int si = 0;
int mi = 0;
int[] next = getNextArray(ms);
while (si < ss.length && mi < ms.length) {
if (ss[si] == ms[mi]) {
si++;
mi++;
} else if (next[mi] == -1) {
si++;
} else {
mi = next[mi];
}
}
return mi == ms.length ? si - mi : -1;
}
public static int[] getNextArray(char[] ms) {
if (ms.length == 1) {
return new int[]{-1};
}
int[] next = new int[ms.length];
next[0] = -1;
next[1] = 0;
int pos = 2;
int cn = 0;
while (pos < next.length) {
if (ms[pos - 1] == ms[cn]) {
next[pos++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[pos++] = 0;
}
}
return next;
}
后记
个人学习总结笔记,不能保证非常详细,轻喷