数据结构之字符串匹配算法(BF算法和KMP算法)

字符串匹配算法: 就是给定两个串,主串(s)和子串(sub), 查找子串是否在主串里面,如果找到便返回子串在主串中第一个元素的位置下标,否贼返回-1,。 在这里我
们讨论的时候主要用字符串来举例实现。 总共有两个算法,分别为BF算法和它的优化算法KMP算法
首先我们来讲一下BF算法,BF 算 法 即 朴 素 算 法 :它 的 实 现 方 式 是 这 样 的 , 假 定 我 们 给 出 字 符串 ”ababcabcdabcde”作为主串, 然后给出子串: ”abcd”,现在我们需要查找子串是否在主串中出现, 出现返回主串中的第一个匹配的下标, 失败返回-1;
数据结构之字符串匹配算法(BF算法和KMP算法)_第1张图片
我们看上图,这是两个字符串,上面是主串,下面是子串,BF算法的基本原理用绿字写在旁边,大家可以动手画一下来体会这个过程。
那么,我用代码来实现一下BF算法函数:

int BF(Str *s,Str *sub,int pos)
{
    if(pos<0||pos>s->length)
    {
        return -1;
    }

    int i = pos;//pos是传的参数,表示从主串的那个位置开始匹配
    int j = 0;

    int lens = s->length;
    int lensub = sub->length;

    while (i < lens && j < lensub)//进入while的条件是标记i和j都没有走完主串和子串
    {
        if (s->elem[i] == sub->elem[j])//字符相等
        {
            i++;
            j++;
        }
        else//字符不相等
        {
            i = i - j + 1;//让i回到开始位置加1
            的地方,继续匹配
            j = 0;//让j指向子串的0号下标
        }
    }
    if (j >= lensub)//这里做一个判断,j>=lensub的情况下就是在主串中找到了子串,就返回i-j,i是pos位置,减去j等于走了一个子串的长度。
        {
            return i - j;
        }
        else 
            return -1;
}

其实它函数就这么一点,很简单,很好理解。BF函数有三个参数,分别是主串(s),子串(sub)和从那个位置开始查找的一个整型变量(pos)。注释很详细。

KMP 算法: 相对于 BF 算法来说 KMP 算法更为高效, 原因在于 BF 算法的时间复杂度是:
O(mn),M 代表主串的长度, n 代表子串的长度。 而 KMP 的话, 时间复杂度就变为 O(m+n);接下来我们看一下, 具体的实现过程, 还是一样, 我们举例来看: KMP 和 BF 唯一不一样的地方在,主串的 i 并不会回退到开始的位置加1的地方, 并且 j 也不会移动到 0 号位置。 我们先看个例子:
数据结构之字符串匹配算法(BF算法和KMP算法)_第2张图片
在这时,i并没有回退,但j退回到0.
数据结构之字符串匹配算法(BF算法和KMP算法)_第3张图片
KMP算法中标记量i,j的移动并不像BF算法那样,这里我们必须引进一个量K,K表示的是匹配失败时子串的回退下标,所有的K都存在一个next数组中,这个next数组说起来就是一个存下标的整型数组嘛,可计算起来就有点绕了。。。
记下来我们手动计算一下next数组的K值
数据结构之字符串匹配算法(BF算法和KMP算法)_第4张图片
上图是我求好next数组的一个字符串,接下来,我要用一段超级好理解的大白话来给你将K值的计算,首先,第一个元素的K值是-1,第二个元素的K值是0,这个是固定的,我们要从第三个元素开始,利用一二元素的K值来计算,先明确两点,1,计算哪个元素的K值要看前面的元素,不能包括自己,2,需找子字符串时必须以0号元素,也就是第一个元素开始,以要计算K值的那个元素的前一个元素结束,回到上图,第三个元素是c,我们只能在前两个字符的子串中找,而且要以a开始,b结束,很明显找不到两个一样的满足条件的真子串,所以2号下标元素的K值为0;3号下标元素为a,不包含自己前面有三个元素,真子串只能是a,b,c,ab,bc,很明显也找不出两个满足条件的真子串,所以3号下标的元素的K值为0;4号下标为b,前面有四个元素,以a元素开始,a元素结束,可以有的真子串是a,b,c,a,ab,bc,ca,abc,bca,可以看出只能是单个元素a瞒足条件,所以4号下标元素K值为1;5号下标元素是d,前面一共有五个字符元素,利用上面的方法很好找出满足条件的是子串ab,因为找出的子串长度为2,则5号下标元素的K值为2;6号下标元素为e,要找一个以a开始,d结尾的真子串,很明显没有,所以6号下标元素的K值为0,后面的7和8号下标元素的K值大家可以自己算算。
手动求K值就是这样一个方法,下面给大家两个例子,可以动手算算,答案在该博客末尾,计算完可以对比一下。
练习 1: 举例对于”abcababcabc”, 求其的 next 数组?
练习 2: 再对”abcabcabcabcdabcde”,求其的 next 数组?
明白了K值的定义和计算,那么我们现在进入到KMP算法最关键的地方,KMP算法的原理和公式推导
数据结构之字符串匹配算法(BF算法和KMP算法)_第5张图片
上图是我给出的一个例子,我们用j来做子串的标记,假设j++走到5号下标处不适配,我们可以算出5号下标元素的K值为2,所以表示紧挨者的j-1和j-2开始的0和1号下标的元素相同,所以在这里等于ab子串已经找过,5号下标d应该回退到2号下标,我们把3号下标设为k,把3号下标的元素设为Pk,跳出这个例子,next数组本身存放的就是j的回退路线下标K值,设0号下标元素为P0,
5号下标元素为Pj(因为此时j刚好调到5号下标元素处),那4号下标元素就是Pj-1。
数据结构之字符串匹配算法(BF算法和KMP算法)_第6张图片
由此可以轻松得到一个数学公式,即:P0 … Pk-1 = Px …Pj-1 因为前后找到的两个字符串本身就是相等的,所以在这样的顺序存储方式中,下标之差应该也是相等的,得到0 + k-1 = x + j-1 合并得x=j-k,所以上面等式可以写为P0 … Pk-1 = Pj-k … Pj-1。我们可以看出这个等式只是有关于下标j和k,那么作为Pk和Pj作为字符,它们比较只有两种结果,要么相等,要么不等,那么,我们先不放假设Pk = Pj,把Pk和Pj分别加到上面等式的两边可得,P0 … Pk = Pj-k … Pj(1),因为next数组本来就是存放K值的,所以next[i] = k(2),那么由(1)(2)两个等式可以找规律明显得出
next[j+1] = k+1,也就是说:
数据结构之字符串匹配算法(BF算法和KMP算法)_第7张图片
那么另一种Pk != Pj的情况呢?还是一样先来看一个例子:
数据结构之字符串匹配算法(BF算法和KMP算法)_第8张图片
当Pk!=Pj时,按相等的方法回退一次并不能满足,但是我们很容易发现,在回退一次的基础上再回退一次便可解决问题,所以这里很容易得出K = next[K];
原理和公式推导就讲到这里,如果你还是不清楚,建议你可以举例子来画着捋一捋思路。
接下来我用代码实现以下求next数组的算法

void GetNext(int* next,char* sub)
{
    int len = strlen(sub);
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int k = 0;
    while(iif((k == -1) || sub[k] == sub[i -1])//if和else就是我上面所说的Pk = Pj和Pk!=Pj的情况,这里k=-1进入的原因是存在回退到0号下标元素的情况,因为它的K值为-1,所以需要执行一次k++,来让它置为0
        {
            next[i] == k+1;
            i++;
            k++;
        }
        else
        {
            k = next[k];
        }
    }
}

代码实现KMP算法函数:

int KMP(const char* s,char* sub,int pos)
{
    int lens = strlen(s);
    int lensub = strlen(sub);
    int i = pos;
    int j = 0;

    int* next = (int*)malloc(sizeof(int)*lensub);//malloc动态开辟内存,大小为sub串的长度乘sizeof取类型
    GetNext(next,sub);//通过上面写的GetNext函数自动获取next数组

    while(i < lens && j < lensub)//while循环的条件是标记值i和j分别没有走完主串和子串,如果主串走完说明没有匹配到,子串走完说明已经匹配到,这是都跳出while循环
    {
        if( (j == -1) || s[i] == sub[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];
        }
    }
    free(next);//这里一定要记得释放next动态开辟的内存,否则会发生内存泄漏的情况
    if(j >= lensub)
        {
            return i - j;
        }
    else
        return -1;
}

最后,完整代码加运行结果奉上:

#include
#include
#include
#include
void GetNext(int* next,char* sub)
{
    int len = strlen(sub);
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int k = 0;
    while(iif((k == -1) || sub[k] == sub[i -1])
        {
            next[i] = k+1;
            i++;
            k++;
        }
        else
        {
            k = next[k];
        }
    }
}

int KMP(const char* s,char* sub,int pos)
{
    int lens = strlen(s);
    int lensub = strlen(sub);
    int i = pos;
    int j = 0;

    int* next = (int*)malloc(sizeof(int)*lensub);
    GetNext(next,sub);

    while(i < lens && j < lensub)
    {
        if( (j == -1) || s[i] == sub[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];
        }
    }
        free(next);
    if(j >= lensub)
        {
            return i - j;
        }
    else
        return -1;
}



int BF(char *s,char *sub,int pos)
{
    if(pos<0 || pos>strlen(s))
    {
        return -1;
    }

    int i = pos;
    int j = 0;

    int lens = strlen(s);
    int lensub = strlen(sub);

    while (i < lens && j < lensub)
    {
        if (s[i] == sub[j])
        {
            i++;
            j++;
        }
        else
        {
            i = i - j + 1;
            j = 0;
        }
    }
    if (j >= lensub)
        {
            return i - j;
        }
        else 
            return -1;
}

int main()
{
    char *s = "abefzkrbc";
    char *sub = "zkr";
    printf("BF   =%d\n", BF(s, sub, 0));
    printf("KMP   =%d\n", KMP(s,sub,0));

    return 0;
}

数据结构之字符串匹配算法(BF算法和KMP算法)_第9张图片

练习答案:第一题:-1 0 0 0 1 2 1 2 3 4 5
第二题:-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0
这是我学习中的浅见,如果那里有问题,或者不全面,之后会继续补充,谢谢!

你可能感兴趣的:(数据结构)