数据结构~串(主要记录一下KMP)

本文深入探讨了字符串的KMP算法,包括串的顺序和链式储存结构,以及模式匹配中的BF算法和KMP算法。重点在于KMP算法的next数组计算,通过实例解析了next数组的意义和优化方法,提供了C++和Java的完整实现代码。
摘要由CSDN通过智能技术生成

数据结构~串(包含KMP详解)

这一小章节就是字符串,主要需要弄明白的就是KMP算法,这个算法搞懂后,这章节就OK了,下面先啰嗦一下串的两种基本储存结构:1、顺序储存,2、链式储存。然后再提一下BF算法,然后详细说一下KMP算法。

1:串的两种基本储存结构
1-1:顺序储存

用一组地址连续的储存单元储存串值的字符序列(储存空间利用率和算法的方便性较好),串多采用此结构

#define MAX 255

///定长串
typedef struct {
    char ch[MAX+1]; //储存串的数组,定义从1开始,所以MAX+1
    int length;     //串当前长度
}MyString1;

///堆式顺序储存结构
typedef struct {
    char *ch;       //空串的时候ch为NULL
    int length;     //串当前长度
}MyString2;

1-2:链式储存

用单链表储存串,这就比较活了,一个节点可以放一个字符,也可以放多个,一个节点存放多个字符的时候最后一个节点要是没被赋满值的话就补上‘#’,(链式结构的好处就是方便插入删除,弥补了串顺序储存的短板)

#define CHUNK 10  ///定义一个块的大小

typedef struct Chunk {
    char ch[CHUNK]; 
    struct Chunk *next;
}Chunk;

typedef struct {
    Chunk *head,*tail; //串的头尾指针
    int length;           //串的当前长度
}MyString;

2:串的模式匹配

2-1:经典BF

BF:Brute-Force,一看这名字,暴力+强迫==依靠蛮力获得,简单直观,就是从头到尾匹配字符,回溯为i=i-j+2(因为这里的串从1开始,i 和 j 都是从1开始,想一下主串1~n,现在比到了i位置,模式串到了j位置,此时出现了T.ch[j]!=S.ch[i],那么i应该回溯到i-j+2,j回到1位置),代码也很简单直观,这里直接给出代码,略过解释

int Index(Mytring S,Mytring T,int pos){ 
/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0。 */
/* 其中,T非空,1≤pos≤S.length。*/
    int i,j;
    if(1<=pos&&pos<=S)
    {
        i=pos;
        j=1;
        while(i<=S.length&&j<=T.length)
            if(S.ch[i]==T.ch[j]){
                ++i;
                ++j;
            }else {
                i=i-j+2;//i回溯到开始比的位的下一位
                j=1;    //j回到1
            }
        if(j>T.length)
            return i-T.length;
        else
            return 0;
    }
    else
    return 0;
}

2-2:KMP算法

这个算法是由3个大佬Knuth+Morris+Pratt同时设计实现,简称KMP算法。
这个算法直接省去 i 的回溯,就是说 i 只需要一直往前走就行。
而且,甚至 可以说 让这个 真正的 比对 过程 直接 跟模式串 没什么 关系了
(因为比对之前,已经获取好模式串的next数组),
太过牛逼,在此敬佩一波…。

KMP,这个算法主要就是求next数组。

要想求next数组,在此引入一个概念,就是前缀子串和后缀子串(随便取个名字),
比如串(ASDFGHJKL)
这里的第5个位置G前(包括G) ASDF 和 SDFG 就是 5 这个位置以内的最长的且长度相等的前缀子串和后缀子串。

ok,我们开始解释next数组的含义:比如next[4]=2,代表的就是4这个位置以内的:
(最长的 并且 长度相等的 并且 长得一样的 )前缀子串和后缀子串的长度为2。
同时next[4]=2也代表着4位置的下一个到2位置。

要是还是感觉没了解,那我再举个例子,比如串(a b a b a a a b a b a a)的next[5]=3
5这个位置的字符为a,相等的前后缀子串的最长的长度为:aba的长度,即3

注意注意!!!这里就要进阶一下了(KMP的真正的next数组)

看next[6],要是按照上面的过程求的话,就应该是next[6]=1,即最长前后缀子串‘a’的长度。
但是!但是这样求的话,要是考研出个选择题,那可就over了,因为这里的第6个字符一看next为1,
但是再看,6前面的字符串ababa,发现了什么,前三个字符 ‘aba’ 这时候已经在6前面的aba对比过了,
所以此时只需要从第四个字符:‘b’继续进行对比就行。
所以next[6]就应该是4,而不是1,(毕竟4还比1少3次)
7这个位置的字符为a,相等的前后缀子串的最长的长度为:a的长度,即1

这里的next数组可以求出来:(当然有一件事得确定,我这里举的这个例子,串是从1位置开始的)

ababaaababaa
next[1]next[2]next[3]next[4]next[5]next[6]next[7]next[8]next[9]next[10]next[11]next[12]
011234223456

当然,上面只是简单的介绍一下next数组是怎么求的,要是仅仅对付考试的话,了解这么多,会肉眼观察快速得出一个模式串的next数组就行。下面给出KMP算法的代码:

int Index_KMP(MyString S,MyString T,int pos){
    int i=pos,j=1;
    while (i<=S.length && j<=T.length){
        if (j==0 || S.ch[i]==T.ch[j]){++i;++j;}
        else j=next[j];
    }
    if (j<T.length) return i-T.length;
    else return 0;
}

下面在给出初始化模式串的next数组(T_next)的代码:

void Init_next(MyString T,int T_next[]){
    int i=1,j=0;
    T_next[1]=0;
    while(i<T.length){
        if (j==0 || T.ch[i]==T.ch[j]){++i;++j;T_next[i]=j;}
        else j=T_next[j];
    }
}

这个算法还可以继续优化:

比如: 模式串:aaaab
主串:aaabaaaab

此时模式串的next数组为:

aaaab
next[1]next[2]next[3]next[4]next[5]
01234

此时对比到i=4和j=4的时候由于next[4]=3;
所以还得进行i=4;j=3的对比,
然而T.ch[4]==T.ch[next[4]]==T.ch[next[next[4]]]==T.ch[next[next[next[4]]]],
多了一些不必要的对比,所以还可以继续优化,
可以判断主串中的S.ch[i]==T.ch[j]的时候,
再判断一下T.ch[j]和T.ch[next[j]]是否相等。
如果不相等,next[i]就还是等于j;
如果相等,由于是从左往右对比的,next[i]就等于next[j]即可;

优化只需要多一句代码:

void Init_next(MyString T,int T_next[]){
    int i=1,j=0;
    T_next[1]=0;
    while(i<T.length){
        if (j==0 || T.ch[i]==T.ch[j]){
            ++i;++j;
            if(T.ch[i]!=T.ch[j}) T_next[i]=j;
            else T_next[i]=T_next[j];
        }
        else j=T_next[j];
    }
}

对于改进后的模式串的next数组,这里也给出书上的例题:
串: ababaabab
next: 011234234
nextval:010104101

next过程:
a b a b a a b a b
next1=0;
next2=1;
next3=length(a)=1;(最长相等前后缀长度)
next4=2(由于第一个字符a,在4前面那个字符已经比较过,所以j此时从2继续)
next5=length(aba)=3;(最长相等前后缀长度)
next6=4(由于前三个字符aba,在6前面那个3字符已经比较过,所以j此时从4继续)
next7=length(ab)=2;(最长相等前后缀长度)
next8=length(aba)=3;(最长相等前后缀长度)
next9=length(abab)=4;(最长相等前后缀长度)

改进的nextval过程:
a b a b a a b a b
nextval1=0;
nextval2=1;
nextval3=0;(此时i=3位置的a,与j=1位置的a相等,所以nextval3=nextval1=0,然后++i,++j)
nextval4=1;(此时i=4位置的b,与j=2位置的b相等,所以nextval4=nextval2=1,然后++i,++j)
nextval5=0;(此时i=5位置的a,与j=3位置的a相等,所以nextval5=nextval3=0,然后++i,++j)
nextval6=4;(注意这时候的i=6,j=4即a!=b,
此时nextval6=j=4;也就是等于next数组中的next6,
同时j=nextval[j]=1,++i,++j)
nextval7=1;(此时i=7位置的b,与j=2位置的b相等,所以nextval7=nextval2=1;,然后++i,++j)
nextval8=0;(此时i=8位置的a,与j=3位置的a相等,所以nextval8=nextval3=0;,然后++i,++j)
nextval9=1;(此时i=9位置的b,与j=4位置的b相等,所以nextval9=nextval4=1)

思路是否逐渐清晰?再试两道题,求它们的next和nextval数组,看看答案是否跟自己写的一样
1:“abcaabbabcab”
2:“abcabaa”

答案:
1:

串:abcaabbabcab
next:011122312345
nextval:011021301105

2:

串:abcabaa
next:0111232
nextval:0110132

这里再记录一段代码(完整的KMP算法代码c++、java两种):

c++:

#define MAX 255
#include <bits/stdc++.h>
using namespace std;

typedef struct {
    char ch[MAX+1]; //储存串的数组
    int length;     //串当前长度
}MyString;

void Init_next(MyString T,int T_next[]){
    int i=1,j=0;
    T_next[1]=0;
    while(i<T.length){
        if (j==0 || T.ch[i]==T.ch[j]){++i;++j;T_next[i]=j;}
        else j=T_next[j];
    }
}

/*///改进的next数组取法
void Init_next(MyString T,int T_next[]){
    int i=1,j=0;
    T_next[1]=0;
    while(i<T.length){
        if (j==0 || T.ch[i]==T.ch[j]){
            ++i;++j;
            if (T.ch[i]!=T.ch[j]) T_next[i]=j;
            else T_next[i]=T_next[j];
        }
        else j=T_next[j];
    }
}
*/
int Index_KMP(MyString S,MyString T,int pos){
    int next[strlen(T.ch)+1];
    Init_next(T,next);
    cout<<"模式串的next数组为:";
    for(int ll=1;ll<=T.length;ll++){
        cout<<next[ll]<<" ";
    }
    cout<<endl;
    int i=pos,j=1;
    while (i<=S.length && j<=T.length){
        if (j==0 || S.ch[i]==T.ch[j]){++i;++j;}
        else j=next[j];
    }
    if (j>T.length) return i-T.length;
    else return 0;
}

int main() {
    MyString s1,s2;
    string s,t;
    ///注意我这里是写的必需先输入主串,然后再输入模式串,
    ///而且主串比模式串长,这个才有意义,不然还研究个啥
    ///有解题需要的,可以自己加一些判断输入是否合法,这里就不加了
    cin>>s>>t;
    for (int i = 0; i < s.length(); ++i) {
        s1.ch[i+1]=s.at(i);
    }
    for (int i = 0; i < t.length(); ++i) {
        s2.ch[i+1]=t.at(i);
    }
    s1.length=s.length();
    s2.length=t.length();
    cout<<Index_KMP(s1,s2,1)<<endl;
    return 0;
}

java:

package myString;

import java.util.Scanner;

public class MyString {
    public static void main(String[] args) {
        String s,t;
        Scanner sc=new Scanner(System.in);
        s=sc.nextLine();
        t=sc.nextLine();
        s="#"+s;
        t="#"+t;
        System.out.println(Index_KMP(s,t,1));
    }

    public static void Init_next(String T,int[] T_next){
        int i=1,j=0;
        T_next[1]=0;
        while(i<T.length()-1){
            if (j==0 || T.charAt(i)==T.charAt(j)){
                ++i;++j;
                if (T.charAt(i)!=T.charAt(j)) T_next[i]=j;
                else T_next[i]=T_next[j];
            }
            else j=T_next[j];
        }
    }

    public static int Index_KMP(String S,String T,int pos){
        int[] next=new int[T.length()+1];
        Init_next(T,next);
        System.out.print("模式串的next数组为:");
        for(int ll=1;ll<=T.length();ll++){
            System.out.print(next[ll]+" ");
        }
        System.out.println("\n");
        int i=pos,j=1;
        while (i<=S.length()-1 && j<=T.length()-1){
            if (j==0 || S.charAt(i)==T.charAt(j)){++i;++j;}
            else j=next[j];
        }
        if (j>T.length()-1) return i-T.length()+1;
        else return 0;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值