0
点赞
收藏
分享

微信扫一扫

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

想溜了的蜗牛 2022-03-30 阅读 30

文章目录

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方法

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算法

一个例子:

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]表示的是应该和下个文本字符比较的模式字符的位置

现在的问题:如何求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 条评论