字符串查找算法:暴力、KMP、DFA

1. 暴力查找

给出主串txt, 模式串pat, 查找pat在txt中出现的位置,不存在则返回-1

package algorithm;

public class SubstrSearchDemo {

	/*
	 * 在txt主串中查找子串pat第一次出现的位置
	 */
	public static int search(String pat,String txt) {
		int M=pat.length();
		int N=txt.length();
		//pat在txt的起始位置为[0,N-M) pat本身长度为M 
		//在位置N-M-1处往后找还不能匹配  后面也就不可能匹配了
		for(int i=0;i<N-M;i++)
		{
			int j;
			for(j=0;j<M;j++)//j扫描模式串pat 
			{
				if(pat.charAt(j)!=txt.charAt(i+j))//不等直接跳出
					break;
			}
			if(j==M)//j==M说明[0,M-1]都匹配了  匹配成功
				return i;
		}
		return -1;//未找到返回-1
	}
	public static void main(String[] args) {
		String txt="helloworld";
		String pat="llo";
		System.out.println(search(pat, txt));//2
	}
}

时间复杂度:O(MN)

暴力法的一种优化:查找失败时,i不需要回退到0,回退到i-j的位置即可, j是已经匹配成功的序列的长度
证明:反证法:假设区间[0,i-j-1]存在一个start,以该start为字符序列的开始位置,可以匹配成功,则不会到达当前j的位置,在j之前就已经匹配成功了,但是现在到达了j位置,所以说明以[0,i-j-1]内的任何位置为起点都不能匹配成功,证毕

package algorithm;

public class SubstrSearchDemo {

	/*
	 * 在txt主串中查找子串pat第一次出现的位置
	 */
	public static int search(String pat,String txt) {
		int M=pat.length();
		int N=txt.length();
		int i,j;
		//i指向txt中匹配过的字符序列的末端
		for(i=0,j=0;i<N&&j<M;i++)
		{
			if(pat.charAt(j)==txt.charAt(i))
				j++;
			else {
				i-=j;//i不用回退到0
				j=0;//j从pat的首字符开始
			}
		}
		if(j==M)
			return i-M;
		return -1;//未找到返回-1
	}
	public static void main(String[] args) {
		String txt="helloworld";
		String pat="llo";
		System.out.println(search(pat, txt));//2
	}
}

2. indexOf方法

indexOf方法是String类中的方法,查找某个子串第一次在主串中的出现位置,它的代码如下:

static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }

        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);

        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            //先找到主串中找到和子串第一个字符匹配的位置
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }
					 
            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
            //从匹配到子串的首字符的下一个位置继续匹配
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {//匹配成功
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }

上述代码的思路和暴力法本质一样,我们自己写的暴力法是直接用一个循环,循环终止的条件是遇到不匹配的字符; indexOf方法是先找到一个相等的字符,如何从这个相等的字符开始一个循环,循环终止的条件是遇到不匹配的字符;


3. KMP算法

一个例子:

假设字母表中只有两个字符,查找的模式字符串pat为 B A A A A A A A A A 。现在,假设已经匹配了模式中的 5 个字符,第 6 个字符匹配失败。当发现不匹配的字符时,可以知道文本txt中的前 6 个字符肯定是 B A A A A B(前 5 个匹配,第 6 个失败),文本指针现在指向的是末尾的字符 B

txt: B A A A A B A A A A
pat: B A A A A A A A A A
这里不需要回退文本指针 i,因为正文中的前 4 个字符都是 A,均与模式的第一个字符B不匹配。另外,i 当前指向的字符 B 和模式的第一个字符相匹配,所以可以直接将 i 加 1,以比较文本中的下一个字符和模式中的第二个字符
(i变成i+1)
在这里插入图片描述
在 KMP 子字符串查找算法中,不会回退文本指针 i,而是使用一个数组 dfa[][] 来记录匹配
失败时模式指针 j 应该回退多远

那么j回退到哪呢? j回退到dfa[txt.charAt(i)][j]
解释dfa[txt.charAt(i)][j]数组的意思:对于每个文本串中 的字符c,在比较了 c 和 pat.charAt(j) 之后,dfa[c][j]表示的是应该和下个文本字符比较的模式字符的位置

当 i 和 j 所指向的字符匹配失败时(从文本的 i-j+1 处开始检查模式的匹配情况),模式可能匹配的下一个位置 应 该 从i-dfa[txt.charAt(i)][j]处开始。按照算法,从该位置开始的dfa[txt.charAt(i)][j]个字符和模式的前 dfa[txt.charAt(i)][j]个字符应该相同,因此无需回退指针 i,只需要将 j 设为dfa[txt.charAt(i)][j]并将 i 加 1 即可,这正是当 i 和 j 所指向的字符匹配时的行为

现在的问题:如何求dfa数组?

看下面一幅图:
在这里插入图片描述

解释:
dfa[c][j]的含义和上面介绍的一样: c=txt.charAt(i) j是当前pat串中的指针位置,当比较c和pat[j]之后,j下一个位置就是j=dfa[c][j]

图中一共有6个状态:

  • 状态0表示匹配刚刚开始,此时可以理解成是一个空串
  • 在状态0的基础上,可能会遇到3种字符(假设txt中只有3种),遇到B C的话直接还是回到起点0,因为pat的第一个字符是A,如果遇到A说明pat的第一个字符匹配成功,成功进入下一状态1,1可以理解成字符串”A“
  • 在状态1的基础上,可能会遇到3种字符(假设txt中只有3种),遇到C的话直接还是回到起点0,因为pat的第一个字符是A,如果遇到A说明pat的第一个字符匹配成功,第2个字符匹配失败,停留在状态1,如果遇到B成功进入下一状态2,1可以理解成字符串”AB“
  • 其余状态依次类推

代码实现:

package algorithm;

public class SubstrSearchDemo {

	String pat;
	String txt;
	int [][]dfa;
	int R=256;//先设置字符种类有256种
	
	public void initDFA() {
		int M=pat.length();
		dfa=new int[R][M];
		dfa[pat.charAt(0)][0] = 1;//设置dfa的第一列值
		for(int X=0,j=1;j<M;j++)
		{
			System.out.println("当前状态:"+j+"  重启状态:"+X+" 当前pat字符:"+pat.charAt(j));
			for(int c=0;c<R;c++)
			{
				//将重置状态的情况先复制到当前的状态  后面改变一种匹配成功的情况
				dfa[c][j]=dfa[c][X];// 复制匹配失败情况下的值 先假设256种字符都匹配失败(虽然不可能)
			}
			
			System.out.println("设置匹配成功之前:");
			for(int h=0;h<pat.length();h++)
				System.out.print("\t"+h);
			System.out.println();
			for(int R='A';R<='C';R++)
			{
				System.out.print((char)(R)+"\t");
				for(int k=0;k<pat.length();k++)
					System.out.print(dfa[R][k]+"\t");
				System.out.println();
			}
			
			//pat.charAt(j)==txt.chatAt(i) 匹配成功
			//dfa的定义是dfa[txt.charAt(i)][j] 
			//状态j下遇到字符c匹配成功进入下一个状态j+1
			dfa[pat.charAt(j)][j]=j+1;// 设置匹配成功情况下的值 在之前的假设所有失败的情况下选择匹配成功的
			System.out.println("设置匹配成功之后:");
			for(int h=0;h<pat.length();h++)
				System.out.print("\t"+h);
			System.out.println();
			for(int R='A';R<='C';R++)
			{
				System.out.print((char)(R)+"\t");
				for(int k=0;k<pat.length();k++)
					System.out.print(dfa[R][k]+"\t");
				System.out.println();
			}
			//X可能是原来的状态 也可能往前进一个状态 也可能往后退一个状态
			X=dfa[pat.charAt(j)][X];// 更新状态
			System.out.println("----------------------------------------");
		}
	}
	/*
	 * 在txt主串中查找子串pat第一次出现的位置
	 */
	public  int search() {
		int M=pat.length();
		int N=txt.length();
		
		int i,j;
		for(i=0,j=0;i<N&&j<M;i++) {
			j=dfa[txt.charAt(i)][j];
		}
		if(j==M)
			return i-M;
		return -1;//未找到返回-1
	}
	public static void main(String[] args) {
		SubstrSearchDemo obj=new SubstrSearchDemo();
		obj.txt="AABRABABACBRAACAADABRA";
		obj.pat="ABABAC";
		obj.initDFA();
		
		System.out.println();
		System.out.println(obj.search());//12
		System.out.println(obj.txt.indexOf(obj.pat));//使用indexOf方法验证
		
	}
}


时间复杂度:O(M+N)

上面代码中,以图中的例子为例:
第一次循环:第一个字符是’A‘: dfa[A][0]=0+1 其他的两个字符B C对应的dfa[B][0]=0 dfa[B][0]=0
第二次循环:第二个字符是’B’: dfa[B][1]=1+1 其他的两个字符A C对应的dfa[A][1]=1 dfa[C][1]=0

在这里插入图片描述
在这里插入图片描述

X的状态什么时候会更新?
假设当前遇到的pat字符是c,并且c在之前的最近的一个状态j时已经匹配成功过一次,则X的下一个状态为j+1
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
字符串匹配算法是一种用来查找一个字符串(即目标串)在另一个字符串(即模式串)中的出现位置的算法。其中,KMP算法是一种比较常用的字符串匹配算法KMP算法的核心思想是通过利用模式串中已经匹配过的信息,来尽量减少目标串和模式串的比较次数,从而提高匹配效率。它利用一个最长公共前缀和最长公共后缀数组,记录模式串中已经匹配成功的前缀和后缀的长度。通过根据这些信息来移动模式串的位置,避免不必要的比较。 而字符串哈希算法是一种将字符串映射为一个较短的固定长度的数值的算法。通过对字符串的每个字符进行一系列运算,如求幂、取模等,最终得到一个哈希值。这个哈希值可以代表该字符串的特征,不同字符串的哈希值一般不会相同。 字符串哈希算法的主要作用是将字符串转化为一个定长的数字,方便在数据结构中进行比较和存储。在字符串匹配中,使用哈希算法可以将目标串和模式串转换为哈希值,然后比较哈希值是否相等来判断是否匹配。由于比较哈希值的时间复杂度较低,使用字符串哈希算法可以提高匹配效率。 总的来说,字符串匹配算法字符串哈希算法都是用来处理字符串匹配的问题。KMP算法通过利用已知信息来减少比较次数,提高匹配效率;而字符串哈希算法则是将字符串转化为哈希值,便于进行比较和存储。两者都在一定程度上提高了字符串匹配的效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodePanda@GPF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值