数据结构与算法——字符串匹配
简单测了下,应该没啥问题,思路肯定没问题,代码如果有问题欢迎指正
1、概念
- 模式串:你需要匹配的字符串
- 主串:作为被匹配范围的主体字符串
2、BF算法
如果【模式串】长度为n,那就从【主串】的第一个字符开始,比较【长度为n的子串】和【模式串】是否匹配,不匹配就再从第二个字符开始,直到匹配到模式串
/**
* BF算法
*
* @param main 主串
* @param pattern 模式串
* @return 是否匹配
*/
public static boolean bf(String main, String pattern) {
final char[] mainChars = main.toCharArray();
final char[] patternChars = pattern.toCharArray();
//主串长度还不及模式串,肯定匹配不到
if (mainChars.length < patternChars.length) {
return false;
}
loop:
for (int i = 0; i < mainChars.length - patternChars.length; i++) {
//循环对比模式串和主串的每个子串
for (int j = 0; j < patternChars.length; j++) {
if (mainChars[i + j] != patternChars[j]) {
continue loop;
}
}
return true;
}
return false;
}
3、RK算法
在BF算法的基础上,将子串做hash运算,和模式串的hash值做比较
使用上一个字串的hash值计算当前子串的hash值,减少bf算法循环对比子串的循环消耗
/**
* RK算法
* @param main 主串
* @param pattern 模式串
* @return 是否匹配
*/
public static boolean rk(String main, String pattern){
final char[] mainChars = main.toCharArray();
final char[] patternChars = pattern.toCharArray();
final int patternLength = patternChars.length;
//主串长度还不及模式串,肯定匹配不到
if (mainChars.length < patternLength) {
return false;
}
final int patternHash = rkHashCode(patternChars,0, patternLength,-1);
int lastMainHash = rkHashCode(mainChars,0, patternLength,-1);
if(patternHash == lastMainHash){
return true;
}
for (int i = 1; i <= mainChars.length - patternLength; i++) {
final int thisHashCode = rkHashCode(mainChars, i, patternLength, lastMainHash);
if(patternHash == (lastMainHash = thisHashCode)){
return true;
}
}
return false;
}
/**
* 计算子串的hash值
* @param chars 主串
* @param start 待计算子串的起始索引
* @param length 子串长度
* @param lastHashCode 上次计算的hash值,-1为第一次计算hash值
* @return 返回子串的hash值
*/
private static int rkHashCode(char[] chars,int start,int length,int lastHashCode){
if(lastHashCode != -1){
//S(n,n+m) = hash(n)*31^(m-1)+hash(n+1)*31^(m-2)+...+hash(m+n)
//S(n+1,n+m+1)=hash(n+1)*31^(m-1)+hash(n+2)*31^(m-2)+...+hash(m+n+1)
// =(S(n,n+m)-hash(n)*31^(m-1))*31+hash(m+n+1)
//利用上一次的hash值和关系,计算这次的hash值,减少计算消耗
//如果你能确定主串和模式串的取值范围,可以设计特定的hash算法来提前用hash表缓存好hash值
return (lastHashCode-Character.hashCode(chars[start-1])*((int) Math.pow(31,length-1)))*31+Character.hashCode(chars[start+length-1]);
}
int hash =0;
for (int i = start; i < start+length; i++) {
hash =hash*31 + Character.hashCode(chars[i]);
}
return hash;
}
4、BM算法
非常高效的字符串搜索算法,主要有三步:
- 坏字符规则:模式串与主串的当前子串从后往前做对比,主串的当前子串中不匹配的第一个字符(从后往前)称为坏字符;在模式串中定位坏字符并进行移位对齐的过程,就是坏字符规则
- 好后缀规则:模式串与主串的当前子串从后往前做对比,主串的当前子串中与模式串能够匹配的最长后缀子串称为好后缀;在模式串中定位好后缀并进行移位对齐的过程,就是好后缀规则。当好后缀规则也没起作用,就找出最长的好后缀与模式串可匹配的前缀子串,然后进行移位对齐
- 在坏字符规则和好后缀规则中选取最远的移位距离,是最高效且稳定的
/**
* BM算法
*
* @param main 主串
* @param pattern 模式串
* @return 是否匹配
*/
public static boolean bm(String main, String pattern) {
final char[] mainChars = main.toCharArray();
final char[] patternChars = pattern.toCharArray();
//构建模式串每个字符的索引,索引为字符的hash值,值为字符在模式串中的位置
//在与主串匹配的时候,如果在主串出现坏字符,可以直接使用坏字符的hash,在模式串字符索引中找到坏字符在模式串中的位置,比循环模式串查找要快些
final MyArrayList<Integer> patternCharIndexList = new MyArrayList<>();
//构建模式串每个后缀子串的索引,索引为后缀子串的长度,值为匹配了后缀子串的子串在模式串中的位置
//在与主串匹配的时候,如果在主串中出现好后缀,可以直接使用好后缀的长度,在模式串后缀子串索引中找到好后缀在模式串中的位置
final MyArrayList<Integer> patternSuffixIndexList = new MyArrayList<>();
//记录模式串的每个后缀子串是否也是他的前缀子串,索引为后缀子串的长度,值为后缀子串是否与自己的前缀子串匹配
final MyArrayList<Boolean> patternPrefixMatchIndexList = new MyArrayList<>();
init(patternChars, patternCharIndexList, patternSuffixIndexList, patternPrefixMatchIndexList);
int start = 0;
while (start <= mainChars.length - patternChars.length) {
//获取坏字符在子串的相对位置
final int badCharIndexInMain = getBadCharacterIndex(mainChars, start, patternChars);
if (badCharIndexInMain == 0) {
//主串的当前子串与模式串匹配
return true;
}
//首先使用坏字符规则进行匹配
final int badCharacterRuleMoving = badCharacterRule(mainChars[start + badCharIndexInMain], badCharIndexInMain, patternChars, patternCharIndexList);
if(badCharIndexInMain == patternChars.length-1){
start+=badCharacterRuleMoving;
continue;
}
//好后缀在子串的相对起始位置,在坏字符的后面
int goodSuffixOffset = badCharIndexInMain + 1;
final int goodSuffixRuleMoving = goodSuffixRule(goodSuffixOffset, patternChars, patternSuffixIndexList, patternPrefixMatchIndexList);
start += Math.max(badCharacterRuleMoving, goodSuffixRuleMoving);
}
return false;
}
/**
* 坏字符规则
*
* @param badChar 坏字符
* @param badCharOffsetInMain 坏字符于子串的相对位置
* @param patternChars 模式串
* @param patternCharIndexList 模式串索引列表
* @return 使用坏字符规则匹配后,模式串需要移动的距离
*/
private static int badCharacterRule(char badChar, int badCharOffsetInMain, char[] patternChars, MyArrayList<Integer> patternCharIndexList) {
if (Character.hashCode(badChar) > patternCharIndexList.size()) {
return badCharOffsetInMain + 1;
}
final Integer badCharIndexInPattern = patternCharIndexList.get(Character.hashCode(badChar));
//为-1说明模式串中没有坏字符,直接把模式串的开头对齐到坏字符后面的位置
//不为-1说明模式串中有坏字符,需要移动的距离就是坏字符在模式串中的位置到模式串尾部的距离
return badCharIndexInPattern == -1 ? badCharOffsetInMain + 1 : (patternChars.length - badCharIndexInPattern - 1);
}
/**
* 好后缀规则
*
* @param goodSuffixOffset 好后缀的起始位置,距离子串的偏移量
* @param patternChars 模式串
* @param patternSuffixIndexList 模式串后缀子串索引
* @param patternPrefixMatchIndexList 模式串的每个后缀子串时候匹配前缀子串
* @return
*/
private static int goodSuffixRule(int goodSuffixOffset, char[] patternChars, MyArrayList<Integer> patternSuffixIndexList, MyArrayList<Boolean> patternPrefixMatchIndexList) {
final int goodSuffixLength = patternChars.length - goodSuffixOffset;
//根据当前子串的好后缀的长度,从预先构建的模式串的后缀子串索引中,找到模式串中与当前子串匹配的非后缀子串
final Integer goodSuffixIndexInPattern = patternSuffixIndexList.get(goodSuffixLength);
if (goodSuffixIndexInPattern != null) {
//模式串中有与好后缀匹配的非后缀子串,直接返回模式串的非后缀子串的起始位置
return (goodSuffixOffset - goodSuffixIndexInPattern);
}
//模式串中没有与好后缀匹配的非后缀子串,就从模式串的前缀子串中,找到能够与好后缀重合最多的前缀子串
for (int i = goodSuffixLength; i >= 0; i--) {
if (Boolean.TRUE.equals(patternPrefixMatchIndexList.get(i))) {
return i;
}
}
//都没匹配,就直接把模式串挪到好后缀的后面
return patternChars.length;
}
/**
* 初始化模式串的各种索引
* @param patternChars 模式串
* @param patternCharIndexList 模式串字符索引,用于坏字符快速定位
* @param patternSuffixIndexList 模式串后缀子串索引,用于好后缀快速定位
* @param patternPrefixMatchIndexList 模式串后缀子串与前缀子串是否匹配,用于寻找好后缀的最长前缀子串
*/
private static void init(char[] patternChars, MyArrayList<Integer> patternCharIndexList, MyArrayList<Integer> patternSuffixIndexList, MyArrayList<Boolean> patternPrefixMatchIndexList) {
int patternCharsMaxHashCode = -1;
for (int i = 0; i < patternChars.length; i++) {
patternCharsMaxHashCode = Math.max(Character.hashCode(patternChars[i]), patternCharsMaxHashCode);
patternSuffixIndexList.add(i, -1);
patternPrefixMatchIndexList.add(i,false);
}
for (int i = 0; i <= patternCharsMaxHashCode; i++) {
patternCharIndexList.add(-1);
}
for (int i = patternChars.length - 1; i >= 0; i--) {
final int patternCharHashCode = Character.hashCode(patternChars[i]);
if (patternCharIndexList.get(patternCharHashCode) == -1) {
//模式串字符索引列表中,多个相同字符只记录最后一个位置
patternCharIndexList.set(patternCharHashCode, i);
}
//子串长度,也是匹配子串和后缀子串的次数
int length = patternChars.length - i;
//从后向前截取等长子串和后缀子串匹配
loop:
for (int j = i - 1; j >= 0; j--) {
//比较子串和后缀子串是否相等
for (int k = length - 1; k >= 0; k--) {
if (patternChars[j + k] != patternChars[i + k]) {
//不相等就将子串起始位置向前挪一个,再截取等长子串比较
continue loop;
}
}
//找到了匹配的子串
patternSuffixIndexList.set(length, j);
patternPrefixMatchIndexList.set(length, j == 0);
break;
}
}
}
/**
* 寻找坏字符在主串中的相对位置
*
* @param mainChars 主串
* @param start 主串的子串的起始位置
* @param patternChars 模式串
* @return 坏字符距离子串起始位置的偏移量
*/
private static int getBadCharacterIndex(char[] mainChars, int start, char[] patternChars) {
for (int i = patternChars.length - 1; i >= 0; i--) {
if (mainChars[start + i] != patternChars[i]) {
return i;
}
}
//说明主串中没有坏字符,和模式串全部匹配,就不移动了
return 0;
}
5、KMP算法
和bm算法类似,主要使用好前缀规则
- 好前缀规则:模式串与主串的当前子串从头匹配,遇到坏字符之前匹配上的字符集叫好前缀。由于好前缀已经比对过,所以我们只要将模式串的最长可匹配前缀子串和当前子串中的最长可匹配后缀子串直接对齐,再比较从哪个坏字符开始的后面的字符,就可以跳过很多重复比对的工作
/**
* KMP算法
*
* @param main 主串
* @param pattern 模式串
* @return 是否匹配
*/
public static boolean kmp(String main, String pattern) {
final char[] mainChars = main.toCharArray();
final char[] patternChars = pattern.toCharArray();
final MyArrayList<Integer> next = new MyArrayList<>();
init(patternChars,next);
int start = 0;
int offset = 0;
while (start <= mainChars.length - patternChars.length) {
final int length = goodPrefixRule(mainChars, start, offset, patternChars, next);
if (offset + length == patternChars.length) {
//好前缀末尾偏移量+后面字符匹配的长度=跟模式串完全匹配
return true;
}
//找到当前长度的好前缀,对应的最长可匹配前缀子串尾部的位置
final Integer prefixTailIndex = next.get(length);
//模式串中没有最长可匹配前缀子串,就直接挪到上一个最长可匹配前缀子串的后面,也就是将模式串对齐坏字符
//模式串中有最长可匹配前缀子串,就将匹配的两个子串对齐
start += prefixTailIndex == -1 ? 1 : (length - prefixTailIndex);
System.out.println(prefixTailIndex == -1 ? 1 : (length - prefixTailIndex));
offset = length;
}
return false;
}
/**
* 好前缀规则
*
* @param mainChars 主串
* @param start 主串中当前子串的起始位置
* @param length 是模式串最长可匹配前缀子串长度
* @param patternChars 模式串
* @param next 模式串的最长可匹配前缀子串尾部的位置,也是最长可匹配前缀子串的长度-1
* @return 获取好前缀的长度
*/
private static int goodPrefixRule(char[] mainChars, int start, int length, char[] patternChars, MyArrayList<Integer> next) {
int i = 0;
//比较模式串与主串的当前子串对齐后,后面的子串
for (; i < patternChars.length - length; i++) {
if (mainChars[start + length+i] != patternChars[length+i]) {
break;
}
}
//如果后面的字符完全匹配,说明找到了完整匹配模式串的子串
//如果后面的字符串一开始就不匹配,就对当前最长可匹配前缀子串,再求最长可匹配前缀子串
return i;
}
/**
* 初始化模式串的最长可匹配前缀子串的结尾的位置
*
* @param patternChars 模式串
* @param next 模式串的最长可匹配前缀子串的结尾的位置
*/
private static void init(char[] patternChars, MyArrayList<Integer> next) {
for (int i=0;i<patternChars.length;i++){
next.add(-1);
}
loop:
for (int i = 1; i < patternChars.length; i++) {
//寻找上一个长度的最长可匹配前缀子串的结尾下标
Integer last = next.get(i-1);
if (last == -1) {
//为空说明上一个长度的后缀子串没有可匹配的前缀子串
if (patternChars[0] == patternChars[i]) {
next.set(i, 0);
}
continue;
}
//循环寻找最长可匹配前缀子串
for (int j = i-1; j >= 0; j--) {
last = next.get(j);
if(last == -1){
continue ;
}
//不为空,就根据上一个长度的最长可匹配前缀子串的结尾下标,找到与他相连的下一个字符
//将上一长度好前缀的最长可匹配前缀子串的下个字符,和上一长度好前缀的下个字符(也就是当前好前缀的末尾字符)做对比
if (patternChars[last + 1] == patternChars[i]) {
//上个长度的最长可匹配前缀子串的下一个字符,和上个长度的好前缀的下一个字符(也就是当前好前缀的末尾字符)相同
//之前的最长可匹配前缀子串是匹配的,下一个字符也匹配,那加上下一个字符,就是当前的最长可匹配前缀子串
next.set(i, last + 1);
continue loop;
}
}
}
}