【数据结构】KMP算法

算法简介


传统暴力算法和KMP算法

设定主串的长度为n,字串的的长度为m。

传统的暴力字符串匹配算法理论上最多需要花费O(nm)的时间复杂度才能完成串的匹配操作,但是在实际使用中,往往也能够以接近O(m+n)的时间复杂的完成匹配操作,因此现在仍被广泛使用。

而KMP算法则理论上最坏可以在O(m+n)的时间复杂度内完成字符串的匹配操作。

暴力匹配算法低效的根源是因为会进行很多次重复的匹配操作,即主串中存在回溯。而KMP算法中,主串的指针的移动方向一直是往右的。例如:

当模式串为0000001,而主串为000000000000000000000000000000000000000001时,由于模式中前6个字符均为“0”,主串中前45个字符均为“0”, 每趟匹配都是比较到模式的最后-一个字符时才发现不等,指针i需回溯40次,总比较次数为40x7= 280次。


KMP算法原理

字符串的前缀、后缀、部分匹配值

要了解子串的结构,首先要弄清楚几个概念:前缀、后缀和部分匹配值。前缀指除最后一个字符以外,字符串的所有头部子串;后缀指除第一个字符外,字符串的所有尾部子串;部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度。下面以’ababa’为例进行说明:

  • 'a’的前缀和后缀都为空集,最长相等前后缀长度为0。
  • 'ab’的前缀为{a},后缀为{b}, {a}n{b} = 0,最长相等前后缀长度为0。
  • 'aba’的前缀为{a,ab},,ba},{a,ab}n{a,ba}={a},最长相等前后缀长度为1。
  • 'abab’的前缀{a,ab,aba}n后缀{b,ab,bab}={ab},最长相等前后缀长度为2。
  • 'ababa’的前缀{a,ab,aba,abab}o后缀{a,ba,aba,baba}={a,aba},公共元素有两个,最长相等前后缀长度为3。故字符串’ababa’的部分匹配值为00123。

这个部分匹配值有什么作用呢?

回到最初的问题,主串为a b c a c b a b,子串为 a b c a c。

利用上述方法可以写出字串abcac的部分匹配之为 00010, 将其写成数组形式,就得到了部分匹配值(Partial Match,PM)的表。

编号 1 2 3 4 5
S a b c a c
PM 0 0 0 1 0

使用PM表匹配字符串

主串 a b a b c a b c a c b a b
b字串 a b c

第一趟匹配过程:
发现c与a不匹配,前面的 2 个字符’ab’是匹配的,查表可知,最后一个匹配字符 b对应的部分匹配值为0,因此按照下面的公式算出子串需要向后移动的位数:移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为2-0=2,所以将子串向后移动 2 位,如下进行第二趟匹配:

主串 a b a b c a b c a c b a b
字串 a b c a c

第二趟匹配过程:
发现c与b不匹配,前面4个字符’abca’是匹配的,最后一个匹配字符a 对应的部分匹配值为1,4-1=3,将子串向后移动3位,如下进行第三趟匹配:

主串 a b a b c a b c a c b a b
字串 a b c a c

子串全部比较完成,匹配成功。整个匹配过程中,主串始终没有回退,故KMP算法可以在O(n + m)的时间数量级上完成串的模式匹配操作,大大提高了匹配效率。

某趟发生失配时,如果对应的部分匹配值为 0,那么表示已匹配相等序列中没有相等的前后缀,此时移动的位数最大,直接将子串首字符后移到主串 i 位置进行下一趟比较;如果已匹配相等序列中存在最大相等前后缀(可理解为首尾重合),那么将子串向右滑动到和该相等前后缀对齐(这部分字符下一趟显然不需要比较),然后从主串i位置进行下一趟比较。


next数组的生成

仔细观察不难发现:

  • 第一个元素右移以后空缺的用-1 来填充,因为若是第一个元素匹配失败,则需要将子串向右移动一位,而不需要计算子串移动的位数。
  • 最后一个元素在右移的过程中溢出,因为原来的子串中,最后一个元素的部分匹配值是其下一个元素使用的,但显然已没有下一个元素,故可以舍去。这样,上式就改写为Move=(j-1)-next[j],相当于将子串的比较指针 j 回退到j=j-Move=j-((j-1)-next[j])=next[j]+1

有时为了使公式更加简洁、计算简单,将 next 数组整体+1。因此,上述子串的next 数组也可以写成

编号 1 2 3 4 5
S a b c a c
next 0 1 1 1 2

最终得到子串指针变化公式j=next[j]。
next[j]的含义是:在子串的第 j 个字符与主串发生失配时,则跳到子串的next[j]位置重新与主串当前位置进行比较。


优化生成方法

当i=4、j=4时,s4跟pa(b+a)失配,如果用之前的 next 数组还需要进行 s4与 P3、S4与 P2、S4与 P1这3 次比较。事实上,因为 pnext [4]-3=p4=a、pnext 13]=2=p3=a、pnext [2]-1=p2=a显然后面3次用一个和p4相同的字符跟 s4比较毫无意义,必然失配。

那么问题出在哪里呢?

问题在于不应该出现 Pj = Pnext[j]理由是:当 Pj != Sj时,下次匹配必然是 Pnext[j] 跟 Sj 比较,如果Pj=Pnext[j],那么相当于拿一个和 Pj 相等的字符跟 Sj 比较,这必然导致继续失配,这样的比较毫无意义。那么如果出现了 Pj = Pnext[j] 应该如何处理呢?

如果出现了,则需要再次递归,将 next[j]修正为 next[next[j]],直至两者不相等为止


算法实现

完整代码

#include 
#include 
#include 

#define MAXLEN 255   // 定义最大串长

// 串的结构
typedef struct
{
    char ch[MAXLEN];    // 串元素
    int length;         // 串当前长度
} SString;

 
/**
 * 初始化一个空串,数组下标从1开始
 * SString t        要初始化的空串
 */
SString initSString(SString t)
{
    // 将串长设置为0
    t.length = 0;
    // 将首元素赋值(没有这个也行,因为第一个元素没什么用)
    t.ch[0] = '_';
    return t;
}

// 串赋值操作,将char数组中的值赋值给SString结构体

/**
 * 串赋值操作,将char数组中的值赋值给SString结构体
 * SString  t       要赋值的空串
 * char chars[]     赋值的内容保存在该数组中
 */
SString strAssign(SString t, char chars[])
{
    // 从下标1开始
    int i = 1;
    // 循环遍历将chars中的字符复制到t的ch数组中,同时长度+1
    // 串结束的标志为'\0'
    for(i; chars[i-1] != '\0' && i < MAXLEN; i++)
    {
        t.ch[i] = chars[i-1];
        t.length++;
    }
    return t;
}

/**
 * 获取Next数组的函数,用于给next数组赋值,下标也是从1开始
 * SString  t       模式串,即要匹配的字串
 */
void getNextVal(SString t, int nextval[])
{
    int i = 1, j = 0;
    // 第一个字符的next数组值默认为0
    nextval[1] = 0;
    // 循环赋值
    while(i < t.length)
    {
        // 如果是第一个字符,或者当前两个字符相等,i和j同时后移一位
        if (j == 0 || t.ch[i] == t.ch[j])
        {
            i++;
            j++;
            // next[i] 的值就是j的值,因为j是当前新的前缀的尾,所以j表示当前前后缀的长度
            nextval[i] = j;
        }
        // 否则j回退,回退的值是next[j]
        else
        {
            j = nextval[j];
        }
    }
}
/**
 * 优化后的获取Next数组的函数,用于给next数组赋值,下标也是从1开始
 * SString  t       模式串,即要匹配的字串
 */
void getNext(SString t, int nextval[])
{
    int i = 1, j = 0;
    nextval[1] = 0;
    while(i < t.length)
    {
        if (j == 0 || t.ch[i] == t.ch[j])
        {
            i++;
            j++;
            if (t.ch[i]!=t.ch[j])
                nextval[i] = j;
            else
                nextval[i] = nextval[j];
        }
        else
        {
            j = nextval[j];
        }
    }
}
/**
 * 匹配函数,用于匹配串s中知否包含串t,若包含,返回串t首字母第一次在串s中出现的位置
 * SString  s       父串,即要被匹配的串
 * SString  t       模式串,即要匹配的子串
 */
int indexKMP(SString s, SString t, int next[])
{
    // 初始化下标从1开始
    int i = 1, j = 1;
    // 当两个串都没有匹配完时循环遍历
    while (i <= s.length && j <= t.length)
    {
        // 如果模式串的指针为0或者当前两个串指针所指的值相同,两个指针同时后移一位
        if (j == 0 || s.ch[i] == t.ch[j])
        {
            i++;
            j++;
        }
        // 否则将模式串的指针前移next[j]位,也就相当于模式串向后移动next[j]位
        else
        {
            j = next[j];
        }
    }
    // 当两个串中有一个结束遍历,判断模式串是否遍历完,如果模式串遍历完,则匹配成功
    if (j > t.length)
    {
        // 模式串的第一次出现下标 = i - t.length
        return i - t.length;
    }
    else
    {
        // 否则s中不包含串t,返回0(因为下标是从1开始的,所以可以返回0,如果从0开始,可以返回-1)
        return 0;
    }
}

int main()
{
    // 定义两个串
    SString t, s;
    // 初始化两个串
    t = initSString(t);
    s = initSString(s);
    // 两个串的内容
    char charsS[] = {'a', 'b', 'a', 'b', 'c', 'a', 'b', 'c', 'a', 'c', 'b', 'a', 'b', '\0'};
    char charsT[] = {'a', 'b', 'c', 'a', 'c', '\0'};
    // 分别赋值
    t = strAssign(t, charsT);
    s = strAssign(s, charsS);
    // 创建next数组,长度位模式串长度+1,因为下标从1开始
    int next[strlen(charsT)];
    // 根据模式串填充next数组
    getNext(t, next);
    // 匹配模式串
    int res = indexKMP(s, t, next);

    if (res == 0)
    {
        printf("Not match!" );
    }
    else
    {
        printf("The index is %d", res);
    }

    return 0;
}

程序输出
【数据结构】KMP算法_第1张图片

你可能感兴趣的:(算法与数据结构,算法,数据结构)