Brute-Force算法
- Brute-Force算法简称BF算法,也叫简单匹配算法或暴力匹配算法,采用穷举法,其基本思想为:从目标串
str="aabaabaaf"
的第一个字符开始和模式串substr="aabaaf"
中的第一个字符相比较。若相等,则继续逐个比较后续字符;否则从目标串str的第二个字符开始重新与模式串substr中的第一个字符比较。以此类推,若从目标串str的第i个字符开始,每个字符依次与模式串substr中的对应字符相等,则匹配成功,该算法返回位置i——表示此时substr的第一个字符在str中出现的下标;否则,匹配失败,说明substr不是str的子串,返回-1 - 它对应的代码为:
package com.cdc.demo;
public class BruteForceDemo {
public static void main(String[] args) {
String str="aabaabaaf";
String substr="aabaaf";
System.out.println("index="+bruteForce(str,substr));
System.out.println("index="+str.indexOf(substr));
}
public static int bruteForce(String str,String subStr){
int i=0;
int j=0;
char[] s1 = str.toCharArray();
char[] s2 = subStr.toCharArray();
//确保不越界
while (i<s1.length && j<s2.length){
if (s1[i] == s2[j]){
i++;
j++;
}else {
//回退str的索引
i=i-(j-1);
j=0;
}
}
if (j >= s2.length){
//返回子串substr第一次出现的位置
return i-j;
}
return -1;
}
}
其运行结果为:
"C:\Program Files\Java\jdk1.8.0_191\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\lib\idea_rt.jar=1026:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_191\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_191\jre\lib\rt.jar;C:\Users\admin\IdeaProjects\demo\out\production\demo" com.cdc.demo.BruteForceDemo
index=3
index=3
Process finished with exit code 0
- 在Brute-Force算法中,有一个很明显的缺点,就是它在匹配到不相同的字符时,会回溯目标串str的索引,并且每次都是回溯到上一次的后面一个位置。假如目标串的长度为m、子串的长度为n,最坏情况下,每次都是最后一个字符没有找到相对应的字符,那么它每次子串扫描的长度就是n个,每次回溯到目标串都需要花费扫描n个字符串的时间,那么m个字符中,它需要回溯的此时就是m*n ,它的时间复杂度就是:O(m*n),这是相当浪费时间的。
KMP算法
- 在Brute-Force算法中,我们了解到它所对应的时间复杂度为O(m*n),每次扫描不相等的字符,都会导致主串回溯到上一次扫描的位置。但是对于KMP而言,它并不是每次扫描到不相等的元素就会导致主串回溯到上一次扫描的位置,而是会去KMP算法中很重要的一个信息:前缀表中查询扫描失败字符的前面一个位置在该表中对应的值,从这里重新开始扫描。
在KMP算法中最重要的内容之一:前缀表
-
在了解前缀表之前,我们需要先了解几个概念:前缀、后缀、最长相等前后缀
- 前缀: 包含首字符,但是不包含尾字符的子串
- 后缀: 包含尾字符,但是不包含首字符的字串
- 相等前后缀: 在前缀和后缀中,相等的子串
-
以模式串:
aabaaf
为例,它的前缀和后缀分别有:a
只有一个字符,它没有前缀也没有后缀,所以也没有相等前后缀,它的最长相等前后缀长度=0aa
它的前缀有:{a}
,后缀有:{a}
,它的相等前后缀有:{a}
,它的最长相等前后缀长度=1{aab}
它的前缀有:{a、aa}
,后缀有:{ab、b}
,它没有相等前后缀,它的最长相等前后缀长度=0{aaba}
它的前缀有:{a、aa、aab}
,后缀有:{aba、ba、a}
,它的相等前后缀有:{a}
,它的最长相等前后缀长度=1{aabaa}
它的前缀有:{a、aa、aab、aaba}
,后缀有:{abaa、baa、aa、a}
,它的相等前后缀有:{a、aa}
,它的最长相等前后缀长度=2{aabaaf}
它的前缀有:{a、aa、aab、aaba、aabaa}
,它的后缀有:{abaaf、baaf、aaf、af、f}
,它没有相等前后缀,它的最长相等前后缀长度=0
-
前缀表就是模式串中各个字符它们的最长相等前后缀长度组成的一个表格,所以模式串
aabaaf
所对应的前缀表就是:
- 在这个表中,上面一行表示的是数组的索引
- 下面一行表示的是它对应的前缀表中的值,也就是各个最长相等前后缀的长度
-
以目标串
str="aabaabaaf"
和模式串substr="aabaaf"
为例,它们对应的索引分别为:i
和j
。在匹配的过程中,分为两个过程:- 当
str[i]
和substr[j]
的值不同时: 模式串会去寻找在未匹配到相等字符前面一位在前缀表中对应的值,并将j
改变到它在next数组也就是前缀表
中的前一位的值,即j=next[j-1]
,此时i
不变,继续比较str[i]
和substr[j]
的位置,如果它们不相等,则继续改变j
的位置。 - 当
str[i]
和substr[j]
的值相同时: 改变i
和j
的位置,继续往后面匹配。 - 举例: 模式串从字符第一个字符
a
开始,它与str
中的字符都是相对应的,所以i与j
是同步改变的。但是 当模式串匹配到f
的位置时,它与目标串没有相对应的字符,所以这时候需要改变j
的值,将它回溯到前缀表中j-1
的位置,也就是next[5-1]
所对应的值,即j=next[j-1]=2
。与此同时i
不会有所变化,str[i=5]='b',substr[j=2]=b
,所以进入到相同情况下,继续匹配。直到匹配到字符串str
的末尾,判断j >= substr.length
是否成立,如果成立,则说明j
遍历substr
完成,并且它与str
都有对应的字符,所以此时需要返回substr
字符首字符在目标串str
中第一次出现的位置,也就是i-(j-1)
;否则返回-1
- 当
-
代码:
package com.cdc.algorithm.common;
import java.util.Arrays;
/**
* @author cdc
* @email c925638766@163.com
* @date 2022/4/15 20:29
*/
public class StringSearchDemo {
public static void main(String[] args) {
String str = "aabaabaaf";
String substr = "aabaaf";
int[] next = generateNext(substr);
System.out.println("aabaaf对应的next数组为:" + Arrays.toString(next));
System.out.println("aabaaf在字符串aabaabaaf中的索引位置为:" + kmpSearch(str, substr, next));
str = "aabaacaabaabaaf";
System.out.println("\n---------------字符aabaacaabaabaaf-------------------------");
System.out.println("aabaaf对应的next数组为:" + Arrays.toString(next));
System.out.println("aabaaf在字符串aabaacaabaabaaf中的索引位置为:" + kmpSearch(str, substr, next));
str = "aabaacaabaabaac";
System.out.println("\n---------------字符aabaacaabaabaac-------------------------");
System.out.println("aabaaf对应的next数组为:" + Arrays.toString(next));
System.out.println("aabaaf在字符串aabaacaabaabaac中的索引位置为:" + kmpSearch(str, substr, next));
}
/**
* 根据所传入的模式串生成其对应的前缀表
*
* @param substr 模式串
* @return 生成好的前缀表
*/
public static int[] generateNext(String substr) {
int[] next = new int[substr.length()];
//在next数组中,0对应着首字符的最长前后缀长度,首字符没有前缀和后缀,所以直接令它=0
next[0] = 0;
char[] chars = substr.toCharArray();
//i表示的是后缀的末尾位置,j表示的是前缀的末尾位置
for (int i = 1, j = 0; i < chars.length; i++) {
//前后缀不相同的情况,需要回退j的位置,将j回退到next数组的上一位
//j>0避免数组越界,并且可能不止需要回退一次,所以需要用whileu循环
while (j > 0 && chars[i] != chars[j]) {
//回退j的值
j = next[j - 1];
}
//如果前缀和后缀的值相等,需要将j后移一位
//以字符"aabaaf"为例,初值i=1,j=0,此时chars[i]=a,chars[j]=a,则此时我们就需要将j后移一位
//将j++后,我们需要更新next数组的值,由于i是循环变量会在for循环中更改,所以我们不用管
if (chars[i] == chars[j]) {
j++;
}
next[i] = j;
}
return next;
}
/**
* kmp算法匹配字符串中子串的位置
* 为了方便,此处直接采用String中的charAt()方法来判断索引位置对应的字符
* @param str 目标串
* @param substr 子串
* @param next 模式串对应的前缀表
* @return 返回子串在目标串的位置,不存在则返回-1
*/
public static int kmpSearch(String str, String substr, int[] next) {
int index = -1;
for (int i = 0, j = 0; i < str.length(); i++) {
//当str[i] != substr[j]情况成立时,将j回溯到前缀表中它前面的一给位置,即j=next[j-1]
//j > 0为了避免数组越界
while (j > 0 && str.charAt(i) != substr.charAt(j)) {
j = next[j - 1];
}
//当目标串str中i的位置和模式串substr中j的位置字符相同时,移动模式串j的位置,进行下一次比较
if (str.charAt(i) == substr.charAt(j)) {
j++;
}
//如果j移动了超过模式串的位置,则说明直到最后一个字符,目标串和模式串的字符都是一致的
// 否则j++得到的值不会比子串的长度大
if (j >= substr.length()) {
System.out.println("j="+j+"substr.length="+substr.length());
index = i - (j - 1);
break;
}
}
return index;
}
}
- 运行结果:
"C:\Program Files\Java\jdk1.8.0_202\bin\java.exe" "-javaagent:F:\JetBrains\IntelliJ IDEA 2020.2.4\lib\idea_rt.jar=56916:F:\JetBrains\IntelliJ IDEA 2020.2.4\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_202\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar;F:\Java自学\数据结构\Java_DataStructure\out\production\Java_DataStructure;F:\Maven_jars\org\projectlombok\lombok\1.18.16\lombok-1.18.16.jar" com.cdc.algorithm.common.StringSearchDemo
aabaaf对应的next数组为:[0, 1, 0, 1, 2, 0]
aabaaf在字符串aabaabaaf中的索引位置为:3
---------------字符aabaacaabaabaaf-------------------------
aabaaf对应的next数组为:[0, 1, 0, 1, 2, 0]
aabaaf在字符串aabaacaabaabaaf中的索引位置为:9
---------------字符aabaacaabaabaac-------------------------
aabaaf对应的next数组为:[0, 1, 0, 1, 2, 0]
aabaaf在字符串aabaacaabaabaac中的索引位置为:-1
Process finished with exit code 0