算法导论——字符串搜索KMP算法

KMP算法,俗称Kill Myself Personally。。。不过今天总算明白了。

理论

KMP算法不容易想,比较晦涩难懂。不过当你知道了它的思想后,算法实现就变得稍微容易起来。在我的上一篇博客算法导论——字符串搜索FiniteMachine有限状态机实现中,我们提到了状态 q q q q q q代表的是模式串 P P P和文本串 T T T已匹配的字符个数; 另外,我们还提到了状态转移表。事实上,KMP算法的核心便是基于有限状态机的改进——如何才能将二维的表转换成一个线性的表,并且通过状态 q q q实现字符串匹配?同样,最简单的,我们用一个例子来说明KMP算法。我们假设模式串 P = { a , b , a , d } P=\{a,b,a,d\} P={a,b,a,d}

前缀表

什么是前缀表?为什么我们要计算前缀表?事实上,我们现在学习的很多东西都是先知道是什么,然后才进行进一步的讲解。这实际上是不对的。我们应该先知道为什么,然后才能去归纳出方法,形成定义。所以,我将从“为什么”出发来解释前缀表。下面给出一幅图:
在这里插入图片描述
这就是KMP算法的全部思想,OK,讲述完毕。大家散了吧。
开个玩笑。从图中,我们可以看到,模式串 P P P的前3个元素与文本串 T T T相匹配,第4个元素不匹配。因为模式串 P P P的第三个元素(作为前三个元素的后缀)与模式串 P P P的第一个元素(作为前三个元素的前缀)相同,因此,下一次比较,我们直接可以滑动到如图所示的位置即模式串 P P P下标为1的地方进行。如果对于一个给定的模式串 P P P,我们知道了它的所有元素对应的滑动性质,那么我们便能够很快地进行字符串匹配了。前人据此定义出了前缀表,其具体定义我不愿多提,因为数学语言较为复杂,大家可以在书本上看到。下面我们给出模式串 P P P的前缀表。

在这里插入图片描述

KMP算法实现思路

现在我们已经有了我们的前缀表,我们把它命名为 Π \Pi Π Π [ q ] 代 表 如 果 模 式 串 P 的 第 q + 1 个 与 文 本 串 T [ i ] 不 匹 配 时 , 我 们 应 该 把 模 式 串 向 前 挪 动 q − Π [ q ] 的 距 离 。 \Pi[q]代表如果模式串P的第q+1个与文本串T[i]不匹配时,我们应该把模式串向前挪动q-\Pi[q]的距离。 Π[q]Pq+1T[i]qΠ[q](看上面的例子, Π [ 3 ] \Pi[3] Π[3]代表 P [ 3 + 1 ] ≠ T [ i ] , 即 d ≠ c , 时 , 我 们 应 该 把 模 式 串 P 向 前 移 动 q − Π [ q ] = 3 − 1 = 2 的 距 离 , 此 时 q = 1 P[3+1] \ne T[i],即d\ne c,时,我们应该把模式串P向前移动q-\Pi [q]=3-1=2的距离,此时q=1 P[3+1]=T[i]d=cPqΠ[q]=31=2q=1)。这时大家就可以发现,这里的 q q q刚好就是我们的已经匹配的字符个数,即我们在有限状态机里的“状态”。状态的转移应该满足什么条件呢?

  • 如果 T [ i ] = P [ q + 1 ] , 则 q = q + 1 。 例 如 : T[i]=P[q+1],则q=q+1。例如: T[i]=P[q+1],q=q+1

在这里插入图片描述
此时 q = 1 , 即 已 经 匹 配 了 1 个 字 符 ‘ a ’ , 且 T [ i ] = b , P [ q + 1 ] = b , T [ i ] = P [ q + 1 ] , 所 以 匹 配 字 符 应 该 加 1 , 即 p = 2 q=1,即已经匹配了1个字符‘a’,且T[i]=b,P[q+1]=b,T[i]=P[q+1],所以匹配字符应该加1,即p=2 q=11aT[i]=b,P[q+1]=b,T[i]=P[q+1]1p=2

  • 如果 T [ i ] ≠ P [ q + 1 ] , 则 如 果 q > 0 , q = Π [ q ] , 一 直 循 环 。 就 以 上 面 的 例 子 为 例 : T[i]\ne P[q+1],则如果q>0,q=\Pi[q],一直循环。就以上面的例子为例: T[i]=P[q+1],q>0,q=Π[q]

在这里插入图片描述
此时 q = 3 , 因 为 匹 配 了 3 个 字 符 ′ a b a ′ , 而 T [ i ] = c , P [ q + 1 ] = d , T [ i ] ≠ P [ q + 1 ] , 我 们 应 该 将 模 式 串 向 前 滑 动 , 滑 动 到 哪 里 呢 ? 就 是 q = Π [ q ] = 1 的 位 置 了 , 即 此 时 只 匹 配 了 一 个 字 符 , 而 此 时 T [ i ] = c , P [ q + 1 ] = b , T [ i ] ≠ P [ q + 1 ] , 此 时 应 该 继 续 滑 动 模 式 串 , 即 q = Π [ q ] = 0 , 也 就 是 说 , 此 时 文 本 串 和 模 式 串 没 有 相 匹 配 的 元 素 了 。 q=3,因为匹配了3个字符'aba',而T[i]=c,P[q+1]=d,T[i]\ne P[q+1],我们应该将模式串向前滑动,滑动到哪里呢?就是q=\Pi[q]=1的位置了,即此时只匹配了一个字符,而此时T[i]=c,P[q+1]=b,T[i]\ne P[q+1],此时应该继续滑动模式串,即q=\Pi[q] = 0,也就是说,此时文本串和模式串没有相匹配的元素了。 q=3,3abaT[i]=c,P[q+1]=d,T[i]=P[q+1]q=Π[q]=1T[i]=c,P[q+1]=b,T[i]=P[q+1]q=Π[q]=0直到下一次,我们有 T [ i ] = P [ q + 1 ] 时 T[i]=P[q+1]时 T[i]=P[q+1],这时,我们就能依据原则一进行下一轮的操作了。如下图所示:
在这里插入图片描述

  • q = m 时 , 也 就 是 模 式 串 的 所 有 字 符 都 已 经 匹 配 。 这 时 , 我 们 就 记 录 下 结 果 , 并 令 q = Π [ q ] , 因 为 此 时 我 们 将 要 准 备 下 一 轮 的 K M P 了 。 q=m时,也就是模式串的所有字符都已经匹配。这时,我们就记录下结果,并令q=\Pi[q],因为此时我们将要准备下一轮的KMP了。 q=mq=Π[q]KMP为了说明这种情况,我们假设模式串 P = { a , b , a } P=\{a,b,a\} P={a,b,a},举出下述例子:

在这里插入图片描述

前缀表实现原理

前缀表的实现也可看成是字符串匹配,只不过是自己匹配自己,但是这里要注意的是 Π [ 0 ] = 0 \Pi[0]=0 Π[0]=0,也就是说,作为文本串的模式串不考虑 P [ 0 ] , 作 为 模 式 串 的 模 式 串 不 考 虑 P [ m ] P[0],作为模式串的模式串不考虑P[m] P[0]P[m]。也就是说,匹配的初始状态如下图:

在这里插入图片描述
事实上,我们可以这么认为。作为文本串的模式串相当于后缀,而作为模式串的模式串相当于前缀,如果他们匹配上了,就说明此时前缀与后缀相等,于是,我们就能够知道模式串的前缀表了。
好了,不多BB了,开撸。

C#实现

Program.cs

using System;
using System.Collections.Generic;

namespace StringMatch
{
    class Program
    {
        static void Main(string[] args)
        {
            
            string T = "Star, I Want to Love with U, I'm so in Love with U";
            string CharacterSet = T;
            string P = "Love with U";


            Console.WriteLine("KMP算法");
            KMP kMP = new KMP();
            result = kMP.KMPStrategy(P, T);
            GetResult(result, P, T);
        }



        private static void GetResult(List<int> result, string P, string T)
        {
            for (int i = 0; i < result.Count; i++)
            {
                Console.WriteLine("在位置" + (result[i] + 1) + "找到:");
                for (int j = result[i]; j < result[i] + P.Length; j++)
                {
                    Console.Write(T[j]);
                }
                Console.WriteLine(" ");
            }
            Console.WriteLine("==========================================");
        }

        private static void GetResult(List<int> result)
        {
            for(int i = 0; i < result.Count; i++)
            {
                Console.Write(result[i]);
                Console.Write(" ");
            }
            Console.WriteLine(" ");
            Console.WriteLine("==========================================");
        }

    }
}

KMP.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace StringMatch
{
    public class KMP
    {
        /*演算
         * 计算P[0...k] = P[n-k...n], k = 0,...,n-2,n-1
         * 为什么是n-1?
         * 因为我们是计算P的前缀和后缀,如果说k=n,那么两个P[0...k] = P[n-k...n]就变成
         * P[0...n] = P[0...n],显然,这种情况并不是我们所说的前后缀, 两个字符串重合了。
         * 问题转换为一个字符串匹配问题,不过匹配点直接从1开始,而不是0.
         */
        public List<int> CalculatePreffixTable(string P)
        {
            int q = 0;
            int m = P.Length;
            List<int> pi = new List<int>();
            pi.Add(0);
            for(int i = 1; i < m; i++)
            {
                while(q > 0 && P[q] != P[i])
                {
                    q = pi[q - 1];
                }
                if (P[q] == P[i])
                {
                    q++;
                }
                pi.Add(q);
            }

            return pi;
        }

        public List<int> KMPStrategy(string P, string T)
        {
            List<int> result = new List<int>();
            int m = P.Length;
            int q = 0;
            List<int> pi = CalculatePreffixTable(P);
            for(int i = 0; i < T.Length; i++)
            {
                while(q > 0 && P[q] != T[i])
                {
                    q = pi[q - 1];
                }
                if(P[q] == T[i])
                {
                    q++;
                }
                if (q == m)
                {
                    result.Add(i - m + 1);
                    q = pi[q - 1];
                }
            }

            return result;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值