字符串的匹配算法【学习算法】

前言

2023-8-6 12:06:42

以下内容源自《【学习算法】》
仅供学习交流使用

版权

禁止其他平台发布时删除以下此话
本文首次发布于CSDN平台
作者是CSDN@日星月云
博客主页是https://blog.csdn.net/qq_51625007
禁止其他平台发布时删除以上此话

推荐

28. 找出字符串中第一个匹配项的下标

字符串的模式匹配


知识内容来源于
第四章 串(数据结构与算法)


子串的定位操作是找子串在主串中从第 pos 个字符后首次出现的位置,又被称为“串的模式匹配"或“串匹配”,此运算的应用非常广泛。例如,在文本编辑程序中,经常要查找某一特定单词在文本中出现的位置。显然,解决此问题的有效算法能极大地提高文本编辑程序的响应性能在串匹配中,一般将主串S称为“目标串”,子串T称为“模式串”。

模式匹配的算法很多。本章仅讨论 BF模式匹配和KMP模式匹配这两种串匹配算法。

BF模式匹配算法

[BE算法思想]
Brute-Force 算法又称“蛮力匹配”算法(简称BP算法),从主串S的第pos个字符开始,和模式串了的第一个字符进行比较,若相等,则继续逐个比后续字符;否则回溯到主串的第 pos+1个字符开始再重新和模式串T进比较。以此类推,直至模式串了中的每一个字符依次和主串中的一个连的字符序列全部相等,则称模式匹配成功,此时返回模式串了的第一个字在主串S中的位置;否则主串中没有和模式串相等的字符序列,称模式匹配不成功。

[BF算法描述]
从主串S的第pos个字符开始的子串与模式串T比较的策略是从前到后依次进行比较。因此在主串中设置指示器i表示主串S中当前比较的字符;在模式串T中设置指示器j表示模式串T中当前比较的字符。

如图4-5所示,给出了一个匹配过程的例子,其中方框阴影对应的字符为主串S和模式串T比较时不相等的失配字符(假设 pos=1)。

从主串S中第pos个字符起和模式串T的第一个字符比较,若相等则继续逐个比较后续字符,此时i++;j++;否则从主串的下一个字符(i-j+2)和模式串的第一个字符(j=1)比较,分析详图4-6(a)。

当匹配成功时,返回模式串T中第一个字符相对于在主串的位置(i-T.en);否则返回0,分析详见图4-6(b),其中m是模式串的长度T.len。

int Index(SString S,int pos,SString T)
	int i=pos,j=1;//主串从第pos开始,模式串从头开始
	while (i<=S.len&&j<=T.len){
    
    
		if(S.ch[i]==T.ch[j]){
    
    //当对应字符相等时,比较后续字符
			i++;
			j++;
		}
		else{
    
    				//当对应字符不等时
			i=i-j+2;			//主串回溯到j-j+2的位置重新比较
			j=1;				//模式串从头开始重新比较
		}
	if(j>T.len)	return i-T.len;	//匹配成功时,返回匹配起始位置
	else return 0;				//匹配失败时,返回0

[BF算法分析]
BF算法的思想比较简单,但当在最坏情况下时,算法的时间复杂度为0(n×m),其中n和m分别是主串和模式的长度。这个算法的主要时间耗费在失配后的比较位置有回溯,因而造成了比较次数过多。为降低时间复杂度可采用无回溯的算法。

KMP模式匹配算法

[KMP算法思想]
Knuth-Morris-Pratt算法(简称KMP),是由DEKnuthJ.HMorris和V.RPratt 共同提出的一个改进算法。KMP算法是模式匹配中的经典算法,和BF算法相比,KMP算法的不同点是消除了 BF算法中主串S指针i回溯的情况。改进后算法的时间复杂度为0(n+m)。

[KMP算法描述]
KMP算法中,每当一趟匹配过程中出现字符比较不等时,主串S中的指针不需回溯,而是利用已经得到的“部分匹配”结果将模式串向右“滑动”尽可能远的一段距离后,继续进行比较。

回顾图4-5的匹配过程示例,在第三趟匹配中,当i=7、j=5字符比较不等时,又从i=4.j=1重新开始比较。然而,经过仔细观察可发现,在i=4和j=1,i=5和j=1以及i=6和j=1这三次比较都是不必进行的。因为从第三趟部分匹配的结果就可得出,主串中第4、5、6个字符必然和模式串中的第2.3.4个字符相等,即都是’bca。因为模式串中的第一个字符是’a’,因此它无须再和这三个字符进行比较,而仅需将模式串向右滑动三个字符的位置继续进行i=7.j=2时的字符比较即可。同理,在第一趟的匹配中出现字符不等时,仅需将模式向右移动两个字符的位置进行i=3、j=1时的字符比较。因此,在整个匹配的过程中,指针没有回溯,如图4-7所示。

在这里插入图片描述

一般情况下,假设主串为’S1S2…Sn’,模式串为’T1T2…Tn’,从上例的分析可知,为了实现KMP算法,需要解决以下问题,当匹配过程中产生“失配”(即Si≠Ti)时,模式串“向右滑动”可滑动的距离有名远,也就是说,当主串中字符Si,与模式串中字符Tj"失配”时,主串中字符Si( i 指针不回溯)应与模式串中哪个字符再进行比较?

假设此时主串中字符Si应与模式中字符Tk(k<j)继续进行比较,则主串S和模式串T满 如下关系。
S=S1S2…Si-j+1Si-j+2…Si-k+1…Si-1Si …Sn
T=            T1    T2    …Tj-k+1…Tj-k+2
T=                               T1     …Tk-1

可以看出,若模式串中存在’T1T2…Tk-1’=‘Tj-k+1Tj-k+2…Tj-1’,且满足1<k<j,则当匹配过程中Si≠Tj 时,仅需将模式串向右滑动至第k个字符和主串中第i个字符对齐,匹配仅需从Si、Tk的比较起继续进行,无需i指针的回溯。在匹配过程中为了尽可能“滑动”远一段的距离,因而应选择满足条件较大的k值。

若令next[j]=k,则next[]表明当模式中第j个字符与主串中相应字符“失配”时,在模式中需重新和主串中该字符进行比较的字符的位置。由此可引出模式串的next函数的定义:
请添加图片描述

由此可见next函数的计算仅和模式串本身有关,而和主串无关。其中’T1T2…Tk-1’是’T1T2…Tj-1‘’ 的真前缀子串,'Tj-k+1Tj-k+2…Tj-1’是’T1T2…Tj-1’的真后缀子串。当next函数定义中的集合不为空时 next[j]的值等于串’T1T2…Tj-1’的真前缀子串和真后缀子串相等时的最大子串长度+1

通过以上分析,推导出模式串’abaabcac’的next值的计算过程如表4.1所示。
(1)当j=1时,由定义得知,next[1]=0;
(2)当j=2时,满足1<k<j的k值不存在,由定义得知next[2]=1

请添加图片描述

模式串’abaabcac’的next函数值如图4-8所示。

在求得模式串的next函数之后,匹配可如下进行:假设以指针ij分别指示主串S和模式串T中当前比较的字符,令i的初始值为pos,j的初始值为1。若在匹配过程中Si=Ti,,则ij分别增1;否则,i不变,而j退到 next[j]的位置再比较(即Si和Tnext[j]进行比较),若相等,则指针各自增1,否则j再退到下一个next值的位置,以此类推,直至下列两种可能:一种是j退到某个next值(next[next[···next[j]]])时字符比较相等,则指针各自增1继续进行匹配;另一种是j退到next值为0(即与模式的第一个字符“失配”),则此时需将主串和模式串都同时向右滑动一个位置(此时j=0,当向右滑动一个位置时,即模式串的第一个字符),即从主串的下一个字符Si+1和模式Ti重新开始比较。图4-9是KMP匹配过程的一个例子(假设pos=1)

[算法4-13]KMP模式匹配算法

int Index_KMP( SString S, in pos, SString T){
    
    
	int i=pos,i=1;						//主串从第pos开始,模式事从头开始
	while(i<=S.len && j<=Tlen){
    
    
		if(j==0||S.ch[j]==T.ch[j]){
    
    		//继续比较后续宇符
			++i;++j;
		}else{
    
    
			j=next[j];					//模式申向右滑动
		}

if(j>T.len) return i-Tlen;				//匹配成功时,返回匹配的起始位置
else relurn 0							//匹配失败时,返回0

[next算法描述]
KMP算法是在已知模式next函数值的基础上执行的,那么,如何求得模式串的next函数值呢?

由定义可知next[1]=0,假设next[j]=k,这表明在模式串中存在’T1T2···Tk-1’='Tj-k+1Tj-k+2···Tj,这样的关系,其中k为满足1<k<j的某个值此时next[j+1]的值可能有以下两种情况。

(1)若Tj=Tk则表明在模式串中’T1T2···Tk-1’=‘Tj-k+1Tj-k+2···Tj

T=T~1~ ··· T~j-k+1~	···	T~j-1~ T~j~ ··· T~m~
					   	        =
T=		    T~1~    ···	T~k-1~ T~k~···
			长度为k

这就是说next[j+1]=k+1,即next[j+1]=next[j]+1

(2)若Tj≠Tk则表明在模式串中’T1T2···Tk-1’≠’Tj-k+1Tj-k+2···Tj

此时可将求next函数值的问题看成是一个模式匹配的问题,整个模式串既是主串又是模式串其中1<k’<k<j。

T=T~1~ ··· T~j-k+1~	··· T~j-k'+1~ ···	T~j-1~ T~j~ ··· T~m~
					   	        ≠
T=		    T~1~    ···	T~k-k'+1~ ···   T~k-1~  T~k~ ···
T=					    T~1~     ···	T~k'-1~ T~k'-next[k]~

①若Tj=Tk’,且k’=next[k],则next[j+1]=k’+1,即next[j+1]=next[k]+1,也相当于next[j+1]=next[next[j]]+1。
②若Tj≠Tk’则继续比较Tj和Tnext[k’]即比较Tj和Tnext[next[k]]
······
然后一直重复下去,若直到最后j=0时都未比较成功,则next[j+1]=1

通过以上分析,计算第j+1个字符的 next 值时,需看第j个字符是否和第j个字符的next值指向的字符相等。

由此推导出模式串T=abaabcac的next值计算过程如下。
①当j=1时,由定义得知,next[1]=0。

j=1
T=a
n=0

②当j=2时,满足1<h<j的值不存在,由定义得知 next[2]=1。

j=12
T=ab
n=01

③当j=3时,由于T2≠T1,且next[1]=0,则next[3]=1。

j=12345678
T=abaabcac
n=011
T~2~ ≠ T~next[2]~	(T~1~)
T~2~ ? T~next[1]~	(0)
next[3]=1

④当j=4时,由于T3=T1则next[4]=next[3]+1,即next[4]=2。

j=12345678
T=abaabcac
n=0112
  
T~3~ = T~next[3]~	(T~1~)
next[4]=next[3]+1

⑤当j=5时,由于T4≠T2,且next[2]的值是1,故继续比较T4和T1,由于T4=T1,则next[5]=next[2]+1,即next[5]=2。

j=12345678
T=abaabcac
n=01122
  
T~4~ ≠ T~next[4]~	(T~2~)
T~4~ = T~next[2]~	(T~1~)
next[5]=next[2]+1

⑥当j=6时,由于T5=T2,则next[6]=next[5]+1,即next[6]=3。

j=12345678
T=abaabcac
n=011223
  
T~5~ = T~next[5]~	(T~2~)
next[6]=next[5]+1

⑦当j=7时,由于T6≠T3,且next[3]的值是1,故继续比较T6和T1,由于T6≠T1,且 next[1]=0,则next[7]=1。

j=12345678
T=abaabcac
n=0112231
  
T~6~ ≠ T~next[6]~	(T~3~)
T~6~ ≠ T~next[3]~	(T~1~)
T~6~ ? T~next[1]~	(0)
next[7]=1

⑧当j=8时由于T7=T1,则next[8]=next[7]+1,即next[8]=2。

j=12345678
T=abaabcac
n=01122312
  
T~7~ = T~next[7]~	(T~1~)
next[8]=next[7]+1,

故得出该模式串的next值如图4-8所示。

j=12345678
T=abaabcac
n=01122312

[算法4-14]next算法

void Get_Next( SString T, int next[]){
	int j=1,k=0;
	next[1]=0;
	while(j<T.len){
		if(k==0||T.ch[j]==Tch[k] ){
			++j;
			++k;
			nexi[j]=k;
		}else{
			k=next[k];
		}
	}
}

[nextval算法描述]

上述定义的next函数在某些情况下尚有缺陷。假设主串为’aaabaaaab’模式串为’aaaab’,则模式串对应的next函数值如下所示。

在求得模式串的next值之后,匹配过程如图4-10(a)所示。

从串匹配的过程可以看到,当i=4,j=4时,S4不等于T4,由next[j]所示还需进行i=4、j=3;i=4、j=2;i=4、j=1这三次比较。实际上,因为模式申中的第1,2,3个字符和第4个字符都相等(即都是a)因此,不需要再和主串中第4个字符相比较,而可以将模式一次向右滑动4个字符的位置直接进行i=5、j=1的字符比较。

这就是说,若按上述定义得到next[j]=k,而模式串中Tj=Tk,则当Si≠Tj时,不需要进行Si和Tk的比较,直接和Tnext[k]进行比较;换句话说,此时的next[j]的值应和next[k]相同,为此将next[j]修正为nextval[j]。而模式串中Tj≠Tk则当Si≠Tj时,还是需要进行Si和Tk的比较,因此nextval[j]的值就是k,即nextval[j]的值就是next[j]的值。

通过以上分析,计算第j个字符的 nextval值时,要看第j个字符是否和第j个字符的next指向的字符相等。若相等,则nextval[j]=nextval[next[j]];否则,nextval[j]=next[j]。由此推出模式串T='aaaab’的nextval值计算过程如下。

①当j=1时,由定义得知,nextval[1]=0。
②当j=2时,由nex[2]=1,且 T2=T1,则nextval[2]=nextval[1],即nextval[2]=0
③当j=3时,由next[3]=2,且T3=T2,则nextval[3]=nextval[2],即nextval[3]=0。
④当j=4时,由next[4]=3,且T4=T3则nextval[4]=nextval[3],即nextval[4]=0。
⑤当j=5时,由next[5]=4,且T5≠T4,则nextval[5]=next[5],即nextval[5]=4。

模式串’aaaab’的nextal函数值如下所示。

在求得模式串的nextval值之后,匹配过程如图4-10(b)所示。

求nextval函数值有两种方法,一种是不依赖next数组值,直接用观察法求得;另一种方法是如上所述的根据next数组值进行推理,在这里仅介绍第二种方法。

[算法4-15] nextval算法

void Get_NextVal(SString T, int next[ ] ,int nextval[ ]){
    
    
	int j=2,k=0;
	Get_Next(T,next);//通过算法4-14获得T的next值
	nextval[1]=0;	
	while (j<=T.len )
		k=next[j];
		if(T.ch[j]==T.ch[k]) nextval[j]=nextyal[ k];
		else nextval[j]=next[j];
		j++;
	}

[KMP算法分析]

KMP算法是在已知模式的 next或nextval的基础上执行的,如果不知道它们两者之一,则没有办法使用KMP算法。虽然有next和 nextval之分,但它们表示的意义和作用完全一样,因此在已知next或nextval进行匹配时,匹配算法不变。

通常模式串的长度m比主串的长度n要小很多且计算next或nextval函数的时间复杂度为0(m)。因此,对于整个匹配算法来说,所增加的计算next或nextval是值得的。

BF算法的时间复杂度为0(nxm),但是实际执行中m往往是远远小于n的,故近似于0(n+m),因此至今仍被采用。KMP算法仅当模式串与主串之间存在许多“部分匹配”的情况下,才会比 BF算法快。KMP算法的最大特点是主串的指针不需要回溯,整个匹配过程中,主串仅需从头到尾扫描一次,对于处理从外设输入的庞大文件很有效,可以边读边匹配。

课本

练习:

abaabcac
01122312

aaaab
01234

abaabcabc
011223123

ababcabaababb
0112312342345

abcabaa
0111232

abcaabbabcabaacbacba
01112231234532211211

结果与语言课本完全一样

import java.util.Arrays;

public class KMP0 {
    
    

    public static final int MAXLEN=50;

    static class SString{
    
    
        char[] ch=new char[MAXLEN+1];//0号单元不使用
        int len;

        public SString() {
    
    
        }

        public SString(char[] ch, int len) {
    
    
            this.ch = ch;
            this.len = len;
        }
    }

    public static void main(String[] args) {
    
    
//        test1();
        test2();

    }

    private static void test2() {
    
    
//        String t0="0abaabcac";
//        String t0="0abaabcabc";
        String t0="0aaaab";

        SString t=new SString(t0.toCharArray(),t0.length()-1);

        int[] next=new int[t.len+1];//0号也不使用
        get_next(t,next);

        System.out.println(Arrays.toString(next));

        int[] nextval=new int[t.len+1];//0号也不使用
        get_nextval(t,next,nextval);
        System.out.println(Arrays.toString(nextval));

    }


    private static void test1() {
    
    
        String s0="0aaabaaaab";
        String t0="0aaaab";
        SString s=new SString(s0.toCharArray(),s0.length()-1);
        SString t=new SString(t0.toCharArray(),t0.length()-1);

        int[] next=new int[t.len+1];//0号也不使用
        get_next(t,next);
        int i = index_kmp(s, 0, t,next);
        System.out.println(i);
    }


    public static int index_kmp(SString s,int pos,SString t,int[] next){
    
    
        int i=pos,j=1;
        while (i<=s.len&&j<=t.len){
    
    
            if (j==0||s.ch[i]==t.ch[j]){
    
    
                ++i;
                ++j;
            }else {
    
    
                j=next[j];
            }
        }
        if(j>t.len) return i-t.len;
        else return 0;
    }

    public static void get_next(SString t,int[] next){
    
    
        int j=1,k=0;
        next[1]=0;
        while (j<t.len){
    
    
            if(k==0||t.ch[j]==t.ch[k]){
    
    
                ++j;
                ++k;
                next[j]=k;
            }else{
    
    
                k=next[k];
            }
        }
    }

    public static void get_nextval(SString t,int[] next,int[] nextval){
    
    
        int j=2,k=0;
        get_next(t,next);
        nextval[1]=0;
        while (j<=t.len){
    
    
            k=next[j];
            if(t.ch[j]==t.ch[k]){
    
    
                nextval[j]=nextval[k];
            }else {
    
    
                nextval[j]=next[j];
            }
            j++;
        }
    }
}

Java中实现算法

官方题解

class Solution {
    
    
    public int strStr(String haystack, String needle) {
    
    
        int n = haystack.length(), m = needle.length();
        if (m == 0) {
    
    
            return 0;
        }
        int[] pi = new int[m];
        for (int i = 1, j = 0; i < m; i++) {
    
    
            while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
    
    
                j = pi[j - 1];
            }
            if (needle.charAt(i) == needle.charAt(j)) {
    
    
                j++;
            }
            pi[i] = j;
        }
        for (int i = 0, j = 0; i < n; i++) {
    
    
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
    
    
                j = pi[j - 1];
            }
            if (haystack.charAt(i) == needle.charAt(j)) {
    
    
                j++;
            }
            if (j == m) {
    
    
                return i - m + 1;
            }
        }
        return -1;
    }
}


调用Java的API

class Solution {
    
    
    public int strStr(String haystack, String needle) {
    
    
       return haystack.indexOf(needle);
    }
}

参考Java的API

就是利用了BF算法

class Solution {
    
    
    public static int strStr(String haystack, String needle) {
    
    
        char[] haystackValue=haystack.toCharArray();
        char[] needleValue=needle.toCharArray();
        return index(haystackValue,needleValue);
    }

    public static int index(char[] source, char[] target) {
    
    
        int sourceOffset=0;
        int targetOffset=0;
        int sourceCount=source.length;
        int targetCount=target.length;
        int max=sourceOffset+(sourceCount-targetCount);

        char first=target[targetOffset];

        for(int i=sourceOffset;i<=max;i++){
    
    
            //找第一个字符
            if (source[i] != first) {
    
    
                while (++i <= max && source[i] != first);
            }
            //匹配剩余的部分
            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){
    
    
                    return i-sourceOffset;
                }
            }
        }

        return -1;
    }
}

BF算法

class Solution {
    
    
    public int strStr(String haystack, String needle) {
    
    
        return indexWithBF(haystack,0,needle);
    }

    public static int indexWithBF(String s,int pos,String t){
    
    
        int i=pos,j=0;//主串从pos开始,模式串从头开始
        while(i<s.length()&&j<t.length()){
    
    
            if(s.charAt(i)==t.charAt(j)){
    
    //当对应字符相等时,比较后续字符
                i++;
                j++;
            }else{
    
                          //对应字符不等时
                i=i-j+1;                //主串回溯到i-j+1的位置重写比较
                j=0;                    //模式串从头开始重写比较
            }
        }
        if(j>=t.length()) return i-t.length();  //匹配成功时,返回匹配起始位置
        else return -1;                         //匹配失败时,返回-1
    }
}

KMP算法

class Solution {
    
    
    public int strStr(String haystack, String needle) {
    
    
        return indexWithKMP(haystack,0,needle);
    }

    public static int indexWithKMP(String s,int pos,String t){
    
    
        int[] next=next(t);

        int i=pos,j=0;                      //主串从pos开始,模式串从头开始
        while(i<s.length()&&j<t.length()){
    
    
            if(j==-1||s.charAt(i)==t.charAt(j)){
    
    //继续比较后续字符
                i++;
                j++;
            }else{
    
                          //模式串向右滑动
                j=next[j];
            }
        }
        if(j>=t.length()) return i-t.length();  //匹配成功时,返回匹配起始位置
        else return -1;                         //匹配失败时,返回-1
    }

    public static int[] next(String t){
    
    
        int len=t.length();
        int[] next=new int[len+1];
        int j=0,k=-1;
        next[0]=-1;
        while (j<len){
    
    
            if(k==-1||t.charAt(j)==t.charAt(k)){
    
    
                j++;
                k++;
                next[j]=k;
            }else{
    
    
                k=next[k];
            }
        }
        return next;
    }
}

C中实现算法

KMP算法

int strStr(char* haystack, char* needle) {
    
    
    int n = strlen(haystack), m = strlen(needle);
    if (m == 0) {
    
    
        return 0;
    }
    int pi[m];
    pi[0] = 0;
    for (int i = 1, j = 0; i < m; i++) {
    
    
        while (j > 0 && needle[i] != needle[j]) {
    
    
            j = pi[j - 1];
        }
        if (needle[i] == needle[j]) {
    
    
            j++;
        }
        pi[i] = j;
    }
    for (int i = 0, j = 0; i < n; i++) {
    
    
        while (j > 0 && haystack[i] != needle[j]) {
    
    
            j = pi[j - 1];
        }
        if (haystack[i] == needle[j]) {
    
    
            j++;
        }
        if (j == m) {
    
    
            return i - m + 1;
        }
    }
    return -1;
}

最后

我们都有光明的未来

祝大家考研上岸
祝大家工作顺利
祝大家得偿所愿
祝大家如愿以偿
点赞收藏关注哦

猜你喜欢

转载自blog.csdn.net/qq_51625007/article/details/132129456