字符串匹配算法

KMP 算法

算法介绍

Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由 Donald KnuthVaughan PrattJames H.Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。

kmp 算法其实就是暴力的优化。

先看暴力,每次失配时,都只跳一位,时间复杂度为 O(nm)O(nm)

我们想想怎么才能让每次失配时都能跳更多位。

设主串为 aa,模式串为 pp

这需要引入一个数组 nextnext,记 nextinext_i 为从 p0p_0 往后数,同时从 pip_i 往前数相同的位数,在保证前后缀相同的情况下,最多能数多少位。(即为 pp 的一个非 pp 本身的子串 tt,满足 tt 既是 pp 的前缀,又是 pp 的后缀,且 tt 的位数最大。)

例如:

子串 nextnext
ABA\color{RED}{A}\color{grey}{B}\color{RED}{A} 11
ABCDCBA\color{RED}{ABC}\color{grey}{D}\color{RED}{CBA} 33
ABABABABABAB\color{RED}{ABAB}\color{grey}{AB} \\ \color{grey}{AB}\color{RED}{ABAB} 44

那求出了 nextnext 数组又有什么用呢?

看一下一个例子:

ABACABABACABADABAC\color{Red}{ABA}\color{green}{B}\color{grey}{ACABAD}

ABACABAD\color{Red}{ABA}CABA\color{green}{D}

当前匹配到的是 BBDD,那现在要让模式串向右移,如果当前在第 jj 位,那么再从 nextjnext_j 开始匹配就行了,即 j=nextj,因为j=next_j,因为 ABA既是既是p的前缀,又是的前缀,又是p的后缀,且的后缀,且ABA的位数最大,为的位数最大,为3$。

QQ:这是有人会问了,上面求得 nextnext 数组时用于 pp 串的,那为什么对 aa 串也适用呢?

AA:首先可以想到当 pp 串的某一个字母 xx 失配时,它前面的都匹配(都相等)了,那上面为例,当前失配的字符在第 77 位,对于 pp 串的区间 [4,6][4,6] 肯定是与 pp 串的 next6next_6(值为 33,串为 pp 的区间 [0,2][0,2])相等的,因为根据 nextnext 的定义,pp 串的区间 [0,2][0,2]pp 串的区间 [4,6][4,6] 是相等的,那前面说过,xx 字母前面的都是相等的了,那么 pp 串的区间 [4,6][4,6] 肯定与 aa 串的区间 [4,6][4,6] 相等,得出 pp 串的区间 [0,2][0,2] 肯定与 aa 串的区间 [4,6][4,6] 相等,那么让模式串的开头与 aa 串第 44 位对齐即可。

那么会变成以下的情况:

ABACABABACABADABAC\color{Red}{ABA}\color{green}{B}\color{grey}{ACABAD}

ABACABAD\quad \quad \quad \color{Red}{ABA}\color{green}{C}\color{grey}{ABAD}

匹配失败,那就继续跳。

那如何求 nextnext 数组呢?

直接自我匹配就行了。

pmt[0] = 0;
for (int i = 1, j = 0; i < p.length(); ++i)
{
    while (j >= 0  && p[i] != p[j])
        j = j ? pmt[j - 1] : -1;
    pmt[i] = ++j;
}

求出了 nextnext 数组,就啥都结束了。

一直到匹配成功就没了,彻底没了~

最后的主串与模式串的匹配

for (int i = 0, j = 0; i < s.length(); ++i)
{
	while (s[i] != p[j] && j >= 0)
		j = j ? pmt[j - 1] : -1;
    ++j;
    if (j == p.length())
    {
    	cout << i - j + 2 << endl;
    	j = pmt[j - 1];
    }
}

代码实现

#include <bits/stdc++.h>
using namespace std;
int pmt[1000005];
string s, p;
int main()
{
    cin >> s >> p;
    pmt[0] = 0;
    int cnt = 0;
    for (int i = 1, j = 0; i < p.length(); ++i)
    {
        while (p[i] != p[j] && j >= 0)
            j = j ? pmt[j - 1] : -1;
        pmt[i] = ++j;
    }
    for (int i = 0, j = 0; i < s.length(); ++i)
    {
        while (s[i] != p[j] && j >= 0)
            j = j ? pmt[j - 1] : -1;
        ++j;
        if (j == p.length())
        {
            cout << i - j + 2 << endl;
            j = pmt[j - 1];
        }
    }
    for (int i = 0; i < p.length(); ++i)
        cout << pmt[i] << ' ';
    cout << endl;
    return 0;
}

Sunday 算法

算法介绍

Sunday 算法由 Daniel M.Sunday1990 年提出,它的思想跟 BM 算法很相似:

只不过 Sunday 算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。

  • 如果该字符没有在模式串中出现则直接跳过,即移动位数 = 模式串长度 + 1;
  • 否则,其移动位数 == 模式串长度 - 该字符最右出现的位置(以 00 开始) == 模式串中该字符最右出现的位置到尾部的距离 +1+ 1

举个例子

主串:a b c d e f g h i

模式串:f g h

  1. 左对齐,依次比较字符。

| a\color{red}{a} | b\color{red}{b} | c\color{red}{c} | d\color{red}{d} | e\color{red}{e} | f\color{red}{f} | g\color{red}{g} | h\color{red}{h} | i\color{red}{i} | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

| f\color{red}{f} | g\color{red}{g} | h\color{red}{h} | \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

  1. 发现模式串首位(f)与主串首位(a)无法匹配,主字符串参与匹配最末尾字符的下一个字符(d)不存在于模式串中(f g h),就需要将模式串向后移动(模式串长度 3 + 1)位。(即 ef 对齐)

| a\color{green}{a} | b\color{red}{b} | c\color{red}{c} | d\color{blue}{d} | e\color{red}{e} | f\color{red}{f} | g\color{red}{g} | h\color{red}{h} | i\color{red}{i} | | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

| f\color{green}{f} | g\color{red}{g} | h\color{red}{h} | \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

\Huge{\Downarrow}

| a\color{red}{a} | b\color{red}{b} | c\color{red}{c} | d\color{red}{d} | e\color{green}{e} | f\color{red}{f} | g\color{red}{g} | h\color{blue}{h} | i\color{red}{i} | | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

| \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | f\color{green}{f} | g\color{red}{g} | h\color{red}{h} | \kern0.5em | \kern0.5em | | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

  1. 发现模式串首位(g)与主串首位(f)无法匹配,主字符串参与匹配最末尾字符的下一个字符(h)存在于模式串中(f g h),将模式串向后移动(模式串中最右端的该字符到模式串末尾的距离 (3 - 3) + 1)。(即让两个 h 对齐)

| a\color{red}{a} | b\color{red}{b} | c\color{red}{c} | d\color{red}{d} | e\color{red}{e} | f\color{green}{f} | g\color{red}{g} | h\color{red}{h} | i\color{red}{i} | | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

| \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | \kern0.5em | f\color{green}{f} | g\color{red}{g} | h\color{red}{h} | \kern0.5em | | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |

  1. 发现模式串首位(f)与主串首位(f)匹配,继续遍历发现所有字符匹配成功。

代码实现

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

const int maxNum = 1005;

int shift[maxNum];
int Sunday(const string &T, const string &P)
{
    int n = T.length();
    int m = P.length();
    for (int i = 0; i < maxNum; i++)
    {
        shift[i] = m + 1;
    }
    for (int i = 0; i < m; i++)
    {
        shift[P[i]] = m - i;
    }

    int s = 0;
    int j;
    while (s <= n - m)
    {
        j = 0;
        while (T[s + j] == P[j])
        {
            j++;
            if (j >= m)
            {
                return s;
            }
        }
        s += shift[T[s + m]];
    }
    return -1;
}

signed main()
{
    string T, P;
    while (true)
    {
        getline(cin, T);
        getline(cin, P);
        int res = Sunday(T, P);
        if (res == -1)
        {
            cout << "主串和模式串不匹配。" << endl;
        }
        else
        {
            cout << "模式串在主串的位置为:" << res << endl;
        }
    }
    return 0;
}
posted @ 2021-08-17 21:00  蒟蒻orz  阅读(2)  评论(0编辑  收藏  举报  来源